#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeMember {
pub pid: i32,
pub started: u64,
}
#[cfg(target_os = "linux")]
fn parse_stat(text: &str) -> Option<(i32, u64, bool)> {
let after_name: &str = text.get(text.rfind(')')? + 2..)?;
let fields: Vec<&str> = after_name.split(' ').collect();
let state: &str = fields.first()?;
let parent: i32 = fields.get(1)?.parse().ok()?;
let started: u64 = fields.get(19)?.parse().ok()?;
return Some((parent, started, state == "Z"));
}
#[cfg(target_os = "linux")]
pub fn descendants_of(root: i32) -> Vec<TreeMember> {
use std::collections::HashMap;
let Ok(listing) = std::fs::read_dir("/proc") else {
return Vec::new();
};
let mut children: HashMap<i32, Vec<TreeMember>> = HashMap::new();
for entry in listing.flatten() {
let Some(pid) = entry.file_name().to_str().and_then(|name| { return name.parse::<i32>().ok(); }) else {
continue;
};
let Ok(text) = std::fs::read_to_string(entry.path().join("stat")) else {
continue;
};
if let Some((parent, started, zombie)) = parse_stat(text.as_str())
&& !zombie
{
children.entry(parent).or_default().push(TreeMember { pid, started });
}
}
let mut found: Vec<TreeMember> = Vec::new();
let mut frontier: Vec<i32> = vec![root];
while let Some(parent) = frontier.pop() {
for member in children.remove(&parent).unwrap_or_default() {
frontier.push(member.pid);
found.push(member);
}
}
return found;
}
#[cfg(not(target_os = "linux"))]
pub fn descendants_of(_root: i32) -> Vec<TreeMember> {
return Vec::new();
}
#[cfg(target_os = "linux")]
pub fn member_of(pid: i32) -> Option<TreeMember> {
let text: String = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let (_parent, started, zombie) = parse_stat(text.as_str())?;
if zombie {
return None;
}
return Some(TreeMember { pid, started });
}
#[cfg(not(target_os = "linux"))]
pub fn member_of(_pid: i32) -> Option<TreeMember> {
return None;
}
#[cfg(target_os = "linux")]
pub fn still_running(member: &TreeMember) -> bool {
let Ok(text) = std::fs::read_to_string(format!("/proc/{}/stat", member.pid)) else {
return false;
};
return parse_stat(text.as_str()).is_some_and(|(_parent, started, zombie)| { return started == member.started && !zombie; });
}
#[cfg(not(target_os = "linux"))]
pub fn still_running(_member: &TreeMember) -> bool {
return false;
}
#[cfg(unix)]
pub fn signal_members(members: &[TreeMember], signal: i32) {
for member in members {
if still_running(member) {
unsafe { libc::kill(member.pid, signal) };
}
}
}
#[cfg(not(unix))]
pub fn signal_members(_members: &[TreeMember], _signal: i32) {}
#[cfg(unix)]
pub fn terminate_members(members: &[TreeMember]) {
signal_members(members, libc::SIGTERM);
}
#[cfg(not(unix))]
pub fn terminate_members(_members: &[TreeMember]) {}
#[cfg(unix)]
pub fn kill_members(members: &[TreeMember]) {
signal_members(members, libc::SIGKILL);
}
#[cfg(not(unix))]
pub fn kill_members(_members: &[TreeMember]) {}
#[cfg(test)]
#[path = "process_tree_tests.rs"]
mod tests;
#[cfg(all(test, not(target_os = "linux")))]
#[path = "process_tree_other_tests.rs"]
mod other_tests;