fix(launch): M48 —— POSIX 组杀假成功导致 stop() 空等超时

CI test (ubuntu-latest) 自 M43 起红的根因不是「组杀打到自身进程组」,
而是 procps-ng 的 kill 把裸 `-<pgid>` 当成又一个信号选项吞掉:退出码 0、
什么都没发。旧实现据此提前 return,单 pid 兜底永远不执行,于是
process_smoke::stop_kills_process_tree 与
pipeline_cases::pipeline_stop_records_killed_exit 只能干等
graceful+5s 预算后报「强制结束后进程仍在运行」。

- process.rs:Unix 分支用 CommandExt::process_group(0)(std 1.64+ 安全
  API,无需 pre_exec/unsafe)让子进程自成一个进程组,与 Windows 的
  CREATE_NEW_PROCESS_GROUP 对称;这也让「树」回收真正覆盖后代。
- tree_kill.rs:组杀改用 `kill -TERM -- -<pgid>`(`--` 不可省),并且
  先过 group_kill_target 三重验证——目标必须是自己的进程组组长、必须
  读得到我们自己的组、目标组不能就是我们所在的组;单 pid 兜底改为
  「永远执行」,kill 缺失(NotFound)返回明确错误而不是静默失败。
  pid==0 守卫从 kill_tree 提到 terminate/force_kill 共用入口。
- 测试钉子(Linux 门住):spawn 后子进程 pgid == 自己的 pid 且 !=
  测试进程 pgid;组杀只接受验证过的独立组;组杀覆盖孙进程且不碰我们
  自己的组;同组子进程靠单 pid 兜底仍能被干掉;pid 0 一律拒绝。

验证:rust:1-bookworm 容器(ull-linux-target 热缓存)cargo fmt --check、
cargo clippy --workspace --all-targets -D warnings、cargo test -p
unlucky-core --test launch(82 passed,两条原红灯转绿)、cargo test
--workspace --all-targets --no-fail-fast 不再被 SIGTERM 打断;Windows
本机 fmt/clippy/cargo test -p unlucky-core 全绿。
This commit is contained in:
AskaEth committed 2026-11-01 10:32:18 +08:00
1 parent eaa1414847
commit 6ce825db8d
5 files changed
+351 -21

No files matched your search

+40
View File
@@ -111,6 +111,46 @@ fn stop_kills_process_tree() {
assert!(!child.is_running());
}
#[cfg(target_os = "linux")]
#[test]
fn spawn_puts_child_in_its_own_process_group() {
/// `/proc/<pid>/stat` 里的 `pgrp` 字段。
fn pgid(pid: u32) -> u32 {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).expect("读 /proc stat");
let tail = &stat[stat.rfind(')').expect("stat 应有 comm 括号") + 1..];
tail.split_whitespace()
.nth(2)
.expect("stat 应有 pgrp 字段")
.parse()
.expect("pgrp 应是数字")
}
let mut child = RunningProcess::spawn(
&shell_command("sleep 30"),
None,
Arc::new(NullSink),
config(),
)
.expect("spawn 应成功");
let pid = child.pid();
let own_pgid = pgid(std::process::id());
assert_eq!(
pgid(pid),
pid,
"子进程应是自己的进程组组长(process_group(0))"
);
assert_ne!(pgid(pid), own_pgid, "子进程不能挂在测试进程的进程组上");
assert_eq!(
unlucky_core::launch::tree_kill::group_kill_target(pid),
Some(pid),
"独立组的组长才是合法的组杀目标"
);
child.stop().expect("停止应成功");
let _ = child.wait();
}
#[test]
fn spawn_reports_missing_program() {
let mut command = shell_command("echo hi");