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 and
15        // 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
65        // name a real process, 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 or the negated
71        // process-group id created by [`isolate`]. ESRCH means the target has
72        // 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
106        // `isolate`, so the public `terminate(pid)` helper (which targets the
107        // negated process-group id) reaches it.
108        let mut command = StdCommand::new("/bin/sh");
109        command
110            .args(["-c", "sleep 30"])
111            .stdin(Stdio::null())
112            .stdout(Stdio::null())
113            .stderr(Stdio::null());
114        isolate(&mut command);
115        let mut child = command.spawn().expect("group-leader child spawns");
116        let pid = child.id();
117
118        assert!(terminate(pid), "terminating the process group succeeds");
119
120        let status = child.wait().expect("child is reaped");
121        assert_eq!(
122            status.signal(),
123            Some(ProcessSignal::Terminate.as_raw()),
124            "child should be terminated by the group SIGTERM"
125        );
126    }
127
128    #[test]
129    fn process_group_helpers_reject_zero_pid() {
130        assert!(!interrupt(0));
131        assert!(!terminate(0));
132        assert!(!kill(0));
133        assert!(!terminate_target(0, false));
134        assert!(!kill_target(0, false));
135    }
136
137    #[test]
138    fn terminate_and_kill_targets_can_signal_child_processes() {
139        let mut terminate_child = StdCommand::new("python3")
140            .args(["-c", "import time; time.sleep(30)"])
141            .stdin(Stdio::null())
142            .stdout(Stdio::null())
143            .stderr(Stdio::null())
144            .spawn()
145            .unwrap();
146        let terminate_pid = terminate_child.id();
147        assert!(terminate_target(terminate_pid, false));
148        let _ = terminate_child.wait().unwrap();
149
150        let mut kill_child = StdCommand::new("python3")
151            .args(["-c", "import time; time.sleep(30)"])
152            .stdin(Stdio::null())
153            .stdout(Stdio::null())
154            .stderr(Stdio::null())
155            .spawn()
156            .unwrap();
157        let kill_pid = kill_child.id();
158        assert!(kill_target(kill_pid, false));
159        let _ = kill_child.wait().unwrap();
160
161        std::thread::sleep(Duration::from_millis(10));
162    }
163}