Skip to main content

scx_pandemonium/cli/
child_guard.rs

1use std::process::Child;
2use std::time::{Duration, Instant};
3
4/// RAII guard for a spawned child process. Tracks its process group ID.
5/// On drop, executes three-phase shutdown (muEmacs pattern):
6///   1. SIGINT to process group (graceful -- scheduler has ctrlc handler)
7///   2. Poll try_wait() for 500ms
8///   3. SIGKILL to process group (force)
9pub struct ChildGuard {
10    child: Option<Child>,
11    pgid: i32,
12}
13
14impl ChildGuard {
15    /// Wrap a child process. PGID is assumed to equal child PID
16    /// (requires the child to have been spawned with `.process_group(0)`).
17    pub fn new(child: Child) -> Self {
18        let pgid = child.id() as i32;
19        Self {
20            child: Some(child),
21            pgid,
22        }
23    }
24
25    pub fn id(&self) -> u32 {
26        self.child.as_ref().map(|c| c.id()).unwrap_or(0)
27    }
28
29    /// Three-phase shutdown: SIGINT → wait 500ms → SIGKILL.
30    /// Targets the entire process group via killpg.
31    pub fn stop(&mut self) {
32        let child = match self.child.as_mut() {
33            Some(c) => c,
34            None => return,
35        };
36
37        // CHECK IF ALREADY EXITED
38        if let Ok(Some(_)) = child.try_wait() {
39            return;
40        }
41
42        // PHASE 1: SIGINT TO PROCESS GROUP
43        unsafe {
44            libc::killpg(self.pgid, libc::SIGINT);
45        }
46
47        // PHASE 2: WAIT UP TO 500MS
48        let deadline = Instant::now() + Duration::from_millis(500);
49        loop {
50            match child.try_wait() {
51                Ok(Some(_)) => return,
52                Ok(None) => {
53                    if Instant::now() >= deadline {
54                        break;
55                    }
56                    std::thread::sleep(Duration::from_millis(50));
57                }
58                Err(_) => break,
59            }
60        }
61
62        // PHASE 3: SIGKILL TO PROCESS GROUP
63        unsafe {
64            libc::killpg(self.pgid, libc::SIGKILL);
65        }
66        let _ = child.wait();
67    }
68
69    /// Consume the guard and return the inner Child without triggering
70    /// the Drop cleanup. Caller becomes responsible for the process.
71    /// Use this when you need wait_with_output() for stdout capture.
72    pub fn into_child(mut self) -> Child {
73        self.child
74            .take()
75            .expect("ChildGuard: child already consumed")
76    }
77}
78
79impl Drop for ChildGuard {
80    fn drop(&mut self) {
81        if self.child.is_some() {
82            self.stop();
83        }
84    }
85}