hotl_platform/process/unix.rs
1//! `setsid` + a process group, reaped with `kill(-pgid)`.
2
3use super::{ProcessControl, TreeReaper};
4use std::io;
5use std::sync::atomic::{AtomicI32, Ordering};
6
7#[derive(Debug, Clone, Copy, Default)]
8pub struct UnixProcessControl;
9
10impl UnixProcessControl {
11 pub const fn new() -> Self {
12 Self
13 }
14}
15
16impl crate::sealed::Sealed for UnixProcessControl {}
17
18impl ProcessControl for UnixProcessControl {
19 type Reaper = UnixReaper;
20
21 fn detach(&self, cmd: &mut tokio::process::Command) {
22 // `setsid` alone, and deliberately **not** alongside
23 // `Command::process_group(0)`: setsid already creates both a new
24 // session and a new process group whose pgid equals the pid, and it
25 // fails with EPERM if the caller is *already* a group leader — which
26 // `process_group(0)` would have just made it. Asking for both is how
27 // you get a spawn that cannot start.
28 //
29 // The session is what detaches the controlling terminal and closes the
30 // TIOCSTI class; the pgid is what makes the reaper's negated kill reach
31 // the whole tree.
32 // SAFETY: `setsid` is async-signal-safe and touches no shared state; it
33 // is the only work done between fork and exec here.
34 unsafe {
35 cmd.pre_exec(|| {
36 if libc::setsid() == -1 {
37 return Err(io::Error::last_os_error());
38 }
39 Ok(())
40 });
41 }
42 }
43
44 fn new_reaper(&self) -> io::Result<Self::Reaper> {
45 Ok(UnixReaper(AtomicI32::new(0)))
46 }
47}
48
49/// The child's pgid, which `detach` arranged to equal its pid.
50pub struct UnixReaper(AtomicI32);
51
52impl TreeReaper for UnixReaper {
53 const ADOPTION_IS_ATOMIC: bool = true;
54
55 fn adopt(&self, child: &tokio::process::Child) -> io::Result<()> {
56 // Nothing to do at the kernel: `process_group(0)` already took effect
57 // before the child ran its first instruction, which is exactly why
58 // `ADOPTION_IS_ATOMIC` is true here. This only records the number.
59 let Some(pid) = child.id() else {
60 return Err(io::Error::other("the child has already been reaped"));
61 };
62 self.0.store(pid as i32, Ordering::SeqCst);
63 Ok(())
64 }
65
66 /// INVARIANT: only ever called while the `Child` is still owned and
67 /// un-reaped, so the pid is either live or a zombie — reserved either way,
68 /// and never reusable by another process. Killing after a wait would be a
69 /// pid-reuse bug: the number could by then name someone else's process, and
70 /// the negation someone else's *group*.
71 fn kill_tree(&self) -> io::Result<()> {
72 let pid = self.0.load(Ordering::SeqCst);
73 if pid == 0 {
74 return Ok(()); // nothing adopted
75 }
76 // SAFETY: plain kill(2); a negative pid targets the process group.
77 unsafe {
78 libc::kill(-pid, libc::SIGKILL);
79 }
80 Ok(())
81 }
82}