#[cfg(target_os = "linux")]
pub fn spawn(
cmd: &mut tokio::process::Command,
) -> std::io::Result<tokio::process::Child> {
cmd.kill_on_drop(true);
let parent_pid = std::process::id();
unsafe {
cmd.pre_exec(move || {
if nix::libc::prctl(nix::libc::PR_SET_PDEATHSIG, nix::libc::SIGKILL) != 0 {
return Err(std::io::Error::last_os_error());
}
if nix::libc::getppid() != parent_pid as nix::libc::pid_t {
return Err(std::io::Error::from_raw_os_error(nix::libc::ESRCH));
}
Ok(())
});
}
cmd.spawn()
}
#[cfg(windows)]
pub fn spawn(
cmd: &mut tokio::process::Command,
) -> std::io::Result<tokio::process::Child> {
cmd.kill_on_drop(true);
let mut child = cmd.spawn()?;
if let Err(e) = assign_to_reaper_job(&child) {
let _ = child.start_kill();
return Err(e);
}
Ok(child)
}
#[cfg(target_os = "macos")]
pub fn spawn(
cmd: &mut tokio::process::Command,
) -> std::io::Result<tokio::process::Child> {
cmd.kill_on_drop(true);
let child = cmd.spawn()?;
let child_pid = match child.id() {
Some(p) => p,
None => return Ok(child),
};
let parent_pid = std::process::id();
if let Err(e) = spawn_guardian(parent_pid, child_pid) {
unsafe {
nix::libc::kill(child_pid as nix::libc::pid_t, nix::libc::SIGKILL);
}
return Err(e);
}
Ok(child)
}
#[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
pub fn spawn(
cmd: &mut tokio::process::Command,
) -> std::io::Result<tokio::process::Child> {
cmd.kill_on_drop(true);
cmd.spawn()
}
#[cfg(target_os = "macos")]
pub fn run_guardian_if_invoked() {
let mut args = std::env::args().skip(1);
if args.next().as_deref() != Some(GUARDIAN_FLAG) {
return;
}
let parent_pid = match args.next().and_then(|s| s.parse::<u32>().ok()) {
Some(p) => p,
None => std::process::exit(2),
};
let child_pid = match args.next().and_then(|s| s.parse::<u32>().ok()) {
Some(p) => p,
None => std::process::exit(2),
};
guardian_main(parent_pid, child_pid);
}
#[cfg(not(target_os = "macos"))]
pub fn run_guardian_if_invoked() {}
#[cfg(windows)]
static REAPER_JOB: std::sync::OnceLock<isize> = std::sync::OnceLock::new();
#[cfg(windows)]
fn assign_to_reaper_job(child: &tokio::process::Child) -> std::io::Result<()> {
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
let child_handle = child
.raw_handle()
.ok_or_else(|| std::io::Error::other("child has no process handle"))?;
let job = match *REAPER_JOB.get_or_init(|| create_kill_on_close_job().unwrap_or(0)) {
0 => create_kill_on_close_job()?,
j => j,
};
if unsafe {
AssignProcessToJobObject(
job as *mut core::ffi::c_void,
child_handle as *mut core::ffi::c_void,
)
} == 0
{
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(windows)]
fn create_kill_on_close_job() -> std::io::Result<isize> {
use windows_sys::Win32::System::JobObjects::{
CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject,
};
unsafe {
let job = CreateJobObjectW(std::ptr::null(), std::ptr::null());
if job.is_null() {
return Err(std::io::Error::last_os_error());
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&info as *const _ as *const core::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
) == 0
{
return Err(std::io::Error::last_os_error());
}
Ok(job as isize)
}
}
#[cfg(target_os = "macos")]
const GUARDIAN_FLAG: &str = "--__objectiveai-subprocess-reaper-guardian";
#[cfg(target_os = "macos")]
fn spawn_guardian(parent_pid: u32, child_pid: u32) -> std::io::Result<()> {
let exe = std::env::current_exe()?;
std::process::Command::new(exe)
.arg(GUARDIAN_FLAG)
.arg(parent_pid.to_string())
.arg(child_pid.to_string())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
Ok(())
}
#[cfg(target_os = "macos")]
fn guardian_main(parent_pid: u32, child_pid: u32) -> ! {
use nix::sys::event::{EventFilter, EventFlag, FilterFlag, KEvent, Kqueue};
let kqueue = match Kqueue::new() {
Ok(k) => k,
Err(_) => std::process::exit(1),
};
let now = nix::libc::timespec { tv_sec: 0, tv_nsec: 0 };
let parent_reg = [KEvent::new(
parent_pid as usize,
EventFilter::EVFILT_PROC,
EventFlag::EV_ADD | EventFlag::EV_ONESHOT,
FilterFlag::NOTE_EXIT,
0,
0,
)];
if kqueue.kevent(&parent_reg, &mut [], Some(now)).is_err() {
unsafe {
nix::libc::kill(child_pid as nix::libc::pid_t, nix::libc::SIGKILL);
}
std::process::exit(0);
}
let child_reg = [KEvent::new(
child_pid as usize,
EventFilter::EVFILT_PROC,
EventFlag::EV_ADD | EventFlag::EV_ONESHOT,
FilterFlag::NOTE_EXIT,
0,
0,
)];
if kqueue.kevent(&child_reg, &mut [], Some(now)).is_err() {
std::process::exit(0);
}
let mut events = [
KEvent::new(0, EventFilter::EVFILT_PROC, EventFlag::empty(), FilterFlag::empty(), 0, 0),
KEvent::new(0, EventFilter::EVFILT_PROC, EventFlag::empty(), FilterFlag::empty(), 0, 0),
];
loop {
match kqueue.kevent(&[], &mut events, None) {
Ok(n) => {
for ev in &events[..n] {
if ev.ident() == parent_pid as usize {
unsafe {
nix::libc::kill(child_pid as nix::libc::pid_t, nix::libc::SIGKILL);
}
std::process::exit(0);
}
if ev.ident() == child_pid as usize {
std::process::exit(0);
}
}
}
Err(nix::errno::Errno::EINTR) => continue,
Err(_) => std::process::exit(1),
}
}
}