ashlar_native/
terminate.rs1use std::sync::{Mutex, PoisonError};
12
13pub type TerminateHook = Box<dyn FnOnce(i32) + Send>;
15
16static HOOK: Mutex<Option<TerminateHook>> = Mutex::new(None);
17
18pub fn on_terminate(hook: impl FnOnce(i32) + Send + 'static) -> bool {
21 *HOOK.lock().unwrap_or_else(PoisonError::into_inner) = Some(Box::new(hook));
22 imp::watch()
23}
24
25pub fn clear() {
27 *HOOK.lock().unwrap_or_else(PoisonError::into_inner) = None;
28}
29
30#[cfg(unix)]
31fn run_hook(signal: i32) {
32 let hook = HOOK.lock().unwrap_or_else(PoisonError::into_inner).take();
33 if let Some(hook) = hook {
34 hook(signal);
35 }
36}
37
38#[cfg(unix)]
39#[allow(unsafe_code)]
40mod imp {
41 use std::sync::OnceLock;
42 use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
43
44 const SIGNALS: [libc::c_int; 3] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP];
45
46 static WRITE_END: AtomicI32 = AtomicI32::new(-1);
47 static FIRED: AtomicBool = AtomicBool::new(false);
48 static WATCHING: OnceLock<bool> = OnceLock::new();
49
50 fn errno_ptr() -> *mut libc::c_int {
53 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
54 unsafe {
56 libc::__error()
57 }
58 #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "freebsd")))]
59 unsafe {
61 libc::__errno_location()
62 }
63 }
64
65 extern "C" fn handler(signal: libc::c_int) {
66 let saved = unsafe { *errno_ptr() };
68 handle(signal);
69 unsafe { *errno_ptr() = saved };
71 }
72
73 fn handle(signal: libc::c_int) {
74 if FIRED.swap(true, Ordering::SeqCst) {
75 unsafe {
78 libc::signal(signal, libc::SIG_DFL);
79 libc::raise(signal);
80 }
81 return;
82 }
83 let byte = signal as u8;
84 unsafe {
87 libc::write(WRITE_END.load(Ordering::SeqCst), (&raw const byte).cast(), 1);
88 }
89 }
90
91 pub(super) fn watch() -> bool {
92 *WATCHING.get_or_init(start)
93 }
94
95 fn start() -> bool {
96 let mut fds = [-1 as libc::c_int; 2];
97 if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
99 return false;
100 }
101 for fd in fds {
102 unsafe {
104 libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
105 }
106 }
107 WRITE_END.store(fds[1], Ordering::SeqCst);
108 let read_end = fds[0];
109 let spawned = std::thread::Builder::new().name("terminate-watch".into()).spawn(move || {
110 let mut byte = 0u8;
111 loop {
112 let n = unsafe { libc::read(read_end, (&raw mut byte).cast(), 1) };
114 if n == 1 {
115 break;
116 }
117 if n < 0 && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted {
118 continue;
119 }
120 return;
121 }
122 let signal = libc::c_int::from(byte);
123 super::run_hook(signal);
124 unsafe {
127 libc::signal(signal, libc::SIG_DFL);
128 libc::raise(signal);
129 }
130 });
131 if spawned.is_err() {
132 return false;
133 }
134 for signal in SIGNALS {
135 unsafe {
138 let mut action: libc::sigaction = std::mem::zeroed();
139 action.sa_sigaction = handler as *const () as libc::sighandler_t;
140 action.sa_flags = libc::SA_RESTART;
141 libc::sigemptyset(&raw mut action.sa_mask);
142 libc::sigaction(signal, &raw const action, std::ptr::null_mut());
143 }
144 }
145 true
146 }
147}
148
149#[cfg(not(unix))]
150mod imp {
151 pub(super) fn watch() -> bool {
152 false
153 }
154}
155
156#[cfg(all(test, unix))]
157mod tests {
158 #[test]
159 fn a_hook_runs_once_and_can_be_cleared() {
160 let ran = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
161 let r = ran.clone();
162 assert!(super::on_terminate(move |sig| r.store(sig, std::sync::atomic::Ordering::SeqCst)));
163 super::run_hook(15);
164 super::run_hook(2);
165 assert_eq!(ran.load(std::sync::atomic::Ordering::SeqCst), 15);
166 super::on_terminate(|_| panic!("cleared"));
167 super::clear();
168 super::run_hook(15);
169 }
170}