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 fn command(program: impl AsRef<std::ffi::OsStr>) -> tokio::process::Command {
let mut command = tokio::process::Command::new(program);
command.no_window();
command
}
pub fn lead_session(command: &mut tokio::process::Command) -> &mut tokio::process::Command {
#[cfg(unix)]
{
unsafe {
command.pre_exec(|| {
if libc::setsid() < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
}
command
}
#[cfg(unix)]
pub async fn terminate_session(
sid: std::num::NonZeroU32,
options: TerminationOptions,
) -> io::Result<bool> {
let groups: Vec<u32> = crate::foreign::session_groups(sid)?
.into_iter()
.filter_map(|group| u32::try_from(group).ok())
.collect();
if groups.is_empty() {
return Ok(false);
}
for &group in &groups {
signal(group, true, libc::SIGTERM)?;
}
let deadline = Instant::now() + options.grace;
while groups.iter().any(|&group| group_exists(group)) && Instant::now() < deadline {
tokio::time::sleep(POLL).await;
}
for &group in &groups {
if group_exists(group) {
signal(group, true, libc::SIGKILL)?;
}
}
let kill_deadline = Instant::now() + KILL_WAIT;
while groups.iter().any(|&group| group_exists(group)) && Instant::now() < kill_deadline {
tokio::time::sleep(POLL).await;
}
Ok(true)
}
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)]
pub fn process_group_exists(pgid: NonZeroU32) -> io::Result<bool> {
crate::foreign::process_group_exists(pgid)
}
#[cfg(unix)]
pub async fn wait_process_group_empty(pgid: NonZeroU32, timeout: Duration) -> io::Result<bool> {
crate::foreign::check_group_id(pgid)?;
let deadline = Instant::now() + timeout;
loop {
if !crate::foreign::process_group_exists(pgid)? {
return Ok(true);
}
if Instant::now() >= deadline {
return Ok(false);
}
tokio::time::sleep(POLL.min(deadline.saturating_duration_since(Instant::now()))).await;
}
}
#[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> {
crate::foreign::check_group_id(pgid)?;
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))
}