Skip to main content

hotl_platform/process/
mod.rs

1//! [`ProcessControl`] — detachment, and reaping a whole process tree.
2
3use std::io;
4
5#[cfg(unix)]
6mod unix;
7#[cfg(unix)]
8pub use unix::{UnixProcessControl, UnixReaper};
9#[cfg(unix)]
10pub type ActiveProcessControl = UnixProcessControl;
11#[cfg(unix)]
12pub type ActiveReaper = UnixReaper;
13
14#[cfg(windows)]
15mod windows;
16#[cfg(windows)]
17pub use windows::{WindowsProcessControl, WindowsReaper};
18#[cfg(windows)]
19pub type ActiveProcessControl = WindowsProcessControl;
20#[cfg(windows)]
21pub type ActiveReaper = WindowsReaper;
22
23/// Two things Unix does with process groups and Windows does with job objects,
24/// behind one contract.
25pub trait ProcessControl: crate::sealed::Sealed {
26    type Reaper: TreeReaper;
27
28    /// Detach the child from the controlling terminal or console so it cannot
29    /// inject input into ours.
30    ///
31    /// Unix: `setsid()` in `pre_exec`, which is the TIOCSTI defense. Windows:
32    /// `DETACHED_PROCESS`, which closes the `WriteConsoleInput`-on-`CONIN$`
33    /// variant of the same attack. Different syscalls, same threat.
34    fn detach(&self, cmd: &mut tokio::process::Command);
35
36    fn new_reaper(&self) -> io::Result<Self::Reaper>;
37}
38
39/// Something that kills every descendant, not just the direct child.
40pub trait TreeReaper: Send + Sync {
41    /// Whether a descendant can escape between spawn and adoption.
42    ///
43    /// Unix process groups: **no** — `process_group(0)` takes effect before the
44    /// child's first instruction. Windows without
45    /// `PROC_THREAD_ATTRIBUTE_JOB_LIST`: **yes**, because
46    /// `std::process::Command` cannot express `CREATE_SUSPENDED`, so a child
47    /// that forks immediately can outrun `AssignProcessToJobObject`. Reported
48    /// as data rather than buried in a comment, so a caller that needs
49    /// atomicity can assert on it and a later change can flip it.
50    const ADOPTION_IS_ATOMIC: bool;
51
52    /// Take ownership of `child` and everything it goes on to spawn.
53    fn adopt(&self, child: &tokio::process::Child) -> io::Result<()>;
54
55    /// Kill the whole tree.
56    ///
57    /// Windows is strictly stronger here and it is worth knowing why: a job
58    /// object with `KILL_ON_JOB_CLOSE` reaps the tree even if **hotl itself**
59    /// dies, which `kill(-pgid)` does not. And the pid-reuse hazard that makes
60    /// the Unix caller order its kill before the wait is moot on Windows — a
61    /// job handle is a kernel object, not a number that can be recycled.
62    fn kill_tree(&self) -> io::Result<()>;
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    /// One body, both mechanisms: a reaper adopts a child that spawns a
70    /// grandchild, and killing the tree kills both. Written so it can fail —
71    /// if adoption raced the spawn, the grandchild survives and the assertion
72    /// catches it.
73    #[test]
74    fn a_reaper_kills_the_grandchild_too() {
75        let rt = tokio::runtime::Builder::new_current_thread()
76            .enable_all()
77            .build()
78            .unwrap();
79        rt.block_on(async {
80            let ctl = crate::PROCESS_CONTROL;
81            let reaper = ctl.new_reaper().unwrap();
82
83            let mut cmd = spawn_a_sleeping_tree();
84            ctl.detach(&mut cmd);
85            let mut child = cmd.spawn().unwrap();
86            reaper.adopt(&child).unwrap();
87
88            reaper.kill_tree().unwrap();
89            // The direct child must be gone promptly. The grandchild shares its
90            // fate through the group/job, which is the property under test —
91            // waiting on the child is how we know the tree was reaped rather
92            // than just signalled.
93            let status = tokio::time::timeout(std::time::Duration::from_secs(10), child.wait())
94                .await
95                .expect("kill_tree left the child running")
96                .unwrap();
97            assert!(!status.success(), "a killed child must not report success");
98        });
99    }
100
101    /// A child that spawns a grandchild and then waits, so the grandchild
102    /// outlives its parent's own exit unless something reaps the tree.
103    fn spawn_a_sleeping_tree() -> tokio::process::Command {
104        #[cfg(unix)]
105        {
106            let mut c = tokio::process::Command::new("/bin/sh");
107            c.arg("-c").arg("sleep 60 & sleep 60");
108            c
109        }
110        #[cfg(windows)]
111        {
112            let mut c = tokio::process::Command::new("cmd.exe");
113            c.arg("/c")
114                .arg("start /b timeout /t 60 /nobreak & timeout /t 60 /nobreak");
115            c
116        }
117    }
118}