use std::io;
use std::sync::OnceLock;
use crate::platform::process::{
OwnerDeathCleanup, OwnerDeathCleanupError, OwnerDeathCleanupStage,
};
static JOB: OnceLock<JobHandle> = OnceLock::new();
pub fn install_owner_death_cleanup() -> Result<OwnerDeathCleanup, OwnerDeathCleanupError> {
if JOB.get().is_some() {
return Ok(OwnerDeathCleanup::KillOnOwnerHandleClose);
}
let job = create_kill_on_close_job()?;
match assign_current_process(job.raw()) {
Ok(()) => match JOB.set(job) {
Ok(()) => Ok(OwnerDeathCleanup::KillOnOwnerHandleClose),
Err(job) => {
std::mem::forget(job);
Ok(OwnerDeathCleanup::AlreadyContained)
}
},
Err(error) if is_access_denied(&error) => Ok(OwnerDeathCleanup::AlreadyContained),
Err(error) => Err(error),
}
}
pub fn owner_death_cleanup_target() -> OwnerDeathCleanup {
OwnerDeathCleanup::KillOnOwnerHandleClose
}
struct JobHandle(winapi::um::winnt::HANDLE);
unsafe impl Send for JobHandle {}
unsafe impl Sync for JobHandle {}
impl JobHandle {
fn raw(&self) -> winapi::um::winnt::HANDLE {
self.0
}
}
impl Drop for JobHandle {
fn drop(&mut self) {
unsafe {
winapi::um::handleapi::CloseHandle(self.0);
}
}
}
fn create_kill_on_close_job() -> Result<JobHandle, OwnerDeathCleanupError> {
use winapi::um::jobapi2::{CreateJobObjectW, SetInformationJobObject};
use winapi::um::winnt::{
JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
let handle = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
if handle.is_null() {
return Err(created(io::Error::last_os_error()));
}
let job = JobHandle(handle);
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let set = unsafe {
SetInformationJobObject(
job.raw(),
JobObjectExtendedLimitInformation,
(&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
if set == 0 {
return Err(created(io::Error::last_os_error()));
}
Ok(job)
}
fn assign_current_process(
job: winapi::um::winnt::HANDLE,
) -> Result<(), OwnerDeathCleanupError> {
use winapi::um::jobapi2::AssignProcessToJobObject;
use winapi::um::processthreadsapi::GetCurrentProcess;
let assigned = unsafe { AssignProcessToJobObject(job, GetCurrentProcess()) };
if assigned == 0 {
return Err(joined(io::Error::last_os_error()));
}
Ok(())
}
fn created(source: io::Error) -> OwnerDeathCleanupError {
OwnerDeathCleanupError { stage: OwnerDeathCleanupStage::CreateContainer, source }
}
fn joined(source: io::Error) -> OwnerDeathCleanupError {
OwnerDeathCleanupError { stage: OwnerDeathCleanupStage::JoinContainer, source }
}
fn is_access_denied(error: &OwnerDeathCleanupError) -> bool {
let error = &error.source;
const ERROR_ACCESS_DENIED: i32 = 5;
error.raw_os_error() == Some(ERROR_ACCESS_DENIED)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn installing_is_idempotent() {
let first = install_owner_death_cleanup().expect("install");
let second = install_owner_death_cleanup().expect("install again");
assert!(matches!(
first,
OwnerDeathCleanup::KillOnOwnerHandleClose | OwnerDeathCleanup::AlreadyContained
));
assert_eq!(first, second, "a second install must not change the answer");
}
#[test]
fn access_denied_is_recognised() {
assert!(is_access_denied(&joined(io::Error::from_raw_os_error(5))));
assert!(!is_access_denied(&joined(io::Error::from_raw_os_error(2))));
assert!(!is_access_denied(&joined(io::Error::other("no os code"))));
}
}