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