Skip to main content

running_process/
process_tree.rs

1//! Best-effort local process-tree termination.
2//!
3//! This API does not require the running-process daemon. It snapshots the
4//! tree, terminates the root first so it cannot create more descendants,
5//! then terminates the captured descendants deepest-first. Process start
6//! times are retained while waiting so a recycled PID is not treated as
7//! the original target. On Windows these are exact kernel creation times;
8//! other platforms use the best timestamp exposed by `sysinfo`.
9
10use std::collections::HashSet;
11use std::io;
12use std::time::{Duration, Instant};
13
14use sysinfo::{Pid, Process, System};
15
16#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
17struct ProcessInstance {
18    pid: Pid,
19    start_time: u64,
20}
21
22impl ProcessInstance {
23    fn from_process(pid: Pid, process: &Process) -> io::Result<Self> {
24        Ok(Self {
25            pid,
26            start_time: process_start_key(pid, process)?,
27        })
28    }
29
30    fn still_matches(self, system: &System) -> bool {
31        system.process(self.pid).is_some_and(|process| {
32            process_start_key(self.pid, process)
33                .is_ok_and(|start_time| start_time == self.start_time)
34        })
35    }
36}
37
38/// Kill the process tree rooted at `pid`.
39///
40/// Returns the number of distinct process instances for which the OS accepted
41/// a kill request. A missing PID is a successful no-op. `timeout` bounds the
42/// post-signal wait and retry loop; the initial termination attempt always
43/// occurs even when the timeout is zero.
44///
45/// The tree is a point-in-time snapshot. A process created in the narrow race
46/// between enumeration and root termination can escape unless the caller also
47/// owns an OS containment primitive such as a Windows Job Object.
48pub fn kill_tree(pid: u32, timeout: Duration) -> io::Result<u32> {
49    let mut system = System::new();
50    system.refresh_processes();
51
52    let root_pid = Pid::from_u32(pid);
53    let Some(root_process) = system.process(root_pid) else {
54        return Ok(0);
55    };
56    let root = match ProcessInstance::from_process(root_pid, root_process) {
57        Ok(root) => root,
58        Err(error) => {
59            // The process may have exited between the sysinfo snapshot and
60            // the exact identity query. Treat that race like a missing PID,
61            // but preserve access-denied and other errors for a live target.
62            system.refresh_processes();
63            if system.process(root_pid).is_none() {
64                return Ok(0);
65            }
66            return Err(error);
67        }
68    };
69
70    let mut descendants = Vec::new();
71    let mut visited = HashSet::new();
72    collect_descendants(&system, root, 1, &mut visited, &mut descendants);
73    descendants.sort_unstable_by_key(|(_, depth)| std::cmp::Reverse(*depth));
74
75    // Root first: once it exits it can no longer respawn a descendant that
76    // was present in the snapshot. Descendants then go deepest-first.
77    let mut targets = Vec::with_capacity(descendants.len() + 1);
78    targets.push(root);
79    targets.extend(descendants.into_iter().map(|(instance, _)| instance));
80
81    let mut signaled = HashSet::new();
82    signal_matching(&system, &targets, &mut signaled);
83
84    let started = Instant::now();
85    loop {
86        system.refresh_processes();
87        let remaining: Vec<_> = targets
88            .iter()
89            .copied()
90            .filter(|target| target.still_matches(&system))
91            .collect();
92        if remaining.is_empty() || started.elapsed() >= timeout {
93            break;
94        }
95
96        signal_matching(&system, &remaining, &mut signaled);
97        let sleep_for = timeout
98            .saturating_sub(started.elapsed())
99            .min(Duration::from_millis(25));
100        if sleep_for.is_zero() {
101            break;
102        }
103        std::thread::sleep(sleep_for);
104    }
105
106    Ok(signaled.len() as u32)
107}
108
109fn signal_matching(
110    system: &System,
111    targets: &[ProcessInstance],
112    signaled: &mut HashSet<ProcessInstance>,
113) {
114    for target in targets {
115        let Some(process) = system.process(target.pid) else {
116            continue;
117        };
118        if process_start_key(target.pid, process)
119            .is_ok_and(|start_time| start_time == target.start_time)
120            && process.kill()
121        {
122            signaled.insert(*target);
123        }
124    }
125}
126
127fn collect_descendants(
128    system: &System,
129    parent: ProcessInstance,
130    depth: usize,
131    visited: &mut HashSet<Pid>,
132    descendants: &mut Vec<(ProcessInstance, usize)>,
133) {
134    for (pid, process) in system.processes() {
135        if process.parent() != Some(parent.pid) || visited.contains(pid) {
136            continue;
137        }
138
139        // A process cannot be the child of a process instance created after
140        // it. This rejects stale PPID links after PID reuse. If an exact
141        // identity cannot be read, skip the ambiguous branch rather than
142        // risking termination of an unrelated process.
143        let Ok(child) = ProcessInstance::from_process(*pid, process) else {
144            continue;
145        };
146        if child.start_time < parent.start_time {
147            continue;
148        }
149
150        visited.insert(*pid);
151        descendants.push((child, depth));
152        collect_descendants(system, child, depth + 1, visited, descendants);
153    }
154}
155
156#[cfg(not(windows))]
157fn process_start_key(_pid: Pid, process: &Process) -> io::Result<u64> {
158    Ok(process.start_time())
159}
160
161#[cfg(windows)]
162fn process_start_key(pid: Pid, _process: &Process) -> io::Result<u64> {
163    use windows_sys::Win32::Foundation::{CloseHandle, FILETIME};
164    use windows_sys::Win32::System::Threading::{
165        GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
166    };
167
168    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid.as_u32()) };
169    if handle.is_null() {
170        return Err(io::Error::last_os_error());
171    }
172
173    let mut creation: FILETIME = unsafe { std::mem::zeroed() };
174    let mut exit: FILETIME = unsafe { std::mem::zeroed() };
175    let mut kernel: FILETIME = unsafe { std::mem::zeroed() };
176    let mut user: FILETIME = unsafe { std::mem::zeroed() };
177    let queried =
178        unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) };
179    let query_error = if queried == 0 {
180        Some(io::Error::last_os_error())
181    } else {
182        None
183    };
184    unsafe {
185        CloseHandle(handle);
186    }
187    if let Some(error) = query_error {
188        return Err(error);
189    }
190
191    Ok((u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime))
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn missing_pid_is_a_successful_noop() {
200        assert_eq!(kill_tree(u32::MAX, Duration::ZERO).unwrap(), 0);
201    }
202}