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}