Skip to main content

bynk/
stop.rs

1//! Stopping `bynk dev` from outside (#1955).
2//!
3//! Without a handler, a SIGTERM or SIGINT sent to `bynk dev` alone (by a CI
4//! step, a test harness, an editor task) killed it on the spot, and its
5//! wranglers and their `workerd`s ran on with their ports bound. A Ctrl-C in a
6//! terminal never showed it, because the terminal signals the whole foreground
7//! group, wranglers included.
8//!
9//! [`install`] replaces the default action with one that only records the
10//! signal. `bynk dev`'s watch loop sees it on its next tick and stops its
11//! contexts the way it does when one of them exits: SIGTERM to each wrangler,
12//! then the sweep of what they leave behind ([`crate::sweep`]). The wranglers
13//! stay in `bynk dev`'s process group, because the sweep finds orphaned
14//! `workerd`s by that group and a Ctrl-C must still reach them all.
15//!
16//! The handler is one-shot (`SA_RESETHAND`): a second Ctrl-C while the
17//! contexts are stopping takes the default action and ends `bynk dev` at once.
18//!
19//! Unix only. On Windows each wrangler's job object holds its tree (#1762), and
20//! [`install`] does nothing.
21
22use std::sync::atomic::{AtomicI32, Ordering};
23
24/// The signal received since [`install`], or 0 for none.
25static RECEIVED: AtomicI32 = AtomicI32::new(0);
26
27/// Record SIGINT and SIGTERM instead of dying of them, for [`received`] to
28/// report. Children spawned afterwards are unaffected: `exec` resets a caught
29/// signal to its default action.
30pub fn install() {
31    #[cfg(unix)]
32    for sig in [libc::SIGINT, libc::SIGTERM] {
33        // SAFETY: `record` only stores to an atomic, which is
34        // async-signal-safe, and `action` is fully initialised before use.
35        unsafe {
36            let mut action: libc::sigaction = std::mem::zeroed();
37            action.sa_sigaction = record as extern "C" fn(libc::c_int) as libc::sighandler_t;
38            action.sa_flags = libc::SA_RESTART | libc::SA_RESETHAND;
39            libc::sigemptyset(&mut action.sa_mask);
40            if libc::sigaction(sig, &action, std::ptr::null_mut()) != 0 {
41                eprintln!(
42                    "bynk dev: could not install a handler for signal {sig}; stopping \
43                     `bynk dev` with it may leave its workerd processes running."
44                );
45            }
46        }
47    }
48}
49
50#[cfg(unix)]
51extern "C" fn record(sig: libc::c_int) {
52    RECEIVED.store(sig, Ordering::SeqCst);
53}
54
55/// The signal that asked `bynk dev` to stop, if one has arrived.
56pub fn received() -> Option<i32> {
57    match RECEIVED.load(Ordering::SeqCst) {
58        0 => None,
59        sig => Some(sig),
60    }
61}
62
63/// #2055: SIGINT and SIGTERM recorded rather than fatal for as long as the
64/// returned guard lives, then the dispositions in force before restored.
65///
66/// `bynk deploy` holds one while a first deploy's `wrangler deploy
67/// --secrets-file` runs, so a Ctrl-C (which the terminal sends to the whole
68/// foreground group, wrangler included) or a SIGTERM sent to `bynk` alone
69/// cannot end `bynk` before it removes the secrets file. A caught signal is
70/// reset to its default by `exec`, so wrangler itself still dies of them as it
71/// would have. Unlike [`install`], the handler is not one-shot: a second
72/// Ctrl-C must not end `bynk` with the file on disk either. Only SIGKILL can,
73/// and the next deploy's sweep removes what that leaves.
74///
75/// Clears any earlier recorded signal, so [`received`] reports only what
76/// arrived under this guard. On Windows it changes nothing.
77#[must_use = "the signals are restored when the guard drops"]
78pub(crate) fn defer() -> Deferred {
79    RECEIVED.store(0, Ordering::SeqCst);
80    #[cfg(unix)]
81    {
82        let mut previous: [Option<libc::sigaction>; 2] = [None, None];
83        for (slot, sig) in previous.iter_mut().zip([libc::SIGINT, libc::SIGTERM]) {
84            // SAFETY: `record` only stores to an atomic, which is
85            // async-signal-safe; both structs are fully initialised before use,
86            // and `old` is only kept when the call succeeded.
87            unsafe {
88                let mut action: libc::sigaction = std::mem::zeroed();
89                action.sa_sigaction = record as extern "C" fn(libc::c_int) as libc::sighandler_t;
90                action.sa_flags = libc::SA_RESTART;
91                libc::sigemptyset(&mut action.sa_mask);
92                let mut old: libc::sigaction = std::mem::zeroed();
93                if libc::sigaction(sig, &action, &mut old) == 0 {
94                    *slot = Some(old);
95                }
96            }
97        }
98        Deferred { previous }
99    }
100    #[cfg(not(unix))]
101    Deferred {}
102}
103
104/// The guard [`defer`] returns. Dropping it puts back the SIGINT and SIGTERM
105/// dispositions [`defer`] replaced.
106pub(crate) struct Deferred {
107    #[cfg(unix)]
108    previous: [Option<libc::sigaction>; 2],
109}
110
111impl Drop for Deferred {
112    fn drop(&mut self) {
113        #[cfg(unix)]
114        for (old, sig) in self.previous.iter().zip([libc::SIGINT, libc::SIGTERM]) {
115            if let Some(old) = old {
116                // SAFETY: `old` is the disposition `sigaction` returned.
117                unsafe {
118                    libc::sigaction(sig, old, std::ptr::null_mut());
119                }
120            }
121        }
122    }
123}
124
125/// Serialises the tests that change this process's signal dispositions or
126/// signal it, which would otherwise race in the shared test binary.
127#[cfg(test)]
128pub(crate) static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
129
130/// Undo [`install`] and forget any recorded signal, so a test that installs
131/// the handler leaves nothing behind in the shared test process.
132#[cfg(test)]
133pub(crate) fn reset() {
134    #[cfg(unix)]
135    for sig in [libc::SIGINT, libc::SIGTERM] {
136        // SAFETY: restoring the default disposition.
137        unsafe {
138            libc::signal(sig, libc::SIG_DFL);
139        }
140    }
141    RECEIVED.store(0, Ordering::SeqCst);
142}
143
144/// The exit byte for a session stopped by `sig`, as [`crate::shell::exit_status_byte`]
145/// maps a child's signal death: SIGINT is a clean stop (it is what Ctrl-C
146/// sends), anything else `128 + sig`, the shell convention.
147pub fn exit_byte(sig: i32) -> u8 {
148    // SIGINT is 2 on every Unix.
149    if sig == 2 {
150        0
151    } else {
152        128u8.saturating_add(sig.clamp(0, 127) as u8)
153    }
154}
155
156/// How to name `sig` in `bynk dev`'s stop notice.
157pub fn name(sig: i32) -> String {
158    match sig {
159        2 => "SIGINT".to_string(),
160        15 => "SIGTERM".to_string(),
161        n => format!("signal {n}"),
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    /// The handler `sig` currently has.
170    #[cfg(unix)]
171    fn disposition(sig: libc::c_int) -> libc::sighandler_t {
172        // SAFETY: a query; `current` is written by the call.
173        unsafe {
174            let mut current: libc::sigaction = std::mem::zeroed();
175            assert_eq!(libc::sigaction(sig, std::ptr::null(), &mut current), 0);
176            current.sa_sigaction
177        }
178    }
179
180    #[cfg(unix)]
181    #[test]
182    fn a_deferred_signal_is_recorded_and_the_old_disposition_comes_back() {
183        // #2055: what keeps `bynk deploy` alive to remove a secrets file.
184        let _lock = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
185        let before = [disposition(libc::SIGINT), disposition(libc::SIGTERM)];
186        {
187            let _deferred = defer();
188            assert_eq!(received(), None, "nothing carried over");
189            // Without the handler this would end the test binary.
190            // SAFETY: signalling this process, whose handler only records.
191            unsafe {
192                libc::raise(libc::SIGTERM);
193                libc::raise(libc::SIGINT);
194            }
195            assert!(received().is_some(), "the signals were recorded");
196            // SAFETY: as above; the handler is not one-shot.
197            unsafe {
198                libc::raise(libc::SIGINT);
199            }
200        }
201        assert_eq!(
202            [disposition(libc::SIGINT), disposition(libc::SIGTERM)],
203            before,
204            "the dispositions in force before are restored"
205        );
206        RECEIVED.store(0, Ordering::SeqCst);
207    }
208
209    #[test]
210    fn a_signal_stop_exits_like_a_signalled_child() {
211        assert_eq!(exit_byte(2), 0, "Ctrl-C is a clean stop");
212        assert_eq!(exit_byte(15), 143);
213    }
214}