Shell 执行与 PTY · 在伪终端里跑命令
让 agent 跑 shell 命令,远不止 spawn 一下。grok 有非流式与流式两条执行路径、跨平台的进程树 kill、边跑边写盘的日志,还有一个基于 portable-pty + alacritty 网格模拟的完整 PTY 控制器。这一章拆执行、流式、后台任务与 PTY。
Shell 执行与 PTY · 在伪终端里跑命令
「跑一条命令」是 agent 最危险也最常用的能力。grok 在这上面做的工程量,远超一句 spawn。
两条执行路径
TerminalRunner(这套执行器如今独立成 xai-grok-shell-terminal crate,xai-grok-shell 以 xai_grok_shell::terminal 重新导出)按 request.stream 分流:
LocalTerminalRunner(非流式,简单):bash -lc "命令"(Unix;Windows 走平台 shell 的 argv),stdout/stderr 管道读到底,timeout到了既start_kill直接子进程、也杀掉整个进程组(免得孙进程攥着管道继续跑),输出按 UTF-8 边界保留末尾 N 字节。StreamingLocalTerminalRunner(流式、可 kill,主路径):这条是重点。
两条都用 setsid 把子进程从 /dev/tty 摘开(非流式走 detach_command,流式走 process_wrap 的 ProcessSession),免得子进程抢终端;两条也都挂上了沙箱的子进程网络过滤(见下一章)。
流式路径的讲究
StreamingLocalTerminalRunner 维护一个全局进程注册表((session_id, terminal_id) → TerminalEntry),spawn 时用 process_wrap 做跨平台进程树 kill(Unix setsid 新进程组、Windows JobObject)+ KillOnDrop。核心是一个 tokio::select! 循环,四个竞争分支:读 stdout / 读 stderr / 定时 tick(默认 100ms 报一次进度)/ 钉住的超时 deadline。
两个细节值得学:
- 输出边读边写盘:每读到一段就立刻写进
output_file——在内存缓冲被截断之后,磁盘日志仍然完整。 - 通知必须 fire-and-forget:进度通知若阻塞,超时分支就会卡住,所以通知发送方契约上就是「发了不等」。
kill 与后台化
kill 一个跑飞的命令要干脆:kill_terminal 用有界回收(timeout(2s, child.wait()))——不会为等一个僵尸进程卡死。会话取消时 kill_and_release_all_for_session 分四步:短锁下抽干注册表 → 同步 SIGKILL 前台 → 后台任务重新插回 → 在独立 task 上回收,让取消路径瞬间返回。
模型侧的后台任务由 BackgroundTaskRegistry 管(每会话上限 10)。它的 wait_for_completion 有个经典的无丢失唤醒写法:先注册 notified() 再检查 completed——顺序反了就可能错过唤醒。
PTY:一个完整的伪终端控制器
有些命令(vim、top、交互式脚本)非得在真终端里跑才正常。grok 为此有 ptyctl——基于 portable-pty(开 PTY)+ alacritty_terminal(终端网格模拟)。PtySession::start 起四个角色:
最漂亮的是 wait.rs 的事件驱动 expect:等某段文字出现,不轮询——feeder 每喂完一批就把一个 watch 代-数 +1,等待方只在 generation 变化时复检(borrow_and_update 先于检查 = 无丢失唤醒);子进程退出(feeder 的 sender 被 drop)则标记 ended。还配了 axum 的 HTTP + WebSocket 控制面,把 PTY 的输入输出对外暴露。
shell 内部还有一套交互式 PTY(pty_session.rs),它的忙碌检测很巧:每 500ms 用 process_group_leader()(Unix tcgetpgrp)对比子进程 pid,判断前台有没有在跑活的进程——基于时间而非输出,因为一个忙进程可能一声不吭。
和《从 LLM 到 Coding Agent》对照
对照 tool-use 与 streaming-tool-execution:最小实现里跑命令就是 exec 拿 stdout。grok 的增量是——真实产品要处理:流式输出、跨平台进程树 kill、瞬时取消、后台任务、边跑边落盘、以及交互式程序必需的 PTY。下一章看跑命令这件事的另一半:怎么用 OS 沙箱把它关进笼子。