PTY (Pseudo Terminal)
PTY 提供了虚拟终端功能,支持在 TUI 中运行命令。
概述
PTY 使用 bun-pty 库创建伪终端会话,支持多个客户端同时连接到同一个终端会话。PTY 会话可以创建、更新、调整大小和删除。
定义位置
packages/opencode/src/pty/index.ts:18-64
主要对象
Info
PTY 会话信息。
| 属性名 | 类型 | 必填 | 说明 | | --------- | --------- | -------- | -------------- | -------- | | id | string | 是 | PTY 唯一标识符 | | title | string | 是 | 显示标题 | | command | string | 是 | 执行的命令 | | args | string[] | 是 | 命令参数列表 | | cwd | string | 是 | 工作目录 | | status | "running" | "exited" | 是 | 会话状态 | | pid | number | 是 | 进程 ID |
CreateInput
创建 PTY 会话的输入。
| 属性名 | 类型 | 必填 | 说明 |
|---|---|---|---|
command | string | 否 | 执行的命令(默认使用 shell) |
args | string[] | 否 | 命令参数 |
cwd | string | 否 | 工作目录(默认项目目录) |
title | string | 否 | 显示标题 |
env | Record<string, string> | 否 | 环境变量 |
UpdateInput
更新 PTY 会话的输入。
| 属性名 | 类型 | 必填 | 说明 |
|---|---|---|---|
title | string | 否 | 新的标题 |
size | object | 否 | 新的终端大小 |
size.rows | number | - | 行数 |
size.cols | number | - | 列数 |
TypeScript 类型定义
export type Info = {
id: string
title: string
command: string
args: string[]
cwd: string
status: "running" | "exited"
pid: number
}
export type CreateInput = {
command?: string
args?: string[]
cwd?: string
title?: string
env?: Record<string, string>
}
export type UpdateInput = {
title?: string
size?: {
rows: number
cols: number
}
}
事件
| 事件名 | 类型 | 说明 |
|---|---|---|
pty.created | info: Info | PTY 创建 |
pty.updated | info: Info | PTY 更新 |
pty.exited | {id, exitCode} | PTY 退出 |
pty.deleted | {id} | PTY 删除 |
常量配置
const BUFFER_LIMIT = 1024 * 1024 * 2 // 2MB 缓冲区限制
const BUFFER_CHUNK = 64 * 1024 // 64KB 发送块大小
典型使用场景
1. 创建 PTY 会话
const pty = await Pty.create({
command: "bash",
cwd: "/home/user/project",
title: "Bash Session",
})
console.log(pty.id) // pty_xxx
console.log(pty.status) // "running"
2. 使用默认 Shell
// 不指定命令,使用默认 shell
const pty = await Pty.create({
cwd: "/home/user/project",
title: "Terminal",
})
// 自动使用 shell.preferred()
3. 创建带参数的命令
const pty = await Pty.create({
command: "npm",
args: ["test", "--watch"],
cwd: "/home/user/project",
title: "Test Runner",
})
4. 列出所有 PTY 会话
const sessions = Pty.list()
for (const session of sessions) {
console.log(`${session.title}: ${session.status}`)
}
5. 获取 PTY 会话信息
const session = Pty.get(ptyID)
`if (session) {
console.log(`PID: ${session.pid}`)
console.log(`Command: ${session.command}`)
console.log(`Status: ${session.status}`)
}`
6. 更新 PTY 会话
// 更新标题
await Pty.update(ptyID, {
title: "New Title",
})
// 调整终端大小
await Pty.update(ptyID, {
size: {
rows: 40,
cols: 120,
},
})
7. 删除 PTY 会话
await Pty.remove(ptyID)
// 这会:
// 1. 终止进程
// 2. 关闭所有 WebSocket 连接
// 3. 从状态中删除
// 4. 发布 pty.deleted 事件
8. 连接客户端到 PTY
const handlers = Pty.connect(ptyID, wsContext)
if (handlers) {
// 处理客户端消息
ws.onMessage((message) => {
handlers.onMessage(message)
})
// 处理客户端断开
ws.onClose(() => {
handlers.onClose()
})
}
9. 调整 PTY 大小
// 快速调整大小
Pty.resize(ptyID, 120, 40)
10. 写入 PTY
// 发送输入到 PTY
Pty.write(ptyID, "ls -la\n")
11. 监听 PTY 事件
Bus.subscribeAll(async (event) => {
switch (event.type) {
case "pty.created":
const { info } = event.properties
console.log(`PTY created: ${info.title}`)
break
case "pty.exited":
const { id, exitCode } = event.properties
console.log(`PTY ${id} exited with code ${exitCode}`)
break
case "pty.deleted":
console.log(`PTY ${event.properties.id} deleted`)
break
}
})
PTY 会话生命周期
客户端连接
WebSocket 连接
// 当客户端连接时
const handlers = Pty.connect(ptyID, ws)
// 处理器包含:
handlers.onMessage // 接收客户端输入
handlers.onClose // 客户端断开
数据转发
缓冲区管理
输出缓冲
PTY 维护一个输出缓冲区:
buffer: string
// 当有新数据时
session.buffer += data
// 限制缓冲区大小
if (session.buffer.length > BUFFER_LIMIT) {
session.buffer = session.buffer.slice(-BUFFER_LIMIT)
}
新客户端连接
当新客户端连接时,发送当前缓冲区:
if (session.buffer) {
// 分块发送,避免过大消息
for (let i = 0; i < buffer.length; i += BUFFER_CHUNK) {
ws.send(buffer.slice(i, i + BUFFER_CHUNK))
}
}
环境变量
PTY 会话自动包含以下环境变量:
{
...process.env,
TERM: "xterm-256color", // 启用 256 色
...input.env, // 用户指定的环境变量
}
Shell 处理
如果命令以 sh 结尾,自动添加 -l 标志:
if (command.endsWith("sh")) {
args.push("-l")
}
最佳实践
- 资源清理: 始终在退出时调用
remove() - 缓冲区限制: 尊意
BUFFER_LIMIT避免内存泄漏 - 错误处理: 捕获并记录 PTY 错误
- 并发安全: 使用状态管理避免竞争条件
- 事件监听: 订阅 PTY 事件以更新 UI
性能考虑
批量发送
// 分批发送大缓冲区
const CHUNK_SIZE = 64 * 1024
for (let i = 0; i < buffer.length; i += CHUNK_SIZE) {
ws.send(buffer.slice(i, i + CHUNK_SIZE))
}
活动客户端检查
// 检查 WebSocket 状态
ws.readyState !== 1 // 不等于 OPEN
对象关系
变更历史
| 版本 | 变更内容 | 日期 |
|---|---|---|
| v1 | 初始 PTY 实现 | - |
| v1.1 | 添加缓冲区限制和分块发送 | - |
| v1.2 | 改进客户端连接管理 | - |