Skip to main content

ante_exec/
process_group.rs

1//! Process-group helpers shared by process execution backends.
2//!
3//! On non-Unix platforms these helpers are no-ops.
4
5use std::io;
6
7#[cfg(target_os = "linux")]
8/// Ensure the child receives SIGTERM when the original parent dies.
9///
10/// This should run in `pre_exec` and uses `parent_pid` captured before spawn to
11/// avoid a race where the parent exits between fork and exec.
12pub fn set_parent_death_signal(parent_pid: libc::pid_t) -> io::Result<()> {
13    if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) } == -1 {
14        return Err(io::Error::last_os_error());
15    }
16
17    if unsafe { libc::getppid() } != parent_pid {
18        unsafe {
19            libc::raise(libc::SIGTERM);
20        }
21    }
22
23    Ok(())
24}
25
26#[cfg(not(target_os = "linux"))]
27/// No-op on non-Linux platforms.
28pub fn set_parent_death_signal(_parent_pid: i32) -> io::Result<()> {
29    Ok(())
30}
31
32#[cfg(unix)]
33/// Detach from the controlling TTY by starting a new session.
34pub fn detach_from_tty() -> io::Result<()> {
35    let result = unsafe { libc::setsid() };
36    if result == -1 {
37        let err = io::Error::last_os_error();
38        if err.raw_os_error() == Some(libc::EPERM) {
39            return set_process_group();
40        }
41        return Err(err);
42    }
43    Ok(())
44}
45
46#[cfg(not(unix))]
47/// No-op on non-Unix platforms.
48pub fn detach_from_tty() -> io::Result<()> {
49    Ok(())
50}
51
52#[cfg(unix)]
53/// Put the calling process into its own process group.
54///
55/// Intended for use in `pre_exec` so the child becomes the group leader.
56pub fn set_process_group() -> io::Result<()> {
57    let result = unsafe { libc::setpgid(0, 0) };
58    if result == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
59}
60
61#[cfg(not(unix))]
62/// No-op on non-Unix platforms.
63pub fn set_process_group() -> io::Result<()> {
64    Ok(())
65}
66
67#[cfg(unix)]
68/// Resolve a process group by PID and send SIGKILL.
69pub fn kill_by_pid(pid: u32) -> io::Result<()> {
70    use std::io::ErrorKind;
71
72    let pid = pid as libc::pid_t;
73    let pgid = unsafe { libc::getpgid(pid) };
74    if pgid == -1 {
75        let err = io::Error::last_os_error();
76        if err.kind() != ErrorKind::NotFound {
77            return Err(err);
78        }
79        return Ok(());
80    }
81
82    let result = unsafe { libc::killpg(pgid, libc::SIGKILL) };
83    if result == -1 {
84        let err = io::Error::last_os_error();
85        if err.kind() != ErrorKind::NotFound {
86            return Err(err);
87        }
88    }
89
90    Ok(())
91}
92
93#[cfg(not(unix))]
94/// No-op on non-Unix platforms.
95pub fn kill_by_pid(_pid: u32) -> io::Result<()> {
96    Ok(())
97}
98
99#[cfg(unix)]
100/// Send SIGKILL to the process group `pgid` directly — no lookup through the
101/// leader pid. This is the form that still reaches a group's surviving
102/// members after the leader exited and was reaped: [`kill_by_pid`]'s
103/// `getpgid` resolution returns `ESRCH` then and silently spares them. A
104/// fully-gone group reads as success. Groups 0 and 1 are rejected rather
105/// than signalled: to `killpg` they mean the caller's own group and init's.
106///
107/// Signals the group id, not the job that created it: a caller holding a
108/// pgid past its group's death accepts the same theoretical recycled-pgid
109/// exposure as a manual `kill -- -<pgid>`.
110pub fn kill_process_group(pgid: u32) -> io::Result<()> {
111    use std::io::ErrorKind;
112
113    if pgid <= 1 {
114        return Ok(());
115    }
116    let Ok(pgid) = libc::pid_t::try_from(pgid) else {
117        return Ok(());
118    };
119    if unsafe { libc::killpg(pgid, libc::SIGKILL) } == -1 {
120        let err = io::Error::last_os_error();
121        if err.kind() != ErrorKind::NotFound {
122            return Err(err);
123        }
124    }
125    Ok(())
126}
127
128#[cfg(not(unix))]
129/// No-op on non-Unix platforms.
130pub fn kill_process_group(_pgid: u32) -> io::Result<()> {
131    Ok(())
132}
133
134#[cfg(unix)]
135/// Whether any process remains in the group led by `pgid`, including members
136/// that outlived the leader. Signal 0 runs `killpg`'s existence and permission
137/// checks without delivering anything, so `EPERM` (the group is another user's)
138/// counts as alive. Groups 0 and 1 are rejected rather than probed: to `killpg`
139/// they mean the caller's own group and init's.
140///
141/// Answers about the group id, not about the job that created it: a recycled
142/// pgid reads as alive.
143pub fn process_group_is_alive(pgid: u32) -> bool {
144    if pgid <= 1 {
145        return false;
146    }
147    let Ok(pgid) = libc::pid_t::try_from(pgid) else {
148        return false;
149    };
150    if unsafe { libc::killpg(pgid, 0) } == 0 {
151        return true;
152    }
153    io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
154}
155
156#[cfg(not(unix))]
157/// No portable existence probe, so this reports every group as alive — callers
158/// fail safe by treating its processes as still running.
159pub fn process_group_is_alive(_pgid: u32) -> bool {
160    true
161}