#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KillSignal {
Kill,
Term,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KillOutcome {
Signalled,
AlreadyGone,
Failed(String),
}
pub fn summarize(results: &[(u32, KillOutcome)]) -> String {
let mut signalled = 0usize;
let mut already = 0usize;
let mut failed: Vec<&u32> = Vec::new();
for (pid, o) in results {
match o {
KillOutcome::Signalled => signalled += 1,
KillOutcome::AlreadyGone => already += 1,
KillOutcome::Failed(_) => failed.push(pid),
}
}
let mut parts = Vec::new();
if signalled > 0 {
parts.push(format!("{signalled} killed"));
}
if already > 0 {
parts.push(format!("{already} already gone"));
}
if !failed.is_empty() {
let pids: Vec<String> = failed.iter().map(|p| p.to_string()).collect();
parts.push(format!("{} failed (pid {})", failed.len(), pids.join(", ")));
}
if parts.is_empty() {
"csm reap: nothing to kill".to_string()
} else {
format!("csm reap: {}", parts.join(", "))
}
}
pub fn kill_all(pids: &[u32], signal: KillSignal) -> Vec<(u32, KillOutcome)> {
pids.iter()
.map(|&pid| (pid, kill_one(pid, signal)))
.collect()
}
#[cfg(unix)]
fn kill_one(pid: u32, signal: KillSignal) -> KillOutcome {
use nix::errno::Errno;
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
let sig = match signal {
KillSignal::Kill => Signal::SIGKILL,
KillSignal::Term => Signal::SIGTERM,
};
match kill(Pid::from_raw(pid as i32), sig) {
Ok(()) => KillOutcome::Signalled,
Err(Errno::ESRCH) => KillOutcome::AlreadyGone,
Err(e) => KillOutcome::Failed(e.to_string()),
}
}
#[cfg(windows)]
fn kill_one(pid: u32, _signal: KillSignal) -> KillOutcome {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
if handle == 0 {
return KillOutcome::AlreadyGone;
}
let ok = TerminateProcess(handle, 1);
CloseHandle(handle);
if ok != 0 {
KillOutcome::Signalled
} else {
KillOutcome::Failed("TerminateProcess failed".to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn summarize_mixed_batch() {
let results = vec![
(100, KillOutcome::Signalled),
(200, KillOutcome::Signalled),
(300, KillOutcome::AlreadyGone),
(400, KillOutcome::Failed("EPERM".into())),
];
let s = summarize(&results);
assert!(s.contains("2 killed"), "{s}");
assert!(s.contains("1 already gone"), "{s}");
assert!(s.contains("1 failed (pid 400)"), "{s}");
}
#[test]
fn summarize_all_success_omits_failed_clause() {
let results = vec![(1, KillOutcome::Signalled), (2, KillOutcome::AlreadyGone)];
let s = summarize(&results);
assert!(s.contains("1 killed") && s.contains("1 already gone"));
assert!(!s.contains("failed"), "no failures → no failed clause: {s}");
}
#[test]
fn summarize_empty_is_nothing_to_kill() {
assert_eq!(summarize(&[]), "csm reap: nothing to kill");
}
#[cfg(unix)]
#[test]
fn kill_nonexistent_pid_is_already_gone() {
let outcome = kill_one(4_000_000_000, KillSignal::Term);
assert!(
matches!(outcome, KillOutcome::AlreadyGone | KillOutcome::Failed(_)),
"high nonexistent pid should be AlreadyGone (or Failed on EPERM), got {outcome:?}"
);
}
}