running_process/
process_tree.rs1use 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
38pub 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 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 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 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}