use std::{io, time::Duration};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessTreeCleanup {
LinuxParentDeathSignal,
WindowsKillOnJobClose,
WindowsAlreadyInJob,
MacosKqueueSupervisorContract,
UnsupportedNoop,
}
pub const MACOS_SUPERVISOR_KILL_DEADLINE: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MacosSupervisorContract {
pub watch_pid: MacosSupervisorWatchPid,
pub kqueue_filter: MacosKqueueFilter,
pub kqueue_note: MacosKqueueNote,
pub registration_barrier: MacosSupervisorRegistrationBarrier,
pub race_guard: MacosSupervisorRaceGuard,
pub exit_action: MacosSupervisorExitAction,
pub kill_deadline: Duration,
}
impl MacosSupervisorContract {
pub const fn phase5() -> Self {
Self {
watch_pid: MacosSupervisorWatchPid::BrokerParent,
kqueue_filter: MacosKqueueFilter::Process,
kqueue_note: MacosKqueueNote::Exit,
registration_barrier: MacosSupervisorRegistrationBarrier::BeforeBackendPipePublication,
race_guard: MacosSupervisorRaceGuard::RecheckBrokerAliveAfterRegistration,
exit_action: MacosSupervisorExitAction::SigkillBackend,
kill_deadline: MACOS_SUPERVISOR_KILL_DEADLINE,
}
}
pub const fn kqueue_filter_name(&self) -> &'static str {
match self.kqueue_filter {
MacosKqueueFilter::Process => "EVFILT_PROC",
}
}
pub const fn kqueue_note_name(&self) -> &'static str {
match self.kqueue_note {
MacosKqueueNote::Exit => "NOTE_EXIT",
}
}
pub const fn termination_signal_name(&self) -> &'static str {
match self.exit_action {
MacosSupervisorExitAction::SigkillBackend => "SIGKILL",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacosSupervisorWatchPid {
BrokerParent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacosKqueueFilter {
Process,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacosKqueueNote {
Exit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacosSupervisorRegistrationBarrier {
BeforeBackendPipePublication,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacosSupervisorRaceGuard {
RecheckBrokerAliveAfterRegistration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacosSupervisorExitAction {
SigkillBackend,
}
pub const fn macos_supervisor_contract() -> MacosSupervisorContract {
MacosSupervisorContract::phase5()
}
#[derive(Debug, thiserror::Error)]
pub enum ProcessTreeError {
#[error("failed to install Linux parent-death signal: {0}")]
LinuxParentDeathSignal(io::Error),
#[error("failed to create Windows kill-on-close Job Object: {0}")]
WindowsJobCreate(io::Error),
#[error("failed to assign broker process to Windows Job Object: {0}")]
WindowsJobAssign(io::Error),
}
pub fn install_cleanup() -> Result<ProcessTreeCleanup, ProcessTreeError> {
platform_install_cleanup()
}
pub fn cleanup_target() -> ProcessTreeCleanup {
cleanup_target_for_platform(current_platform())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CleanupPlatform {
#[cfg(any(target_os = "linux", test))]
Linux,
#[cfg(any(windows, test))]
Windows,
#[cfg(any(target_os = "macos", test))]
Macos,
#[cfg(any(
all(unix, not(any(target_os = "linux", target_os = "macos"))),
all(not(unix), not(windows)),
test
))]
Other,
}
fn cleanup_target_for_platform(platform: CleanupPlatform) -> ProcessTreeCleanup {
match platform {
#[cfg(any(target_os = "linux", test))]
CleanupPlatform::Linux => ProcessTreeCleanup::LinuxParentDeathSignal,
#[cfg(any(windows, test))]
CleanupPlatform::Windows => ProcessTreeCleanup::WindowsKillOnJobClose,
#[cfg(any(target_os = "macos", test))]
CleanupPlatform::Macos => ProcessTreeCleanup::MacosKqueueSupervisorContract,
#[cfg(any(
all(unix, not(any(target_os = "linux", target_os = "macos"))),
all(not(unix), not(windows)),
test
))]
CleanupPlatform::Other => ProcessTreeCleanup::UnsupportedNoop,
}
}
#[cfg(target_os = "linux")]
fn current_platform() -> CleanupPlatform {
CleanupPlatform::Linux
}
#[cfg(windows)]
fn current_platform() -> CleanupPlatform {
CleanupPlatform::Windows
}
#[cfg(target_os = "macos")]
fn current_platform() -> CleanupPlatform {
CleanupPlatform::Macos
}
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
fn current_platform() -> CleanupPlatform {
CleanupPlatform::Other
}
#[cfg(all(not(unix), not(windows)))]
fn current_platform() -> CleanupPlatform {
CleanupPlatform::Other
}
#[cfg(target_os = "linux")]
fn platform_install_cleanup() -> Result<ProcessTreeCleanup, ProcessTreeError> {
let rc = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, linux_parent_death_signal()) };
if rc == -1 {
Err(ProcessTreeError::LinuxParentDeathSignal(
io::Error::last_os_error(),
))
} else {
Ok(ProcessTreeCleanup::LinuxParentDeathSignal)
}
}
#[cfg(target_os = "linux")]
fn linux_parent_death_signal() -> libc::c_int {
libc::SIGTERM
}
#[cfg(windows)]
fn platform_install_cleanup() -> Result<ProcessTreeCleanup, ProcessTreeError> {
if JOB_HANDLE.get().is_some() {
return Ok(ProcessTreeCleanup::WindowsKillOnJobClose);
}
let job = create_kill_on_close_job()?;
match assign_current_process_to_job(job.as_raw()) {
Ok(()) => match JOB_HANDLE.set(job) {
Ok(()) => Ok(ProcessTreeCleanup::WindowsKillOnJobClose),
Err(job) => {
std::mem::forget(job);
Ok(ProcessTreeCleanup::WindowsAlreadyInJob)
}
},
Err(source) if windows_error_is_access_denied(&source) => {
Ok(ProcessTreeCleanup::WindowsAlreadyInJob)
}
Err(source) => Err(ProcessTreeError::WindowsJobAssign(source)),
}
}
#[cfg(target_os = "macos")]
fn platform_install_cleanup() -> Result<ProcessTreeCleanup, ProcessTreeError> {
Ok(ProcessTreeCleanup::MacosKqueueSupervisorContract)
}
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
fn platform_install_cleanup() -> Result<ProcessTreeCleanup, ProcessTreeError> {
Ok(ProcessTreeCleanup::UnsupportedNoop)
}
#[cfg(all(not(unix), not(windows)))]
fn platform_install_cleanup() -> Result<ProcessTreeCleanup, ProcessTreeError> {
Ok(ProcessTreeCleanup::UnsupportedNoop)
}
#[cfg(windows)]
static JOB_HANDLE: std::sync::OnceLock<WindowsJobHandle> = std::sync::OnceLock::new();
#[cfg(windows)]
struct WindowsJobHandle(usize);
#[cfg(windows)]
impl WindowsJobHandle {
fn as_raw(&self) -> winapi::um::winnt::HANDLE {
self.0 as winapi::um::winnt::HANDLE
}
}
#[cfg(windows)]
impl Drop for WindowsJobHandle {
fn drop(&mut self) {
unsafe {
winapi::um::handleapi::CloseHandle(self.as_raw());
}
}
}
#[cfg(windows)]
fn create_kill_on_close_job() -> Result<WindowsJobHandle, ProcessTreeError> {
use winapi::shared::minwindef::FALSE;
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
use winapi::um::jobapi2::{CreateJobObjectW, SetInformationJobObject};
use winapi::um::winnt::{
JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
if job.is_null() || job == INVALID_HANDLE_VALUE {
return Err(ProcessTreeError::WindowsJobCreate(
io::Error::last_os_error(),
));
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK;
let ok = unsafe {
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
(&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
if ok == FALSE {
let err = io::Error::last_os_error();
unsafe { CloseHandle(job) };
return Err(ProcessTreeError::WindowsJobCreate(err));
}
Ok(WindowsJobHandle(job as usize))
}
#[cfg(windows)]
fn assign_current_process_to_job(job: winapi::um::winnt::HANDLE) -> Result<(), io::Error> {
use winapi::shared::minwindef::FALSE;
use winapi::um::jobapi2::AssignProcessToJobObject;
use winapi::um::processthreadsapi::GetCurrentProcess;
let ok = unsafe { AssignProcessToJobObject(job, GetCurrentProcess()) };
if ok == FALSE {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(windows)]
fn windows_error_is_access_denied(err: &io::Error) -> bool {
use winapi::shared::winerror::ERROR_ACCESS_DENIED;
err.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cleanup_target_model_states_phase_5_platform_contracts() {
assert_eq!(
cleanup_target_for_platform(CleanupPlatform::Linux),
ProcessTreeCleanup::LinuxParentDeathSignal
);
assert_eq!(
cleanup_target_for_platform(CleanupPlatform::Windows),
ProcessTreeCleanup::WindowsKillOnJobClose
);
assert_eq!(
cleanup_target_for_platform(CleanupPlatform::Macos),
ProcessTreeCleanup::MacosKqueueSupervisorContract
);
assert_eq!(
cleanup_target_for_platform(CleanupPlatform::Other),
ProcessTreeCleanup::UnsupportedNoop
);
}
#[test]
fn cleanup_target_is_explicit_for_current_platform() {
#[cfg(target_os = "linux")]
assert_eq!(cleanup_target(), ProcessTreeCleanup::LinuxParentDeathSignal);
#[cfg(windows)]
assert_eq!(cleanup_target(), ProcessTreeCleanup::WindowsKillOnJobClose);
#[cfg(target_os = "macos")]
assert_eq!(
cleanup_target(),
ProcessTreeCleanup::MacosKqueueSupervisorContract
);
#[cfg(all(not(any(target_os = "linux", target_os = "macos")), not(windows)))]
assert_eq!(cleanup_target(), ProcessTreeCleanup::UnsupportedNoop);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_parent_death_signal_is_sigterm() {
assert_eq!(linux_parent_death_signal(), libc::SIGTERM);
}
#[test]
fn macos_supervisor_contract_pins_phase_5_cleanup_requirements() {
let contract = macos_supervisor_contract();
assert_eq!(contract.watch_pid, MacosSupervisorWatchPid::BrokerParent);
assert_eq!(contract.kqueue_filter_name(), "EVFILT_PROC");
assert_eq!(contract.kqueue_note_name(), "NOTE_EXIT");
assert_eq!(
contract.registration_barrier,
MacosSupervisorRegistrationBarrier::BeforeBackendPipePublication
);
assert_eq!(
contract.race_guard,
MacosSupervisorRaceGuard::RecheckBrokerAliveAfterRegistration
);
assert_eq!(contract.termination_signal_name(), "SIGKILL");
assert_eq!(contract.kill_deadline, Duration::from_secs(5));
}
}