Skip to main content

rskit_process/
process_group.rs

1//! Process-group helpers for subprocess isolation and termination.
2
3use std::process::Command as StdCommand;
4
5use crate::signal::ProcessSignal;
6
7/// Configure a command so the spawned child becomes the leader of a new process group.
8pub fn isolate(command: &mut StdCommand) {
9    #[cfg(unix)]
10    {
11        use std::os::unix::process::CommandExt;
12
13        // SAFETY: `pre_exec` runs in the child process after fork and before exec.
14        // The closure only calls the async-signal-safe `setpgid` libc function
15        // and returns an `io::Error` on failure, which is the supported usage pattern.
16        unsafe {
17            command.pre_exec(|| {
18                if libc::setpgid(0, 0) != 0 {
19                    return Err(std::io::Error::last_os_error());
20                }
21                Ok(())
22            });
23        }
24    }
25    #[cfg(not(unix))]
26    {
27        let _ = command;
28    }
29}
30
31/// Request graceful interruption for the process group led by the provided child PID.
32#[must_use]
33pub fn interrupt(pid: u32) -> bool {
34    signal(pid, ProcessSignal::Interrupt)
35}
36
37/// Request graceful termination for the process group led by the provided child PID.
38#[must_use]
39pub fn terminate(pid: u32) -> bool {
40    signal(pid, ProcessSignal::Terminate)
41}
42
43/// Forcefully terminate the process group led by the provided child PID.
44#[must_use]
45pub fn kill(pid: u32) -> bool {
46    signal(pid, ProcessSignal::Kill)
47}
48
49pub(crate) fn terminate_target(pid: u32, process_group: bool) -> bool {
50    signal_target(pid, ProcessSignal::Terminate, process_group)
51}
52
53pub(crate) fn kill_target(pid: u32, process_group: bool) -> bool {
54    signal_target(pid, ProcessSignal::Kill, process_group)
55}
56
57fn signal(pid: u32, signal: ProcessSignal) -> bool {
58    signal_target(pid, signal, true)
59}
60
61fn signal_target(pid: u32, signal: ProcessSignal, process_group: bool) -> bool {
62    if pid == 0 {
63        return false;
64    }
65
66    #[cfg(unix)]
67    {
68        // A live Unix PID always fits in `i32`; a value that does not cannot name a real process,
69        // so there is nothing to signal.
70        let Ok(pid) = i32::try_from(pid) else {
71            return false;
72        };
73        let target = if process_group { -pid } else { pid };
74        // SAFETY: `kill` targets either the child pid
75        // or the negated process-group id created by [`isolate`].
76        // ESRCH means the target has already exited.
77        unsafe {
78            let result = libc::kill(target, signal.as_raw());
79            if result != 0 {
80                let error = std::io::Error::last_os_error();
81                if error.raw_os_error() != Some(libc::ESRCH) {
82                    return false;
83                }
84            }
85            true
86        }
87    }
88    #[cfg(not(unix))]
89    {
90        let _ = pid;
91        let _ = signal;
92        let _ = process_group;
93        false
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use std::process::Stdio;
100    use std::time::Duration;
101
102    use super::*;
103
104    #[cfg(unix)]
105    #[test]
106    fn public_helpers_signal_a_real_process_group_leader() {
107        use std::os::unix::process::ExitStatusExt;
108
109        // Spawn a child that becomes its own process-group leader via `isolate`,
110        // so the public `terminate(pid)` helper (which targets the negated process-group id) reaches it.
111        let mut command = StdCommand::new("/bin/sh");
112        command
113            .args(["-c", "sleep 30"])
114            .stdin(Stdio::null())
115            .stdout(Stdio::null())
116            .stderr(Stdio::null());
117        isolate(&mut command);
118        let mut child = command.spawn().expect("group-leader child spawns");
119        let pid = child.id();
120
121        assert!(terminate(pid), "terminating the process group succeeds");
122
123        let status = child.wait().expect("child is reaped");
124        assert_eq!(
125            status.signal(),
126            Some(ProcessSignal::Terminate.as_raw()),
127            "child should be terminated by the group SIGTERM"
128        );
129    }
130
131    #[test]
132    fn process_group_helpers_reject_zero_pid() {
133        assert!(!interrupt(0));
134        assert!(!terminate(0));
135        assert!(!kill(0));
136        assert!(!terminate_target(0, false));
137        assert!(!kill_target(0, false));
138    }
139
140    #[test]
141    fn terminate_and_kill_targets_can_signal_child_processes() {
142        let mut terminate_child = StdCommand::new("python3")
143            .args(["-c", "import time; time.sleep(30)"])
144            .stdin(Stdio::null())
145            .stdout(Stdio::null())
146            .stderr(Stdio::null())
147            .spawn()
148            .unwrap();
149        let terminate_pid = terminate_child.id();
150        assert!(terminate_target(terminate_pid, false));
151        let _ = terminate_child.wait().unwrap();
152
153        let mut kill_child = StdCommand::new("python3")
154            .args(["-c", "import time; time.sleep(30)"])
155            .stdin(Stdio::null())
156            .stdout(Stdio::null())
157            .stderr(Stdio::null())
158            .spawn()
159            .unwrap();
160        let kill_pid = kill_child.id();
161        assert!(kill_target(kill_pid, false));
162        let _ = kill_child.wait().unwrap();
163
164        std::thread::sleep(Duration::from_millis(10));
165    }
166}