Skip to main content

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}