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 全绿。
232 lines
7.4 KiB
Rust
232 lines
7.4 KiB
Rust
//! 进程管理: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
|
||
}
|