use std::collections::BTreeSet;
use std::thread;
use std::time::Duration;
use sysinfo::Pid;
#[cfg(unix)]
use sysinfo::Signal;
use crate::error::AppResult;
use crate::process::ProcessCatalog;
pub struct KillOptions {
pub force: bool,
pub silent: bool,
pub force_after_timeout: u64,
pub wait_for_exit: u64,
}
pub fn kill_processes(
catalog: &mut ProcessCatalog,
pids: &BTreeSet<Pid>,
options: &KillOptions,
) -> AppResult<()> {
if pids.is_empty() {
return Err("no matching processes found".to_string());
}
let mut failed = Vec::new();
let mut killed_any = false;
for pid in pids {
if *pid == catalog.current_pid() {
failed.push(format!(
"{} (refusing to kill current process)",
pid.as_u32()
));
continue;
}
catalog.refresh();
let Some(process) = catalog.system().process(*pid) else {
failed.push(format!("{} (already exited)", pid.as_u32()));
continue;
};
let name = process.name().to_string_lossy().into_owned();
let attempted_graceful = !options.force && try_terminate(process);
let killed = if attempted_graceful {
wait_until_exit_or_timeout(
catalog,
*pid,
Duration::from_millis(options.force_after_timeout),
) || force_kill(catalog, *pid)
} else {
force_kill(catalog, *pid)
};
let exited = if killed {
wait_until_exit_or_timeout(catalog, *pid, Duration::from_millis(options.wait_for_exit))
} else {
false
};
if exited {
killed_any = true;
if !options.silent {
let mode = if options.force || !attempted_graceful {
"force"
} else {
"graceful"
};
println!("Killed {} ({name}) via {mode}", pid.as_u32());
}
} else {
failed.push(format!("{} ({name})", pid.as_u32()));
}
}
if !failed.is_empty() {
return Err(format!("failed to kill: {}", failed.join(", ")));
}
if !killed_any && !options.silent {
println!("No processes were terminated.");
}
Ok(())
}
#[cfg(unix)]
fn try_terminate(process: &sysinfo::Process) -> bool {
process.kill_with(Signal::Term).unwrap_or(false)
}
#[cfg(not(unix))]
fn try_terminate(_process: &sysinfo::Process) -> bool {
false
}
fn force_kill(catalog: &mut ProcessCatalog, pid: Pid) -> bool {
catalog.refresh();
match catalog.system().process(pid) {
Some(process) => process.kill(),
None => true,
}
}
fn wait_until_exit_or_timeout(catalog: &mut ProcessCatalog, pid: Pid, timeout: Duration) -> bool {
for sleep_ms in backoff_intervals(timeout) {
catalog.refresh();
if catalog.system().process(pid).is_none() {
return true;
}
thread::sleep(Duration::from_millis(sleep_ms));
}
catalog.refresh();
catalog.system().process(pid).is_none()
}
fn backoff_intervals(timeout: Duration) -> Vec<u64> {
let timeout_ms = timeout.as_millis() as u64;
if timeout_ms == 0 {
return Vec::new();
}
let mut intervals = Vec::new();
let mut elapsed = 0_u64;
let mut current = 50_u64;
while elapsed < timeout_ms {
let next = current.min(timeout_ms - elapsed);
intervals.push(next);
elapsed += next;
current = (current.saturating_mul(2)).min(1_000);
}
intervals
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::backoff_intervals;
#[test]
fn backoff_stays_within_timeout() {
let intervals = backoff_intervals(Duration::from_millis(1_500));
assert_eq!(intervals.iter().sum::<u64>(), 1_500);
}
#[test]
fn backoff_grows_then_caps() {
let intervals = backoff_intervals(Duration::from_millis(3_000));
assert_eq!(&intervals[..5], &[50, 100, 200, 400, 800]);
assert!(intervals.iter().all(|value| *value <= 1_000));
}
}