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        let target = if process_group {
65            -(pid as i32)
66        } else {
67            pid as i32
68        };
69        // SAFETY: `kill` targets either the child pid or the negated
70        // process-group id created by [`isolate`]. ESRCH means the target has
71        // already exited.
72        unsafe {
73            let result = libc::kill(target, signal.as_raw());
74            if result != 0 {
75                let error = std::io::Error::last_os_error();
76                if error.raw_os_error() != Some(libc::ESRCH) {
77                    return false;
78                }
79            }
80            true
81        }
82    }
83    #[cfg(not(unix))]
84    {
85        let _ = pid;
86        let _ = signal;
87        let _ = process_group;
88        false
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use std::process::Stdio;
95    use std::time::Duration;
96
97    use super::*;
98
99    #[cfg(unix)]
100    #[test]
101    fn public_helpers_signal_a_real_process_group_leader() {
102        use std::os::unix::process::ExitStatusExt;
103
104        // Spawn a child that becomes its own process-group leader via
105        // `isolate`, so the public `terminate(pid)` helper (which targets the
106        // 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}