use crate::{NoWindow, TerminationOptions};
use std::{io, process::ExitStatus, time::Duration};
use tokio::process::Child;
#[cfg(unix)]
use crate::{AdoptedIdentity, AdoptedSignal};
#[cfg(unix)]
use std::num::NonZeroU32;
#[cfg(unix)]
use tokio::time::Instant;
#[cfg(unix)]
const POLL: Duration = Duration::from_millis(20);
const KILL_WAIT: Duration = Duration::from_secs(10);
impl NoWindow for tokio::process::Command {
fn no_window(&mut self) -> &mut Self {
#[cfg(windows)]
{
self.creation_flags(crate::CREATE_NO_WINDOW);
}
self
}
}
pub async fn terminate_tree(
child: &mut Child,
options: TerminationOptions,
) -> io::Result<ExitStatus> {
if let Some(status) = child.try_wait()? {
return Ok(status);
}
let Some(pid) = child.id() else {
return wait_bounded(child).await;
};
terminate_platform(child, pid, options).await
}
#[cfg(unix)]
async fn terminate_platform(
child: &mut Child,
pid: u32,
options: TerminationOptions,
) -> io::Result<ExitStatus> {
let leads_group = unsafe { libc::getpgid(pid as libc::pid_t) } == pid as libc::pid_t;
let swept = if leads_group {
Vec::new()
} else {
sweep_targets(pid)
};
signal(pid, leads_group, libc::SIGTERM)?;
let deadline = Instant::now() + options.grace;
let mut status = None;
loop {
if status.is_none() {
status = child.try_wait()?;
}
let rest_gone = status.is_some()
&& if leads_group {
!group_exists(pid)
} else {
swept.iter().all(|t| !is_present(t))
};
if rest_gone || Instant::now() >= deadline {
break;
}
tokio::time::sleep(POLL.min(deadline.saturating_duration_since(Instant::now()))).await;
}
if status.is_none() {
signal(pid, leads_group, libc::SIGKILL)?;
} else if leads_group && group_exists(pid) {
signal(pid, true, libc::SIGKILL)?;
}
for target in &swept {
let _ = target.signal(AdoptedSignal::Force);
}
match status {
Some(status) => Ok(status),
None => wait_bounded(child).await,
}
}
#[cfg(unix)]
fn signal(pid: u32, group: bool, signal: libc::c_int) -> io::Result<()> {
let target = if group { -(pid as i32) } else { pid as i32 };
if unsafe { libc::kill(target, signal) } == 0 {
return Ok(());
}
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::ESRCH) => Ok(()),
Some(libc::EPERM) if group => Ok(()),
_ => Err(error),
}
}
#[cfg(unix)]
fn group_exists(pgid: u32) -> bool {
if unsafe { libc::kill(-(pgid as i32), 0) } == 0 {
return true;
}
io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(unix)]
fn sweep_targets(pid: u32) -> Vec<AdoptedIdentity> {
let Some(root) = NonZeroU32::new(pid) else {
return Vec::new();
};
let Ok(Some(identity)) = AdoptedIdentity::capture(root) else {
return Vec::new();
};
let mut targets = identity.descendants().unwrap_or_default();
targets.reverse(); targets
}
#[cfg(unix)]
fn is_present(target: &AdoptedIdentity) -> bool {
matches!(target.verify(), Ok(crate::IdentityCheck::Same))
}
#[cfg(not(unix))]
async fn terminate_platform(
child: &mut Child,
pid: u32,
_options: TerminationOptions,
) -> io::Result<ExitStatus> {
use crate::{AdoptedIdentity, AdoptedSignal};
let mut targets = std::num::NonZeroU32::new(pid)
.and_then(|pid| AdoptedIdentity::capture(pid).ok().flatten())
.and_then(|root| root.descendants().ok())
.unwrap_or_default();
targets.reverse(); for target in &targets {
let _ = target.signal(AdoptedSignal::Force);
}
match child.start_kill() {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::InvalidInput => {}
Err(error) => return Err(error),
}
wait_bounded(child).await
}
async fn wait_bounded(child: &mut Child) -> io::Result<ExitStatus> {
match tokio::time::timeout(KILL_WAIT, child.wait()).await {
Ok(result) => result,
Err(_) => Err(io::Error::new(
io::ErrorKind::TimedOut,
"the child did not exit after the forced stage",
)),
}
}
#[cfg(unix)]
pub async fn terminate_process_group(
pgid: NonZeroU32,
options: TerminationOptions,
) -> io::Result<bool> {
if pgid.get() as libc::pid_t == unsafe { libc::getpgrp() } {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"refusing to signal this process's own group",
));
}
let pgid = pgid.get();
if !group_exists(pgid) {
return Ok(true);
}
signal(pgid, true, libc::SIGTERM)?;
let deadline = Instant::now() + options.grace;
while group_exists(pgid) && Instant::now() < deadline {
tokio::time::sleep(POLL.min(deadline.saturating_duration_since(Instant::now()))).await;
}
if group_exists(pgid) {
signal(pgid, true, libc::SIGKILL)?;
let end = Instant::now() + Duration::from_secs(2);
while group_exists(pgid) && Instant::now() < end {
tokio::time::sleep(POLL).await;
}
}
Ok(!group_exists(pgid))
}