Files
Unlucky-Launcher/crates/core/src/launch/process.rs
T
AskaEth 6ce825db8d 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 全绿。
2026-11-01 10:32:18 +08:00

232 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 进程管理:spawn + stdout/stderr 泵 + 进程树回收。
//!
//! 对应 `analyse/02` §6 的四条线程模型(`stdout-pump` / `stderr-pump` / `exit-waiter`),
//! 但线程生命周期的收尾由 [`crate::launch::watchdog`] 兜底,不无条件 `join`。
//!
//! 本模块只管“把命令行跑起来并收集输出”,不做组件检查、不做诊断——
//! 那些是 `launch` 上层流水线(阶段B)与 M15 的职责。
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::launch::args_table::AssembledCommand;
use crate::launch::error::{LaunchError, Result};
use crate::launch::pump::{LineSink, LogLine, PumpGroup, RingBuffer, RingBufferConfig};
use crate::launch::tree_kill;
use crate::launch::watchdog::{self, PumpVerdict, WatchdogConfig};
/// 强制结束后等进程消失的预算。
const FORCE_KILL_BUDGET: Duration = Duration::from_secs(5);
/// 轮询进程状态的间隔。
const POLL_INTERVAL: Duration = Duration::from_millis(20);
/// 进程管理配置。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProcessConfig {
/// 日志泵等待预算。
pub watchdog: WatchdogConfig,
/// 停止时温和关闭的宽限期。
pub graceful_stop: Duration,
/// 环形缓冲配置。
pub buffer: RingBufferConfig,
}
impl Default for ProcessConfig {
fn default() -> Self {
Self {
watchdog: WatchdogConfig::default(),
graceful_stop: Duration::from_secs(3),
buffer: RingBufferConfig::default(),
}
}
}
/// 已启动的游戏进程。
#[derive(Debug)]
pub struct RunningProcess {
child: Child,
pump: PumpGroup,
buffer: Arc<Mutex<RingBuffer>>,
started_at: Instant,
pid: u32,
config: ProcessConfig,
}
impl RunningProcess {
/// 启动进程并接管两条输出流。
pub fn spawn(
command: &AssembledCommand,
working_dir: Option<&Path>,
sink: Arc<dyn LineSink>,
config: ProcessConfig,
) -> Result<Self> {
let mut process = Command::new(&command.program);
process.args(&command.args);
for (key, value) in &command.env {
process.env(key, value);
}
if let Some(dir) = working_dir {
process.current_dir(dir);
}
process
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
/// 独立进程组:便于 `taskkill /T` 递归回收。
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
process.creation_flags(CREATE_NEW_PROCESS_GROUP);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
// 与 Windows 的 CREATE_NEW_PROCESS_GROUP 对称:子进程自成一个进程组,
// `tree_kill` 的 `kill -- -<pgid>` 才能覆盖整棵子树。`process_group` 是
// std 1.64+ 的**安全** API,不需要 pre_exec / unsafe(旧注释说“unsafe 被挡”已过时)。
// 参数 0 = `pgid = 子进程 pid`。
process.process_group(0);
}
let mut child = process.spawn().map_err(|source| LaunchError::Spawn {
program: command.program.clone(),
source,
})?;
let pid = child.id();
let buffer = Arc::new(Mutex::new(RingBuffer::new(config.buffer)));
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let pump = PumpGroup::spawn(stdout, stderr, buffer.clone(), sink);
Ok(Self {
child,
pump,
buffer,
started_at: Instant::now(),
pid,
config,
})
}
/// 进程号。
pub fn pid(&self) -> u32 {
self.pid
}
/// 已运行毫秒数。
pub fn uptime_ms(&self) -> u64 {
u64::try_from(self.started_at.elapsed().as_millis()).unwrap_or(u64::MAX)
}
/// 启动时刻(窗口检测计时用)。
pub fn started_at(&self) -> Instant {
self.started_at
}
/// 共享的日志环形缓冲。
pub fn buffer(&self) -> Arc<Mutex<RingBuffer>> {
self.buffer.clone()
}
/// 当前可读日志快照。
pub fn lines(&self) -> Vec<LogLine> {
match self.buffer.lock() {
Ok(guard) => guard.lines().cloned().collect(),
Err(_) => Vec::new(),
}
}
/// 等待进程退出(不等待日志泵)。
pub fn wait(&mut self) -> Result<ExitStatus> {
self.child.wait().map_err(|source| LaunchError::Io {
op: "wait_child",
path: PathBuf::from(self.pid.to_string()),
source,
})
}
/// 进程退出后,按预算收尾日志泵。
pub fn supervise(&mut self) -> PumpVerdict {
watchdog::wait_for_pumps(&mut self.pump, &self.config.watchdog)
}
/// 进程是否仍在运行。
pub fn is_running(&mut self) -> bool {
matches!(self.child.try_wait(), Ok(None))
}
/// 三段式停止整棵进程树(温和 → 宽限 → 强制)。
///
/// 判定标准是**进程是否真的没了**,不是 `taskkill`/`kill` 的退出码:
/// 外部命令对“已经退出的进程”会返回非 0(Windows 上 `taskkill` 报 128),
/// 把它当失败会误报——尤其是温和阶段已经把进程关掉的情况。
pub fn stop(&mut self) -> Result<()> {
if !self.is_running() {
return Ok(());
}
let _ = tree_kill::terminate(self.pid);
if self.wait_until_exit(self.config.graceful_stop) {
return Ok(());
}
let _ = tree_kill::force_kill(self.pid);
if self.wait_until_exit(FORCE_KILL_BUDGET) {
return Ok(());
}
Err(LaunchError::Kill {
pid: self.pid,
source: std::io::Error::other("强制结束后进程仍在运行"),
})
}
/// 直接强杀(看门狗/崩溃兜底路径)。
pub fn force_kill(&mut self) -> Result<()> {
if !self.is_running() {
return Ok(());
}
let _ = tree_kill::force_kill(self.pid);
if self.wait_until_exit(FORCE_KILL_BUDGET) {
Ok(())
} else {
Err(LaunchError::Kill {
pid: self.pid,
source: std::io::Error::other("强制结束后进程仍在运行"),
})
}
}
fn wait_until_exit(&mut self, budget: Duration) -> bool {
let deadline = Instant::now() + budget;
loop {
match self.child.try_wait() {
Ok(Some(_)) => return true,
Ok(None) => {}
Err(_) => return false,
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(POLL_INTERVAL);
}
}
}
/// 从退出状态取整数退出码。
///
/// POSIX 上被子进程信号杀死没有 `code()`,按 shell 惯例补 `128 + signal`
/// (`SIGKILL` → 137、`SIGTERM` → 143,与 `analyse/02` 附录 C 一致)。
pub fn exit_code(status: &ExitStatus) -> i32 {
if let Some(code) = status.code() {
return code;
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = status.signal() {
return 128 + signal;
}
}
-1
}