diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs index fb8844a95..758e40619 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs @@ -135,6 +135,10 @@ impl OwnedProcessTree { .ok() .filter(|pid| *pid > 0) .ok_or("app-server-process-owner-missing")?; + #[cfg(target_os = "linux")] + if let Some(has_live_member) = linux_process_group_has_live_member(pid) { + return Ok(!has_live_member); + } if unsafe { libc::kill(-pid, 0) } == 0 { return Ok(false); } @@ -183,6 +187,34 @@ impl OwnedProcessTree { } } +/// 容器 PID 1 不回收被 reparent 的僵尸孙进程时,kill(-pgid, 0) 仍认为组存活; +/// 僵尸对退役语义等价于已退出,扫描 /proc 时只把非僵尸成员视为存活。 +#[cfg(all(unix, target_os = "linux"))] +fn linux_process_group_has_live_member(pgid: i32) -> Option { + for entry in std::fs::read_dir("/proc").ok()? { + let entry = entry.ok()?; + let Ok(stat) = std::fs::read_to_string(entry.path().join("stat")) else { + continue; + }; + let Some(close) = stat.rfind(')') else { + continue; + }; + let mut fields = stat[close + 1..].split_whitespace(); + let (Some(state), Some(_ppid), Some(member_pgid)) = + (fields.next(), fields.next(), fields.next()) + else { + continue; + }; + if member_pgid.parse::().ok() != Some(pgid) { + continue; + } + if state != "Z" { + return Some(true); + } + } + Some(false) +} + impl Drop for OwnedProcessTree { fn drop(&mut self) { // Drop 只做应急回收,永远不伪造完成证明;正式终态必须 await shutdown。