Skip to main content

ashlar_native/
terminate.rs

1//! Run a hook when the process is asked to stop, then stop as asked. On Unix that is SIGINT
2//! (Ctrl-C), SIGTERM and SIGHUP: the first one runs the hook on a thread of its own, after which
3//! the signal is delivered again with its default action, so the process ends the way it would
4//! have (a shell sees "killed by SIGTERM"). A second signal while the hook runs ends the process at
5//! once. For a long-running command-line process (a stdio server) that must write something down
6//! before it goes. Always compiled; on other systems [`on_terminate`] does nothing.
7//!
8//! The signal handler only writes the signal's number to a pipe (async-signal-safe); a thread
9//! reads it and runs the hook. Signal masks are not touched, so child processes start as usual.
10
11use std::sync::{Mutex, PoisonError};
12
13/// Called once, with the signal's number, on its own thread.
14pub type TerminateHook = Box<dyn FnOnce(i32) + Send>;
15
16static HOOK: Mutex<Option<TerminateHook>> = Mutex::new(None);
17
18/// Install `hook` (replacing an earlier one that has not run). True when the signals are watched:
19/// on Unix, once the pipe and the watcher thread exist; false elsewhere.
20pub 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
25/// Remove the hook if it has not run (the signals then end the process as before).
26pub 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    /// The calling thread's errno. A handler that calls write() must put errno back, or the code
51    /// it interrupted may read a value it didn't cause.
52    fn errno_ptr() -> *mut libc::c_int {
53        #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
54        // SAFETY: returns the calling thread's errno location; always valid.
55        unsafe {
56            libc::__error()
57        }
58        #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "freebsd")))]
59        // SAFETY: returns the calling thread's errno location; always valid.
60        unsafe {
61            libc::__errno_location()
62        }
63    }
64
65    extern "C" fn handler(signal: libc::c_int) {
66        // SAFETY: reading and writing this thread's errno is async-signal-safe.
67        let saved = unsafe { *errno_ptr() };
68        handle(signal);
69        // SAFETY: as above.
70        unsafe { *errno_ptr() = saved };
71    }
72
73    fn handle(signal: libc::c_int) {
74        if FIRED.swap(true, Ordering::SeqCst) {
75            // a second request while the hook runs: stop now
76            // SAFETY: signal() and raise() are async-signal-safe; SIG_DFL is a valid disposition.
77            unsafe {
78                libc::signal(signal, libc::SIG_DFL);
79                libc::raise(signal);
80            }
81            return;
82        }
83        let byte = signal as u8;
84        // SAFETY: write() is async-signal-safe; the buffer is one valid byte on this stack. A
85        // failed write (no pipe) is ignored: the watcher never runs and nothing is stopped twice.
86        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        // SAFETY: `fds` is a valid array of two ints for pipe() to fill.
98        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
99            return false;
100        }
101        for fd in fds {
102            // SAFETY: fd is an open descriptor this function just created.
103            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                // SAFETY: reads at most one byte into `byte`, a valid buffer, from an open descriptor.
113                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            // then end as the signal would have
125            // SAFETY: restoring the default disposition and raising a standard signal.
126            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            // SAFETY: a zeroed sigaction with a valid handler, an empty mask and SA_RESTART is a
136            // valid argument; the previous action is not needed.
137            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}