use std::collections::HashMap;
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
const SHELLS: &[&str] = &[
"bash",
"sh",
"zsh",
"fish",
"dash",
"ksh",
"nu",
"elvish",
"xonsh",
"pwsh",
"powershell",
"cmd",
];
const PLUMBING: &[&str] = &["conhost", "openconsole"];
#[must_use]
pub fn busy(shells: &[Option<u32>]) -> Vec<bool> {
let mut system = System::new();
system.refresh_processes_specifics(ProcessesToUpdate::All, true, ProcessRefreshKind::nothing());
let mut tree = ProcessTree::default();
for (pid, process) in system.processes() {
tree.add(
pid.as_u32(),
process.parent().map(Pid::as_u32),
&process.name().to_string_lossy(),
);
}
shells
.iter()
.map(|pid| match pid {
Some(pid) => tree.is_busy(*pid),
None => true,
})
.collect()
}
#[derive(Debug, Default)]
struct ProcessTree {
children: HashMap<u32, Vec<u32>>,
names: HashMap<u32, String>,
}
impl ProcessTree {
fn add(&mut self, pid: u32, parent: Option<u32>, name: &str) {
let name = name.to_ascii_lowercase();
let name = name.strip_suffix(".exe").unwrap_or(&name).to_owned();
self.names.insert(pid, name);
if let Some(parent) = parent {
self.children.entry(parent).or_default().push(pid);
}
}
fn is_busy(&self, shell: u32) -> bool {
if !self.names.contains_key(&shell) {
return false;
}
self.has_command_below(shell, 0)
}
fn has_command_below(&self, pid: u32, depth: usize) -> bool {
if depth > 16 {
return true;
}
let Some(children) = self.children.get(&pid) else {
return false;
};
children.iter().any(|child| {
let name = self.names.get(child).map(String::as_str).unwrap_or("");
if PLUMBING.contains(&name) {
false
} else if SHELLS.contains(&name) {
self.has_command_below(*child, depth + 1)
} else {
true
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tree(entries: &[(u32, Option<u32>, &str)]) -> ProcessTree {
let mut tree = ProcessTree::default();
for (pid, parent, name) in entries {
tree.add(*pid, *parent, name);
}
tree
}
#[test]
fn a_shell_at_its_prompt_is_idle() {
let t = tree(&[(10, Some(1), "pwsh.exe")]);
assert!(!t.is_busy(10));
}
#[test]
fn a_shell_with_a_command_running_is_busy() {
let t = tree(&[(10, Some(1), "pwsh.exe"), (11, Some(10), "cargo.exe")]);
assert!(t.is_busy(10));
}
#[test]
fn console_hosts_do_not_count() {
let t = tree(&[(10, Some(1), "cmd.exe"), (11, Some(10), "conhost.exe")]);
assert!(!t.is_busy(10));
}
#[test]
fn a_shell_under_a_shell_counts_only_when_it_is_busy() {
let idle = tree(&[(10, Some(1), "bash.exe"), (11, Some(10), "bash.exe")]);
assert!(!idle.is_busy(10));
let busy = tree(&[
(10, Some(1), "bash.exe"),
(11, Some(10), "bash.exe"),
(12, Some(11), "make.exe"),
]);
assert!(busy.is_busy(10));
}
#[test]
fn a_shell_that_is_gone_is_not_busy() {
let t = tree(&[(10, Some(1), "pwsh")]);
assert!(!t.is_busy(99));
}
#[test]
fn an_unknown_pid_is_treated_as_busy() {
assert_eq!(busy(&[None]), vec![true]);
}
#[test]
fn reads_the_live_process_table() {
assert_eq!(busy(&[Some(u32::MAX)]), vec![false]);
}
}