date: 2026-06-10 tags: [inbox, project/cli-agent, type/experiment-report] public: true
lh Local System Shell 进程生命周期实验
摘要
目标:单独追踪 lh Local System shell process lifecycle 问题,尤其是 killCommand / cleanupAll 是否能清理 shell 启动的完整 process tree。
当前判断:
killCommand返回success=true不代表 shell command 的全部子孙进程都已结束。- 对
timeout sh -c ... | head ...这类 pipeline/嵌套 shell 命令,lh当前只杀 direct child 时,子孙进程可能被 reparent 后继续运行。 - 这是独立于 IO buffer OOM 的第二条问题线,但两者会叠加:后台高输出任务被 kill 或父进程崩溃后,残留进程可能继续占用 CPU/IO/文件描述符。
相关背景笔记:lh-shell-io-buffer-pressure-experiment
背景机制
runCommand 使用 /bin/sh -c <command> 启动命令。真实命令通常不是单一进程,而是一个进程树:
- direct child:
/bin/sh -c ... - nested shell:例如
timeout 180s sh -c ... - pipeline 子进程:例如
find、head、yes、sleep
如果 killCommand 只对 direct child 调用 process.kill(),可能发生:
- direct child 退出。
- 子孙进程仍在原 process group 或新 process group 中继续运行。
- 子孙进程被 reparent 到其他父进程。
lh侧认为 shell session 已经被清理,但宿主机还有实际工作负载。
D1:killCommand 未清理 process tree
时间:2026-06-10 13:48。
实验环境:
| 项 | 值 |
|---|---|
| 工作目录 | /home/cy948/workspace/github/lobe-search-agent-eval |
| tmux session | lh-shell-exp |
| connect pane | lh-shell-exp:0.0 |
| agent pane | lh-shell-exp:0.1 |
| device gateway | http://localhost:8787 |
| agent gateway | http://localhost:8788 |
触发命令:
timeout 180s sh -c 'while :; do find /usr /home/cy948/workspace/github -maxdepth 8 -type f -print 2>&1; done | head -c 20971520; sleep 120'
复现步骤:
- 用
runCommandbackground 启动上面的命令。 - 记录返回的
shell_id,本轮为sh-1至sh-4。 - 对这些
shell_id调用killCommand。 - 观察工具返回
success=true。 - 在宿主机检查残留进程。
工具返回:
| shell_id | killCommand |
|---|---|
sh-1 | success=true |
sh-2 | success=true |
sh-3 | success=true |
sh-4 | success=true |
宿主机检查命令:
pgrep -af 'timeout 180s sh -c|find /usr /home/cy948/workspace/github|maxdepth 8'
ps -o pid,ppid,pgid,sid,stat,etime,args -p <pid-list>
残留进程示例:
timeout 180s sh -c while :; do find /usr /home/cy948/workspace/github -maxdepth 8 -type f -print 2>&1; done | head -c 20971520; sleep 120
sh -c while :; do find /usr /home/cy948/workspace/github -maxdepth 8 -type f -print 2>&1; done | head -c 20971520; sleep 120
ps 观察:
PID PPID PGID SID STAT COMMAND
1831610 2435 1831610 1753655 S timeout 180s sh -c ...
1831611 1831610 1831610 1753655 S sh -c ...
1831612 1831611 1831610 1753655 R sh -c ...
关键现象:
lh的 direct child 已经退出。- 残留进程不再是
lh connect的 direct child。 - 部分 PPID 变为
2435。 - 残留进程仍保留自己的 PGID。
结论:killCommand 当前语义不足以保证清理 process tree。
复现变体
R2 日志流变体
命令:
timeout 180s sh -c 'yes LOG_STREAM_LINE_abcdefghijklmnopqrstuvwxyz0123456789 | head -c 20971520; sleep 120'
用途:
- 同时复现 IO buffer OOM。
- OOM 后检查
yes/head/timeout/ nestedsh是否残留。 - 验证
lh connect崩溃时cleanupAll是否有机会执行,以及即使执行是否能清理完整 process tree。
初步修复方向
- Unix:启动 shell 时建立独立 process group,kill 时对 process group 发信号。
- Windows:使用 job object 或 tree-kill 类能力处理子孙进程。
killCommand:返回前应尽量确认 process tree 已退出,至少区分 signal sent 和 actually terminated。cleanupAll:进程退出和 gateway 断连时也应走同一套 process tree 清理逻辑。- background session:需要记录 process group / root pid / child pid 的生命周期元数据,便于观测和清理。
- 超时路径:foreground timeout、background kill、connect shutdown 应复用同一套 terminate 逻辑。
验证判据
修复前:
killCommand返回success=true。pgrep仍能找到timeout/ nestedsh/ pipeline 子进程。
修复后:
killCommand返回成功后,pgrep找不到该 shell session 关联的子孙进程。cleanupAll或lh connect正常关闭后,无残留子孙进程。- R1/R2 这类 pipeline 命令都能被清理。
- 普通 foreground 命令、background 命令、timeout 命令的结果语义不回归。
TODO
- 查看
packages/local-file-shell/src/shell/runner.ts的 spawn options 是否能设置独立 process group。 - 查看
packages/local-file-shell/src/shell/process-manager.ts的kill/cleanupAll,设计 process tree cleanup。 - 补一个最小自动化测试:启动 nested shell + pipeline,kill 后断言子孙进程不存在。
- 和 lh-shell-io-buffer-pressure-experiment 的 IO buffer 修复分开推进,避免两个问题互相遮蔽。