use std::sync::{Mutex, PoisonError};
pub type TerminateHook = Box<dyn FnOnce(i32) + Send>;
static HOOK: Mutex<Option<TerminateHook>> = Mutex::new(None);
pub fn on_terminate(hook: impl FnOnce(i32) + Send + 'static) -> bool {
*HOOK.lock().unwrap_or_else(PoisonError::into_inner) = Some(Box::new(hook));
imp::watch()
}
pub fn clear() {
*HOOK.lock().unwrap_or_else(PoisonError::into_inner) = None;
}
#[cfg(unix)]
fn run_hook(signal: i32) {
let hook = HOOK.lock().unwrap_or_else(PoisonError::into_inner).take();
if let Some(hook) = hook {
hook(signal);
}
}
#[cfg(unix)]
#[allow(unsafe_code)]
mod imp {
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
const SIGNALS: [libc::c_int; 3] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP];
static WRITE_END: AtomicI32 = AtomicI32::new(-1);
static FIRED: AtomicBool = AtomicBool::new(false);
static WATCHING: OnceLock<bool> = OnceLock::new();
fn errno_ptr() -> *mut libc::c_int {
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
unsafe {
libc::__error()
}
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "freebsd")))]
unsafe {
libc::__errno_location()
}
}
extern "C" fn handler(signal: libc::c_int) {
let saved = unsafe { *errno_ptr() };
handle(signal);
unsafe { *errno_ptr() = saved };
}
fn handle(signal: libc::c_int) {
if FIRED.swap(true, Ordering::SeqCst) {
unsafe {
libc::signal(signal, libc::SIG_DFL);
libc::raise(signal);
}
return;
}
let byte = signal as u8;
unsafe {
libc::write(WRITE_END.load(Ordering::SeqCst), (&raw const byte).cast(), 1);
}
}
pub(super) fn watch() -> bool {
*WATCHING.get_or_init(start)
}
fn start() -> bool {
let mut fds = [-1 as libc::c_int; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return false;
}
for fd in fds {
unsafe {
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
}
}
WRITE_END.store(fds[1], Ordering::SeqCst);
let read_end = fds[0];
let spawned = std::thread::Builder::new().name("terminate-watch".into()).spawn(move || {
let mut byte = 0u8;
loop {
let n = unsafe { libc::read(read_end, (&raw mut byte).cast(), 1) };
if n == 1 {
break;
}
if n < 0 && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted {
continue;
}
return;
}
let signal = libc::c_int::from(byte);
super::run_hook(signal);
unsafe {
libc::signal(signal, libc::SIG_DFL);
libc::raise(signal);
}
});
if spawned.is_err() {
return false;
}
for signal in SIGNALS {
unsafe {
let mut action: libc::sigaction = std::mem::zeroed();
action.sa_sigaction = handler as *const () as libc::sighandler_t;
action.sa_flags = libc::SA_RESTART;
libc::sigemptyset(&raw mut action.sa_mask);
libc::sigaction(signal, &raw const action, std::ptr::null_mut());
}
}
true
}
}
#[cfg(not(unix))]
mod imp {
pub(super) fn watch() -> bool {
false
}
}
#[cfg(all(test, unix))]
mod tests {
#[test]
fn a_hook_runs_once_and_can_be_cleared() {
let ran = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let r = ran.clone();
assert!(super::on_terminate(move |sig| r.store(sig, std::sync::atomic::Ordering::SeqCst)));
super::run_hook(15);
super::run_hook(2);
assert_eq!(ran.load(std::sync::atomic::Ordering::SeqCst), 15);
super::on_terminate(|_| panic!("cleared"));
super::clear();
super::run_hook(15);
}
}