#[cfg(unix)]
pub fn detach_session(cmd: &mut tokio::process::Command) {
unsafe {
use std::os::unix::process::CommandExt;
cmd.as_std_mut().pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
#[cfg(not(unix))]
pub fn detach_session(_cmd: &mut tokio::process::Command) {}
#[cfg(unix)]
static LIVE_GROUPS: std::sync::Mutex<Vec<u32>> = std::sync::Mutex::new(Vec::new());
#[cfg(unix)]
pub(crate) fn register_group(pgid: u32) {
if pgid <= 1 {
return;
}
let mut g = LIVE_GROUPS.lock().unwrap_or_else(|e| e.into_inner());
if !g.contains(&pgid) {
g.push(pgid);
}
}
#[cfg(unix)]
pub(crate) fn unregister_group(pgid: u32) {
let mut g = LIVE_GROUPS.lock().unwrap_or_else(|e| e.into_inner());
if let Some(i) = g.iter().position(|&p| p == pgid) {
g.swap_remove(i);
}
}
#[cfg(unix)]
pub(crate) fn reap_all_groups() {
let pgids: Vec<u32> = {
let g = LIVE_GROUPS.lock().unwrap_or_else(|e| e.into_inner());
g.clone()
};
reap_groups(&pgids);
}
#[cfg(unix)]
fn reap_groups(pgids: &[u32]) {
for &pgid in pgids {
if pgid > 1 {
unsafe {
let _ = libc::kill(-(pgid as libc::pid_t), libc::SIGKILL);
}
}
}
}
#[cfg_attr(not(unix), allow(dead_code))]
pub struct ProcessGroupGuard {
#[cfg(unix)]
pgid: u32,
}
impl ProcessGroupGuard {
pub fn from_pid(pid: Option<u32>) -> Option<Self> {
#[cfg(unix)]
{
pid.filter(|&p| p > 1).map(|pgid| {
register_group(pgid);
Self { pgid }
})
}
#[cfg(not(unix))]
{
let _ = pid;
None
}
}
}
#[cfg(unix)]
impl Drop for ProcessGroupGuard {
fn drop(&mut self) {
if self.pgid <= 1 {
return;
}
unregister_group(self.pgid);
unsafe {
let _ = libc::kill(-(self.pgid as libc::pid_t), libc::SIGKILL);
}
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
fn group_is_registered(pgid: u32) -> bool {
LIVE_GROUPS
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(&pgid)
}
#[test]
fn guard_registers_on_construct_and_unregisters_on_drop() {
let fake = 1_000_003;
assert!(!group_is_registered(fake));
let guard = ProcessGroupGuard::from_pid(Some(fake)).expect("guard");
assert!(
group_is_registered(fake),
"constructing a guard must register its group for the reaper"
);
drop(guard);
assert!(
!group_is_registered(fake),
"dropping a guard (normal path) must unregister its group"
);
}
#[tokio::test]
async fn reap_groups_kills_a_registered_child() {
let mut cmd = tokio::process::Command::new("sleep");
cmd.arg("30").kill_on_drop(false);
detach_session(&mut cmd);
let mut child = cmd.spawn().expect("spawn sleep");
let pid = child.id().expect("child pid");
let guard = ProcessGroupGuard::from_pid(Some(pid)).expect("guard");
assert!(
group_is_registered(pid),
"reap_all_groups() would include this pid via its registry snapshot"
);
reap_groups(&[pid]);
use std::os::unix::process::ExitStatusExt;
let status = child.wait().await.expect("wait");
assert_eq!(
status.signal(),
Some(libc::SIGKILL),
"the group reap must SIGKILL the registered child group"
);
drop(guard); }
#[test]
fn from_pid_rejects_reserved_and_missing_groups() {
assert!(ProcessGroupGuard::from_pid(None).is_none());
assert!(
ProcessGroupGuard::from_pid(Some(0)).is_none(),
"group 0 is our own"
);
assert!(
ProcessGroupGuard::from_pid(Some(1)).is_none(),
"group 1 is everything"
);
assert!(ProcessGroupGuard::from_pid(Some(4242)).is_some());
}
#[tokio::test]
async fn guard_group_kill_reaps_the_child_on_drop() {
let mut cmd = tokio::process::Command::new("sleep");
cmd.arg("30").kill_on_drop(false);
detach_session(&mut cmd);
let mut child = cmd.spawn().expect("spawn sleep");
let pid = child.id().expect("child pid");
assert_eq!(
unsafe { libc::kill(-(pid as libc::pid_t), 0) },
0,
"process group should be alive before the guard drops"
);
let guard = ProcessGroupGuard::from_pid(Some(pid)).expect("guard");
drop(guard);
use std::os::unix::process::ExitStatusExt;
let status = child.wait().await.expect("wait");
assert_eq!(
status.signal(),
Some(libc::SIGKILL),
"child must be terminated by the group SIGKILL"
);
}
}