use std::collections::HashSet;
use std::io;
use std::time::{Duration, Instant};
use sysinfo::{Pid, Process, System};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct ProcessInstance {
pid: Pid,
start_time: u64,
}
impl ProcessInstance {
fn from_process(pid: Pid, process: &Process) -> io::Result<Self> {
Ok(Self {
pid,
start_time: process_start_key(pid, process)?,
})
}
fn still_matches(self, system: &System) -> bool {
system.process(self.pid).is_some_and(|process| {
process_start_key(self.pid, process)
.is_ok_and(|start_time| start_time == self.start_time)
})
}
}
pub fn kill_tree(pid: u32, timeout: Duration) -> io::Result<u32> {
let mut system = System::new();
system.refresh_processes();
let root_pid = Pid::from_u32(pid);
let Some(root_process) = system.process(root_pid) else {
return Ok(0);
};
let root = match ProcessInstance::from_process(root_pid, root_process) {
Ok(root) => root,
Err(error) => {
system.refresh_processes();
if system.process(root_pid).is_none() {
return Ok(0);
}
return Err(error);
}
};
let mut descendants = Vec::new();
let mut visited = HashSet::new();
collect_descendants(&system, root, 1, &mut visited, &mut descendants);
descendants.sort_unstable_by_key(|(_, depth)| std::cmp::Reverse(*depth));
let mut targets = Vec::with_capacity(descendants.len() + 1);
targets.push(root);
targets.extend(descendants.into_iter().map(|(instance, _)| instance));
let mut signaled = HashSet::new();
signal_matching(&system, &targets, &mut signaled);
let started = Instant::now();
loop {
system.refresh_processes();
let remaining: Vec<_> = targets
.iter()
.copied()
.filter(|target| target.still_matches(&system))
.collect();
if remaining.is_empty() || started.elapsed() >= timeout {
break;
}
signal_matching(&system, &remaining, &mut signaled);
let sleep_for = timeout
.saturating_sub(started.elapsed())
.min(Duration::from_millis(25));
if sleep_for.is_zero() {
break;
}
std::thread::sleep(sleep_for);
}
Ok(signaled.len() as u32)
}
fn signal_matching(
system: &System,
targets: &[ProcessInstance],
signaled: &mut HashSet<ProcessInstance>,
) {
for target in targets {
let Some(process) = system.process(target.pid) else {
continue;
};
if process_start_key(target.pid, process)
.is_ok_and(|start_time| start_time == target.start_time)
&& process.kill()
{
signaled.insert(*target);
}
}
}
fn collect_descendants(
system: &System,
parent: ProcessInstance,
depth: usize,
visited: &mut HashSet<Pid>,
descendants: &mut Vec<(ProcessInstance, usize)>,
) {
for (pid, process) in system.processes() {
if process.parent() != Some(parent.pid) || visited.contains(pid) {
continue;
}
let Ok(child) = ProcessInstance::from_process(*pid, process) else {
continue;
};
if child.start_time < parent.start_time {
continue;
}
visited.insert(*pid);
descendants.push((child, depth));
collect_descendants(system, child, depth + 1, visited, descendants);
}
}
#[cfg(not(windows))]
fn process_start_key(_pid: Pid, process: &Process) -> io::Result<u64> {
Ok(process.start_time())
}
#[cfg(windows)]
fn process_start_key(pid: Pid, _process: &Process) -> io::Result<u64> {
use windows_sys::Win32::Foundation::{CloseHandle, FILETIME};
use windows_sys::Win32::System::Threading::{
GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid.as_u32()) };
if handle.is_null() {
return Err(io::Error::last_os_error());
}
let mut creation: FILETIME = unsafe { std::mem::zeroed() };
let mut exit: FILETIME = unsafe { std::mem::zeroed() };
let mut kernel: FILETIME = unsafe { std::mem::zeroed() };
let mut user: FILETIME = unsafe { std::mem::zeroed() };
let queried =
unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) };
let query_error = if queried == 0 {
Some(io::Error::last_os_error())
} else {
None
};
unsafe {
CloseHandle(handle);
}
if let Some(error) = query_error {
return Err(error);
}
Ok((u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_pid_is_a_successful_noop() {
assert_eq!(kill_tree(u32::MAX, Duration::ZERO).unwrap(), 0);
}
}