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}