Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions components/execd/PTY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Interactive PTY sessions

Use this when you need a **long-lived Bash** driven over **WebSocket**: PTY mode behaves like a real terminal (colors, `stty`, resize); **pipe mode** (`pty=0`) splits stdout/stderr without a TTY. **Unix/macOS/Linux only** — not supported on Windows.

## Typical usage

1. **Create a session** (shell starts on the first WebSocket, not here):

```bash
curl -s -X POST http://127.0.0.1:44772/pty \
-H 'Content-Type: application/json' \
-d '{"cwd":"/tmp"}'
# → { "session_id": "<id>" }
```

2. **Open WebSocket** — default is PTY mode:

```
ws://127.0.0.1:44772/pty/<session_id>/ws
```

| Query | Use |
|-------|-----|
| `pty=0` | Pipe mode instead of PTY |
| `since=<offset>` | After reconnect, replay from byte offset (use `output_offset` from `GET /pty/:id`) |

3. **Traffic** — after a JSON `connected` frame, the server sends **binary** chunks: first byte is the channel (`0x01` stdout, `0x02` stderr in pipe mode only, `0x03` replay with an 8-byte offset header). Send **stdin** as binary: `0x00` + raw bytes. For **resize** / **signals** / **ping**, send **JSON text** frames, e.g. `{"type":"resize","cols":120,"rows":40}`, `{"type":"signal","signal":"SIGINT"}`, `{"type":"ping"}`.

4. **One WebSocket per session** — a second connection gets **409** until the first closes.

5. **End** — when Bash exits, you get a JSON `exit` frame with `exit_code` and the socket closes. Use **`DELETE /pty/:id`** to tear down the session from the server side.

## Modes

- **PTY (default)** — ANSI and TTY-aware tools work as usual.
- **Pipe** — `?pty=0`; stderr is separate binary frames. Good when you do not need a TTY.

## Notes

- Output is also buffered for **replay**; reconnect with `since=` to catch up.
- In PTY streams, **shell echo** may appear before your command’s real output, so avoid matching only on text that also appears in the typed line.
1 change: 1 addition & 0 deletions components/execd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ English | [中文](README_zh.md)
- **Command execution**: Synchronous and background shell commands
- **File operations**: Full filesystem CRUD with chunked upload/download
- **Monitoring**: Real-time host metrics (CPU, memory, uptime)
- **Interactive PTY**: Long-lived Bash over WebSocket (TTY or pipe mode, replay on reconnect) — see [PTY.md](./PTY.md)

## Core Features

Expand Down
1 change: 1 addition & 0 deletions components/execd/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- **命令执行**:同步执行和异步执行 shell 命令
- **文件操作**:完整的文件系统 CRUD,支持分块上传/下载
- **监控**:实时系统指标(CPU、内存、运行时间)
- **交互式 PTY**:通过 WebSocket 长连接 Bash(TTY 或管道模式、支持按偏移回放)— 详见 [PTY.md](./PTY.md)

## 核心特性

Expand Down
10 changes: 5 additions & 5 deletions components/execd/pkg/runtime/pty_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,13 @@ func TestPTYSession_ANSISequences(t *testing.T) {
_, err := s.WriteStdin([]byte("printf $'\\033[1;32mGREEN\\033[0m\\n'\n"))
require.NoError(t, err)

// Wait for "GREEN" to appear in the replay buffer.
require.True(t, replayContains(t, s, "GREEN", 5*time.Second),
"expected 'GREEN' in replay buffer")
// Wait for ESC from printf output. Do not match on "GREEN" alone: the echoed
// command line contains that substring before printf runs.
require.True(t, replayContains(t, s, "\x1b", 5*time.Second),
"expected ESC bytes in PTY replay buffer")

// PTY mode should propagate ESC bytes verbatim.
data, _ := s.replay.ReadFrom(0)
assert.Contains(t, string(data), "\x1b", "expected ESC bytes in PTY output")
assert.Contains(t, string(data), "GREEN", "expected GREEN text in PTY output")
}

func TestPTYSession_PipeMode(t *testing.T) {
Expand Down
Loading