ADR 0019:sidecar 子树生命周期
状态:已接受 日期:2026-08-25 关联:ADR 0014(UI 迁移到 Tauri)
关闭应用后,sidecar 进程(e2m2e)留在后台继续运行——包括正在计算的轨道族任务。我们决定在 Windows 上把 sidecar 子树划入 kill-on-close 的 Job Object,由内核保证其随应用进程死亡而终结。
背景与根因
sidecar 的 stdin EOF 只在空闲读循环时生效:e2m2e run_loop 逐行读请求,深陷计算(轨道族延拓可达分钟级)时不读 stdin,感知不到 EOF。而现有杀进程手段全都够不到真正干活的进程:
TerminateProcess/kill_on_drop只杀直接子进程。dev 期命令是uv run e2m2e serve-stdio(uv.exe → python.exe),分发期 sidecar 是 PyInstaller onefile(bootloader 运行时再拉一个子进程)——两种形态下 python 都是孙进程;显式
shutdown()无调用时机能覆盖崩溃、被杀、updaterstd::process::exit等退出路径。
Considered Options
Windows Job Object(kill-on-close):spawn 后立刻把直接子进程划入新建 job;job 成员资格在进程创建时继承,孙进程自动入内。应用无论怎么退出(关窗口、崩溃、被杀、updater 重启),OS 关闭 job 句柄即由内核终结整棵树,不依赖任何用户态清理代码运行。只圈 sidecar 子树、不圈应用自身,updater 拉起的安装器等外部进程不受影响。
退出事件里显式 shutdown + 树杀(taskkill /T 或手工枚举):只覆盖优雅退出路径,崩溃与被杀路径仍残留;进程树枚举代码量更大且需维护。
依赖 stdin EOF(维持现状):仅空闲态有效,忙碌态残留分钟级;用户在计算中途关窗即复现。
Consequences
SidecarHandle(src-tauri/src/sidecar/process.rs)持有 job 句柄,句柄链随应用状态销毁或进程死亡关闭,内核即终结子树。回归测试busy_grandchild_tree_killed_when_handle_dropped固化了"drop 句柄 → 忙碌孙进程树被终结"。竞态窗口可忽略:assign 在 spawn 返回后微秒级执行,解释器(uv / bootloader)启动到拉孙进程是毫秒级以上。
job 创建或划入失败时降级为无兜底(警告日志),不阻断应用启动。
Unix 无 Job Object 等价物:空闲态由 EOF 覆盖,忙碌态会跑完当前计算后经 EOF 退出,不产生永久残留;如未来 Linux 用户报告计算中关窗残留,再引入进程组(
setsid+killpg)方案。