fix(runtime): 回收分离子进程,并按在线状态/退避收敛常驻轮询 - #801
Merged
Merged
Conversation
现场:主进程空闲时 `ps` 衰减均值 65.6%、`top` 稳态 13.3~41%,且每个工具会话在 父进程下留一个 <defunct>(其中一个明确是 Browser 工具拉起的 Chrome)。 `std::process::Child` 不实现 drop-reap——spawn 后丢弃句柄就没人 wait()。 - 新增 `runtime::process::spawn_and_reap` / `spawn_child_reaper`:启动器"不等它、 但要收尸",在分离线程里 wait() 掉子进程。替换 4 处会漏僵尸的写法: `commands/workspace/fs.rs` 的 open / explorer.exe / xdg-open 三个分支,以及 `commands/workspace/git.rs` 的 `spawn_system_file_manager`(文件管理器中显示)。 - 浏览器子进程(`services/browser/launcher.rs`):Child 句柄交给收尸线程,结构体只 保留 pid + 启动时间;Drop 改走 `terminate_process_tree_by_pid_if_same`——先比对 `ps -o etime` 反推的启动时间(±2s 容差)再发信号,避免收尸后 pid 被复用误杀。 - `runtime/managed_process.rs`:restored 条目的存活探测从每 2s 一次 fork/exec `ps` 放宽到 15s(遗留进程的存活粒度不需要秒级)。 - `services/gateway/controller.rs`:租约清扫与 runtime-status 重发布两条 5s 循环改为 仅在 `status().online` 时工作(此前未配置/未连上网关也照跑,后者每次都要重建并 protobuf 编码整份信封)。 - `commands/integration/mcp.rs`:SSE 重连从固定 1s 改为 1s→30s 指数退避,连上即复位。 - 新增回归测试:分离子进程不得停留在 defunct(`ps -o stat=` 观察,旧写法必失败)。
This was referenced Sep 11, 2026
coder-hhx
added a commit
that referenced
this pull request
Sep 11, 2026
v1.3.5 发布时 Windows x64 job 在 `Build Windows bundles` 失败:
error[E0425]: cannot find value `PID_START_TIME_TOLERANCE_MS` in this scope
--> crates\agent-gui\src-tauri\src\runtime\process.rs:246:56
#801 把这个常量定义为 `#[cfg(unix)]`(它的注释只讲了 `ps -o etime=` 的秒级精度),
但使用它的 `terminate_process_tree_by_pid_if_same` 是跨平台的:macOS / Linux 编译
都看不到问题,只有 Windows 目标才落到这条分支。
修复:常量改为所有平台可见,并补齐注释说明为何不能在 unix 下单独定义;windows 侧
`GetProcessTimes` 给的是精确创建时间,同一份 ±2s 宽容度只是稍宽,不会漏判 pid 复用。
验证:
- 把 `process.rs` 单独按 `--target x86_64-pc-windows-msvc` 做类型检查:修复前精确
复现同一处 E0425,修复后干净通过(本机缺 Windows SDK,整 crate 检查会死在
aws-lc-sys 的 C 构建)。
- `cargo check --lib`(host)无 warning;`cargo test --lib runtime::process` 3/3。
- 对 #801 全部改动做了 cfg 门控审计:新增引用到的 `spawn_and_reap` /
`spawn_child_reaper` / `terminate_process_tree_by_pid_if_same` /
`process_start_time_ms` 均无门控,此处是唯一的跨平台遗漏点。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue
Closes #798
Summary
现场(v1.3.3,无任何交互):主进程
ps衰减均值 65.6%、top稳态 13.3~41%,43 个线程;并且每个工具会话都在父进程下留一个
<defunct>(其中一个明确是 Browser 工具拉起的 Chrome 实例)。两件事其实是一个主题:子进程与常驻轮询都没有收敛。
僵尸
std::process::Child不实现 drop-reap:spawn()之后把句柄丢掉,子进程退出时没人wait(),就会一直挂在父进程下。本轮找到 4 处这种"启动器"写法:
commands/workspace/fs.rs:macOSopen/ Windowsexplorer.exe/ Linuxxdg-open三个分支。commands/workspace/git.rs的spawn_system_file_manager(git 面板"在文件管理器中显示")。浏览器路径机制不同:
services/browser/launcher.rs持有Child以便 kill,但子进程存活期间(整个会话)没有任何 tick 会
try_wait它——用户手关窗口或它崩掉后,defunct 一直挂到会话结束。做法:新增
runtime::process::spawn_and_reap/spawn_child_reaper("不等它、但要收尸",分离线程里
wait()),替换上面 4 处;浏览器改为把Child交给收尸线程,结构体只保留pid + 启动时间,
Drop走新增的terminate_process_tree_by_pid_if_same——先比对ps -o etime反推的启动时间(±2s 容差)再发信号。收尸之后 pid 可能被内核复用,直接按 pidKILL 会误伤无辜进程;这个守卫也顺带修掉了旧
terminate_process_tree_by_pid的同一隐患(探测失败 Unknown 时仍沿用"保守发信号"的旧语义,不改变既有取舍)。
已经正确、没有改动的地方(避免重复"修"):
shell_runner(wait_timeout收尸)、managed_process的 live 条目(try_wait)、MCP stdio、cua_driver、power_activity的
caffeinate(stop()里kill + wait)、git clone/subagent_worktree(wait_with_output)。空闲 CPU
按贡献排序核对后,改了其中 4 项(都是"无条件常驻"的确定性浪费):
runtime/managed_process.rsfork/exec一次ps -p <pid> -o etime=services/gateway/controller.rs租约清扫status().online时工作services/gateway/controller.rsstatus 重发布commands/integration/mcp.rsSSE 重连未覆盖、需要单独处理的最大单项:每个 workdir 一个递归 FSEvents 监听 + 聚合线程
(外加 git 元数据目录的额外递归监听),不随窗口可见性收敛
(
services/workspace_watch/watcher.rs)。构建产物/编辑器自动保存这类高频写入会持续产生事件。之所以没在本 PR 里做:需要把可见性信号接到
WorkspaceWatchService::set_desired且不能影响 gateway 来源的监听集合,属于必须在真机上验证行为的功能改动,收益大但风险不匹配,
已在 #798 里单列。前端网关模式的 1s idle poll 与网关重连退避上限(5s→30~60s)同属行为权衡,
也没动。
Change scope
crates/agent-gui/src-tauri/src/runtime/process.rs(新增 reaper 与 pid 守卫,+1 回归测试)crates/agent-gui/src-tauri/src/runtime/managed_process.rscrates/agent-gui/src-tauri/src/services/browser/launcher.rscrates/agent-gui/src-tauri/src/services/gateway/controller.rscrates/agent-gui/src-tauri/src/commands/workspace/{fs.rs,git.rs}crates/agent-gui/src-tauri/src/commands/integration/mcp.rsScreenshots / preview
非 UI 改动,下面是行为/进程层面的前后对照。
僵尸(新增回归测试的观察方式:
ps -p <pid> -o stat=)空闲轮询
Verification
cargo check --manifest-path crates/agent-gui/src-tauri/Cargo.toml --lib→ 通过,无 warning。cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml --lib runtime::process→3/3 通过,含新增
detached_child_is_reaped_instead_of_left_defunct。cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml --lib -- chat_history ssh_local_forward shell_runner integration_commands::mcp(CI 的同一条过滤器)→ 126/126 通过。ps -o stat=:进程消失或非Z即通过;旧实现(丢弃Child)下会一直读到
Z,所以这条测试在修复前必然失败。chat_history/ssh_local_forward/shell_runner/integration_commands::mcp),不覆盖runtime::process的新用例;它需要
cargo test --lib runtime::process(已在本地跑通)。CI 的过滤器在 workflow 文件里,而本机凭据缺
workflowscope 无法推送 workflow 改动,故未顺手扩过滤器。Pre-submit checklist