#[cfg(unix)]
use std::time::{Duration, Instant};
#[cfg(unix)]
const DEFAULT_KILL_GRACE: Duration = Duration::from_secs(3);
#[cfg(unix)]
const KILL_POLL_INTERVAL: Duration = Duration::from_millis(25);
#[cfg(unix)]
pub fn cleanup_process_group(child: &tokio::process::Child) {
if let Some(pid) = child.id() {
unsafe {
libc::kill(-(pid as i32), libc::SIGTERM);
}
}
}
#[cfg(not(unix))]
pub fn cleanup_process_group(_child: &tokio::process::Child) {}
#[cfg(unix)]
pub fn force_kill_process_group(child: &tokio::process::Child) {
if let Some(pid) = child.id() {
kill_with_grace(pid as i32, DEFAULT_KILL_GRACE);
}
}
#[cfg(not(unix))]
pub fn force_kill_process_group(_child: &tokio::process::Child) {}
#[cfg(unix)]
pub fn kill_with_grace(pgid: i32, grace: Duration) {
if pgid <= 0 {
return;
}
kill_group_and(pgid, &descendants(pgid as u32), grace);
}
#[cfg(unix)]
pub fn kill_group_with_grace(pgid: i32, grace: Duration) {
if pgid > 0 {
kill_group_and(pgid, &[], grace);
}
}
#[cfg(unix)]
fn kill_group_and(pgid: i32, descendants: &[ProcessEntry], grace: Duration) {
let pids = descendants.iter().map(|entry| entry.pid).collect::<Vec<_>>();
signal_process_group(pgid, libc::SIGTERM);
signal_processes(&pids, libc::SIGTERM);
wait_for_process_group_exit(pgid, grace);
if process_group_exists(pgid) {
signal_process_group(pgid, libc::SIGKILL);
}
signal_processes(&unchanged_pids(descendants, &process_table()), libc::SIGKILL);
}
#[cfg(unix)]
fn signal_processes(pids: &[u32], signal: i32) {
for &pid in pids {
if pid > 1 && pid <= i32::MAX as u32 {
unsafe {
libc::kill(-(pid as i32), signal);
libc::kill(pid as i32, signal);
}
}
}
}
#[cfg(unix)]
#[derive(Debug, Clone, PartialEq, Eq)]
struct ProcessEntry {
pid: u32,
ppid: u32,
started: String,
}
#[cfg(unix)]
pub fn descendant_pids(root: u32) -> Vec<u32> {
descendants(root).into_iter().map(|entry| entry.pid).collect()
}
#[cfg(unix)]
fn descendants(root: u32) -> Vec<ProcessEntry> {
if root <= 1 {
return Vec::new();
}
descendants_in_table(root, &process_table())
}
#[cfg(unix)]
fn process_table() -> Vec<ProcessEntry> {
std::process::Command::new("ps")
.args(["-A", "-o", "pid=", "-o", "ppid=", "-o", "lstart="])
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| parse_table(&String::from_utf8_lossy(&output.stdout)))
.unwrap_or_default()
}
#[cfg(unix)]
fn parse_table(table: &str) -> Vec<ProcessEntry> {
table
.lines()
.filter_map(|line| {
let mut fields = line.split_whitespace();
let pid = fields.next()?.parse::<u32>().ok()?;
let ppid = fields.next()?.parse::<u32>().ok()?;
let started = fields.collect::<Vec<_>>().join(" ");
(!started.is_empty()).then_some(ProcessEntry { pid, ppid, started })
})
.collect()
}
#[cfg(unix)]
fn descendants_in_table(root: u32, table: &[ProcessEntry]) -> Vec<ProcessEntry> {
let mut found: Vec<ProcessEntry> = Vec::new();
let mut frontier = vec![root];
while let Some(parent) = frontier.pop() {
for entry in table {
let fresh = !found.iter().any(|seen| seen.pid == entry.pid);
if entry.ppid == parent && entry.pid != root && entry.pid > 1 && fresh {
found.push(entry.clone());
frontier.push(entry.pid);
}
}
}
found
}
#[cfg(unix)]
fn unchanged_pids(snapshot: &[ProcessEntry], now: &[ProcessEntry]) -> Vec<u32> {
snapshot
.iter()
.filter(|old| now.iter().any(|cur| cur.pid == old.pid && cur.started == old.started))
.map(|old| old.pid)
.collect()
}
#[cfg(unix)]
fn wait_for_process_group_exit(pgid: i32, grace: Duration) {
let start = Instant::now();
while start.elapsed() < grace {
if !process_group_exists(pgid) {
return;
}
std::thread::sleep(KILL_POLL_INTERVAL.min(grace.saturating_sub(start.elapsed())));
}
}
#[cfg(unix)]
fn process_group_exists(pgid: i32) -> bool {
let result = unsafe { libc::kill(-pgid, 0) };
if result == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(unix)]
fn signal_process_group(pgid: i32, signal: i32) {
unsafe {
libc::kill(-pgid, signal);
}
}
#[cfg(test)]
#[cfg(unix)]
#[path = "process_group_tests.rs"]
mod tests;