Skip to main content

agentd/
signals.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! Signal handling + the self-pipe wakeup.
3//!
4//! Handlers are async-signal-safe — they only touch atomics and `write()` one
5//! byte to a **self-pipe** so a blocked reactor wakes promptly (`SA_RESTART`
6//! is deliberately off, so blocked syscalls also return `EINTR`). The reactor
7//! selects on `wakeup_fd()` alongside its channels; on wake it checks the
8//! flags and drains the pipe.
9//!
10//! - `SIGTERM`/`SIGINT` → one-way `DRAINING` (a second sets `FORCE`).
11//! - `SIGCHLD` → set the child-exit flag (the reactor runs `reap::reap_pending`).
12//! - `SIGPIPE` → ignored, so the supervisor never dies writing to a dead child.
13
14#[cfg(unix)]
15mod imp {
16    use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
17
18    static DRAINING: AtomicBool = AtomicBool::new(false);
19    static FORCE: AtomicBool = AtomicBool::new(false);
20    static CHILD_EXIT: AtomicBool = AtomicBool::new(false);
21    // Hot-reload request latch. The SIGHUP handler sets it and wakes the
22    // reactor; the reactive supervisor consults `reload_requested()` on its next
23    // tick (after `health::tick()`, like `draining()`) and runs the
24    // validate-first/quiesce/apply choreography, then `clear_reload()`s it. A
25    // SIGHUP while DRAINING is ignored (drain wins — checked at the consult site),
26    // so this latch can be set-but-never-honoured during a drain, which is fine:
27    // the process is exiting. The handler is registered ONLY under the
28    // `hot-reload` feature; without it SIGHUP keeps its default disposition.
29    static RELOAD: AtomicBool = AtomicBool::new(false);
30    // Trigger-attribution latch for the `config.reload_requested` event, whose
31    // `trigger` field is either "sighup" or "watch". The inotify file-watch thread
32    // sets BOTH `RELOAD` and this flag via `request_reload_from_watch()`; the
33    // reactive apply step reads-and-clears it with `take_reload_was_watch()` to
34    // pick the trigger string, DEFAULTING to
35    // "sighup" when unset (the SIGHUP handler / `request_reload()` never set it).
36    // The watcher is a normal thread (not a signal handler), so a plain atomic
37    // store is fine — no async-signal-safety constraint here.
38    static RELOAD_FROM_WATCH: AtomicBool = AtomicBool::new(false);
39    // Reload-in-progress guard: true only while the reactive supervisor is
40    // APPLYING a validated reload's diff, so a reader can tell a mid-apply moment
41    // (where config values may be half old, half new) from a settled one. Unlike
42    // `DRAINING` it is transient — the apply step brackets itself and always
43    // clears it, so a reader that refuses work while it is set cannot wedge.
44    // Like PAUSED/LAME_DUCK it rides here rather than in a feature-gated module,
45    // so any feature can read it without depending on another; only the
46    // `hot-reload` apply step ever sets it.
47    static RELOADING: AtomicBool = AtomicBool::new(false);
48    // Lame-duck override: forces readiness toward NotReady without exiting —
49    // set when a drain begins so a load balancer stops sending new work while the
50    // tree winds down, and clearable again. NOT a signal. It rides here rather
51    // than in a feature-gated module so both the `/readyz` probe (obs::serve,
52    // `metrics`) and the served control surface (mcp::server, `a2a`) read one
53    // process-global truth without either feature depending on the other.
54    // Distinct from `DRAINING`: lame-duck never exits.
55    static LAME_DUCK: AtomicBool = AtomicBool::new(false);
56    // Tree-wide pause state. Like `LAME_DUCK`, it rides here rather than in a
57    // feature-gated module so any feature can read one process-global truth
58    // without depending on another. Distinct from DRAINING/LAME_DUCK: pause
59    // freezes the agentic loops only — it never exits and never touches
60    // readiness, so the supervisor reactor and the liveness heartbeat keep
61    // running and a paused instance still answers probes.
62    static PAUSED: AtomicBool = AtomicBool::new(false);
63    // Intelligence all-endpoints-down latch. The model loop runs in a re-exec'd
64    // CHILD process that owns its own intel client + circuit-breaker / failover
65    // state; the supervisor has NO LLM and no live view of that breaker
66    // state. The child therefore reports its reachability UPWARD (an edge-triggered
67    // `AgentMsg::IntelHealth` at the breaker/failover seam — on entering all-down
68    // and on recovering); the supervisor latches it HERE so the readiness probe,
69    // the `agentd_intel_all_down` gauge, and the `agentd://intelligence`/`capacity`
70    // bodies all read ONE truth without a feature dependency (it rides here, not in
71    // a feature-gated module, exactly like LAME_DUCK/PAUSED).
72    //
73    // SEMANTICS (be honest): this is EVENTUALLY-CONSISTENT, last-child-experience.
74    // A fresh subagent spawn starts with FRESH breakers (all CLOSED), so the latched
75    // flag reflects the MOST RECENT child's intel reachability and persists between
76    // reactions — it is the right "should the fleet route work to this pod" signal,
77    // but it is NOT a continuous supervisor-side probe of the endpoints. There is no
78    // model loop in the supervisor to probe with; the truth comes from whichever
79    // child last exercised the endpoints. Distinct from DRAINING/LAME_DUCK (which an
80    // operator/SIGTERM set): this is set by the data path (a child's failover).
81    static INTEL_ALL_DOWN: AtomicBool = AtomicBool::new(false);
82    // Self-pipe fds (-1 until install()). The write end is touched from signal
83    // handlers; the read end is what the reactor waits on.
84    static WAKE_R: AtomicI32 = AtomicI32::new(-1);
85    static WAKE_W: AtomicI32 = AtomicI32::new(-1);
86
87    /// Async-signal-safe: write one byte to the self-pipe. A full/again pipe is
88    /// fine — the reactor only needs *a* readable byte to wake.
89    fn wake() {
90        let w = WAKE_W.load(Ordering::Relaxed);
91        if w >= 0 {
92            let b = [0u8; 1];
93            unsafe {
94                libc::write(w, b.as_ptr() as *const libc::c_void, 1);
95            }
96        }
97    }
98
99    extern "C" fn on_term(_sig: libc::c_int) {
100        if DRAINING.swap(true, Ordering::SeqCst) {
101            FORCE.store(true, Ordering::SeqCst);
102        }
103        wake();
104    }
105
106    extern "C" fn on_chld(_sig: libc::c_int) {
107        CHILD_EXIT.store(true, Ordering::SeqCst);
108        wake();
109    }
110
111    /// Async-signal-safe SIGHUP handler: set the RELOAD latch + wake the reactor.
112    /// Exactly the SIGTERM pattern (one atomic store + one self-pipe byte); the
113    /// heavy lifting (re-load, validate, apply) runs on the reactor thread, never
114    /// here — none of it is async-signal-safe. Registered only under the
115    /// `hot-reload` feature.
116    #[cfg(feature = "hot-reload")]
117    extern "C" fn on_hup(_sig: libc::c_int) {
118        RELOAD.store(true, Ordering::SeqCst);
119        wake();
120    }
121
122    fn set_handler(sig: libc::c_int, handler: libc::sighandler_t, flags: libc::c_int) {
123        unsafe {
124            let mut sa: libc::sigaction = std::mem::zeroed();
125            sa.sa_sigaction = handler;
126            libc::sigemptyset(&mut sa.sa_mask);
127            sa.sa_flags = flags; // never SA_RESTART
128            libc::sigaction(sig, &sa, std::ptr::null_mut());
129        }
130    }
131
132    fn make_self_pipe() {
133        if WAKE_R.load(Ordering::SeqCst) >= 0 {
134            return; // already created
135        }
136        let mut fds = [0 as libc::c_int; 2];
137        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
138            return;
139        }
140        for &fd in &fds {
141            unsafe {
142                let fl = libc::fcntl(fd, libc::F_GETFL);
143                libc::fcntl(fd, libc::F_SETFL, fl | libc::O_NONBLOCK);
144                let fdfl = libc::fcntl(fd, libc::F_GETFD);
145                libc::fcntl(fd, libc::F_SETFD, fdfl | libc::FD_CLOEXEC);
146            }
147        }
148        WAKE_R.store(fds[0], Ordering::SeqCst);
149        WAKE_W.store(fds[1], Ordering::SeqCst);
150    }
151
152    pub fn install() {
153        make_self_pipe();
154        let term = on_term as extern "C" fn(libc::c_int) as libc::sighandler_t;
155        let chld = on_chld as extern "C" fn(libc::c_int) as libc::sighandler_t;
156        set_handler(libc::SIGTERM, term, 0);
157        set_handler(libc::SIGINT, term, 0);
158        // SA_NOCLDSTOP: only fire on child *termination*, not stop/continue.
159        set_handler(libc::SIGCHLD, chld, libc::SA_NOCLDSTOP);
160        set_handler(libc::SIGPIPE, libc::SIG_IGN, 0);
161        // SIGHUP -> hot reload, only when the feature is built. Without it
162        // SIGHUP keeps its default disposition (terminate), so a build that
163        // cannot reload never swallows the signal and looks wedged instead.
164        #[cfg(feature = "hot-reload")]
165        {
166            let hup = on_hup as extern "C" fn(libc::c_int) as libc::sighandler_t;
167            set_handler(libc::SIGHUP, hup, 0);
168        }
169    }
170
171    pub fn draining() -> bool {
172        DRAINING.load(Ordering::SeqCst)
173    }
174    pub fn force() -> bool {
175        FORCE.load(Ordering::SeqCst)
176    }
177
178    /// Programmatically request a graceful drain — the SAME one-way latch
179    /// SIGTERM sets, plus a wakeup so a blocked reactor begins the drain
180    /// choreography promptly. Idempotent and monotonic: a request after drain has
181    /// begun is a no-op that never escalates to FORCE. Only a *second* signal
182    /// escalates, so a programmatic call can never cut a drain short.
183    pub fn request_drain() {
184        DRAINING.store(true, Ordering::SeqCst);
185        // Reuse the signal-handler wakeup so the reactor leaves its blocking
186        // select and runs the drain state machine.
187        wake();
188    }
189
190    pub fn lame_duck() -> bool {
191        LAME_DUCK.load(Ordering::SeqCst)
192    }
193
194    /// Set/clear the lame-duck readiness override. `true` forces `/readyz`
195    /// NotReady while the supervisor keeps running; `false` clears the override
196    /// (readiness then reflects the genuine computed state). No drain, no exit,
197    /// reversible.
198    pub fn set_lame_duck(on: bool) {
199        LAME_DUCK.store(on, Ordering::SeqCst);
200    }
201
202    pub fn paused() -> bool {
203        PAUSED.load(Ordering::SeqCst)
204    }
205
206    /// Set/clear the instance-wide pause state. Reporting-only: the per-session
207    /// pause channels do the actual loop suspension, so clearing this flag alone
208    /// does not resume anything. Reversible; never exits, never touches
209    /// readiness.
210    pub fn set_paused(on: bool) {
211        PAUSED.store(on, Ordering::SeqCst);
212    }
213
214    pub fn intel_all_down() -> bool {
215        INTEL_ALL_DOWN.load(Ordering::SeqCst)
216    }
217
218    /// Latch the intelligence all-endpoints-down state from a child's upward
219    /// `AgentMsg::IntelHealth` report. Returns `true` iff the value
220    /// TRANSITIONED (so the supervisor fires the `agentd://intelligence`
221    /// notify-then-read exactly on a breaker enter/exit, not on every report).
222    /// Eventually-consistent / last-child-experience — see the static's doc above.
223    pub fn set_intel_all_down(on: bool) -> bool {
224        INTEL_ALL_DOWN.swap(on, Ordering::SeqCst) != on
225    }
226
227    /// Take and clear the SIGCHLD flag — the reactor then runs the waitpid loop.
228    pub fn take_child_exit() -> bool {
229        CHILD_EXIT.swap(false, Ordering::SeqCst)
230    }
231
232    /// Has a hot reload been requested (SIGHUP)? Read by the reactive
233    /// supervisor's tick; cleared with `clear_reload()` once the reload
234    /// routine has run (whether it applied or was rejected — both consume the
235    /// request). Always readable, but only ever SET under the `hot-reload`
236    /// feature (the handler is the only setter besides `request_reload`).
237    pub fn reload_requested() -> bool {
238        RELOAD.load(Ordering::SeqCst)
239    }
240
241    /// Clear the hot-reload latch (after the reload routine has run, or when a
242    /// drain supersedes it). Idempotent.
243    pub fn clear_reload() {
244        RELOAD.store(false, Ordering::SeqCst);
245    }
246
247    /// Programmatically request a hot reload (parity with `request_drain` — for
248    /// a future `reload` operator tool / tests), plus a reactor wakeup. Honoured
249    /// only by a `hot-reload` build's reactive loop; a no-feature build never
250    /// consults the latch, so this is inert there.
251    pub fn request_reload() {
252        RELOAD.store(true, Ordering::SeqCst);
253        wake();
254    }
255
256    /// Request a hot reload attributed to the file-watch trigger: set the SAME
257    /// RELOAD latch SIGHUP/`request_reload` do, PLUS the watch-attribution flag
258    /// the apply step reads to emit `config.reload_requested{trigger:"watch"}`.
259    /// Called by the inotify watcher thread; a reactor wakeup follows. Inert on a
260    /// build without the reactive reload loop.
261    pub fn request_reload_from_watch() {
262        RELOAD_FROM_WATCH.store(true, Ordering::SeqCst);
263        RELOAD.store(true, Ordering::SeqCst);
264        wake();
265    }
266
267    /// Take-and-clear the watch-attribution flag: `true` if the pending reload was
268    /// set by the file-watch trigger, `false` (the default) for SIGHUP / a
269    /// programmatic `request_reload`. The apply step calls this once per reload to
270    /// pick the `config.reload_requested` `trigger` string.
271    pub fn take_reload_was_watch() -> bool {
272        RELOAD_FROM_WATCH.swap(false, Ordering::SeqCst)
273    }
274
275    /// Is a validated reload mid-apply? True only between the apply step's
276    /// `set_reloading(true)` and `(false)`, so a reader can refuse work that
277    /// would otherwise observe a half-applied config.
278    pub fn reloading() -> bool {
279        RELOADING.load(Ordering::SeqCst)
280    }
281
282    /// Set/clear the reload-in-progress guard (the reactive apply step brackets
283    /// its reloadable-diff application with `set_reloading(true)`/`(false)`).
284    pub fn set_reloading(on: bool) {
285        RELOADING.store(on, Ordering::SeqCst);
286    }
287
288    pub fn wakeup_fd() -> i32 {
289        WAKE_R.load(Ordering::SeqCst)
290    }
291
292    /// Drain all pending wakeup bytes (the pipe is edge-ish; we level it).
293    pub fn drain_wakeup() {
294        let r = WAKE_R.load(Ordering::SeqCst);
295        if r < 0 {
296            return;
297        }
298        let mut buf = [0u8; 64];
299        loop {
300            let n = unsafe { libc::read(r, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
301            if n <= 0 {
302                break; // EAGAIN (drained) or error
303            }
304        }
305    }
306
307    /// Test-only: clear the one-way `DRAINING`/`FORCE` latches (production has no
308    /// clear — drain is monotonic for a process's life). The signals test guard
309    /// uses this so a draining test cannot poison readiness for later tests that
310    /// share this process (cargo runs tests multithreaded in one binary).
311    #[cfg(test)]
312    pub fn clear_drain_for_test() {
313        DRAINING.store(false, Ordering::SeqCst);
314        FORCE.store(false, Ordering::SeqCst);
315    }
316}
317
318#[cfg(not(unix))]
319mod imp {
320    pub fn install() {}
321    #[cfg(test)]
322    pub fn clear_drain_for_test() {}
323    pub fn draining() -> bool {
324        false
325    }
326    pub fn force() -> bool {
327        false
328    }
329    pub fn request_drain() {}
330    pub fn lame_duck() -> bool {
331        false
332    }
333    pub fn set_lame_duck(_on: bool) {}
334    pub fn paused() -> bool {
335        false
336    }
337    pub fn set_paused(_on: bool) {}
338    pub fn intel_all_down() -> bool {
339        false
340    }
341    pub fn set_intel_all_down(_on: bool) -> bool {
342        false
343    }
344    pub fn take_child_exit() -> bool {
345        false
346    }
347    pub fn reload_requested() -> bool {
348        false
349    }
350    pub fn clear_reload() {}
351    pub fn request_reload() {}
352    pub fn request_reload_from_watch() {}
353    pub fn take_reload_was_watch() -> bool {
354        false
355    }
356    pub fn reloading() -> bool {
357        false
358    }
359    pub fn set_reloading(_on: bool) {}
360    pub fn wakeup_fd() -> i32 {
361        -1
362    }
363    pub fn drain_wakeup() {}
364}
365
366/// Install SIGTERM/SIGINT/SIGCHLD/SIGPIPE handlers + the self-pipe. Call once
367/// at supervisor startup.
368pub fn install() {
369    imp::install();
370}
371
372/// Has a graceful drain been requested (first SIGTERM/SIGINT)?
373pub fn draining() -> bool {
374    imp::draining()
375}
376
377/// Has a forced shutdown been requested (second SIGTERM/SIGINT)?
378pub fn force() -> bool {
379    imp::force()
380}
381
382/// Request a graceful drain programmatically — the same one-way `DRAINING` latch
383/// SIGTERM sets, plus a reactor wakeup. Idempotent and monotonic; never
384/// escalates to FORCE (only a second signal does), so a caller cannot cut an
385/// in-flight drain short.
386pub fn request_drain() {
387    imp::request_drain()
388}
389
390/// Is the lame-duck readiness override active? When true,
391/// `/readyz` reports NotReady even though the supervisor keeps running.
392pub fn lame_duck() -> bool {
393    imp::lame_duck()
394}
395
396/// Set or clear the lame-duck readiness override. `true` overrides readiness
397/// toward NotReady; `false` clears it. The reactor sets it when a drain begins,
398/// so traffic stops arriving while the tree winds down.
399pub fn set_lame_duck(on: bool) {
400    imp::set_lame_duck(on)
401}
402
403/// Is the instance-wide pause active? When true, the agentic
404/// loops are suspended at their turn boundaries; the supervisor and readiness
405/// are unaffected.
406pub fn paused() -> bool {
407    imp::paused()
408}
409
410/// Set or clear the instance-wide pause state. Reporting-only — the per-session
411/// pause channels perform the actual suspension.
412pub fn set_paused(on: bool) {
413    imp::set_paused(on)
414}
415
416/// Is the intelligence channel all-endpoints-down? The latched,
417/// EVENTUALLY-CONSISTENT last-child-experience truth a child reports up via
418/// `AgentMsg::IntelHealth` — read by `/readyz` (flips NotReady), the
419/// `agentd_intel_all_down` gauge, and the `agentd://intelligence`/`capacity`
420/// bodies. NOT a live supervisor-side probe (there is no model loop in the
421/// supervisor): it reflects whichever child last exercised the endpoints.
422pub fn intel_all_down() -> bool {
423    imp::intel_all_down()
424}
425
426/// Latch the intelligence all-endpoints-down state from a child's `AgentMsg::
427/// IntelHealth` report. Returns `true` iff the value TRANSITIONED,
428/// so the supervisor can fire the `agentd://intelligence` notify exactly on a
429/// breaker enter/exit. Eventually-consistent / last-child-experience: a fresh
430/// spawn has fresh breakers, so this reflects the most recent child's reachability
431/// and persists between reactions — the right "route work here?" signal, not a
432/// continuous probe.
433pub fn set_intel_all_down(on: bool) -> bool {
434    imp::set_intel_all_down(on)
435}
436
437/// Take-and-clear the SIGCHLD flag — true if a child exited since last checked.
438pub fn take_child_exit() -> bool {
439    imp::take_child_exit()
440}
441
442/// Has a hot reload been requested (SIGHUP)? The reactive supervisor consults
443/// this each tick; a drain supersedes it (the caller checks
444/// `draining()` first). Always `false` on a build without the `hot-reload`
445/// feature (the handler that sets it is feature-gated).
446pub fn reload_requested() -> bool {
447    imp::reload_requested()
448}
449
450/// Clear the hot-reload latch once the reload routine has run (applied or
451/// rejected), or when a drain supersedes the request. Idempotent.
452pub fn clear_reload() {
453    imp::clear_reload()
454}
455
456/// Programmatically request a hot reload (the same RELOAD latch SIGHUP sets) +
457/// a reactor wakeup. Parity with `request_drain`; honoured only by a
458/// `hot-reload` build's reactive loop.
459pub fn request_reload() {
460    imp::request_reload()
461}
462
463/// Request a hot reload attributed to the **file-watch** trigger: the same
464/// RELOAD latch SIGHUP/`request_reload` set, plus the watch-attribution flag the
465/// apply step reads to emit `config.reload_requested{trigger:"watch"}`. Called by
466/// the inotify watcher thread (`config-watch`).
467pub fn request_reload_from_watch() {
468    imp::request_reload_from_watch()
469}
470
471/// Take-and-clear the watch-attribution flag — `true` if the pending reload came
472/// from the file-watch trigger, `false` (the default) for SIGHUP or a
473/// programmatic `request_reload`. The reactive apply step calls this once per
474/// reload to label the `config.reload_requested` `trigger`.
475pub fn take_reload_was_watch() -> bool {
476    imp::take_reload_was_watch()
477}
478
479/// Is a validated reload mid-apply? True only while the reloadable diff is being
480/// written into the live runtime, so a reader can refuse work that would
481/// otherwise observe a half-applied config. Always `false` off the `hot-reload`
482/// path (only the reactive apply step ever sets it).
483pub fn reloading() -> bool {
484    imp::reloading()
485}
486
487/// Set or clear the reload-in-progress guard. The reactive apply step brackets
488/// its reloadable-diff application with `set_reloading(true)` then `(false)`.
489pub fn set_reloading(on: bool) {
490    imp::set_reloading(on)
491}
492
493/// The read end of the self-pipe — the reactor waits on it for prompt wakeups.
494/// Returns -1 before `install()` (or on non-Unix).
495pub fn wakeup_fd() -> i32 {
496    imp::wakeup_fd()
497}
498
499/// Drain pending wakeup bytes after a wake.
500pub fn drain_wakeup() {
501    imp::drain_wakeup()
502}
503
504// ── Test isolation for the process-global signal state ──────────────────────
505// `DRAINING` is a one-way latch and `PAUSED`/`LAME_DUCK`/`RELOADING`/
506// `INTEL_ALL_DOWN` are process-global, so tests that touch them race and poison
507// each other when cargo runs them in parallel within one test binary (e.g. a
508// drain test leaves `DRAINING` set, breaking every later readiness assertion).
509// Every test that reads OR writes this state takes `test_guard()`: it serializes
510// them on one mutex and resets the state to a clean slate for the test body.
511#[cfg(test)]
512static SIGNALS_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
513
514/// Reset every process-global signal latch to its initial (unset) state.
515/// Test-only; called under the [`test_guard`] lock.
516#[cfg(test)]
517pub fn reset_for_test() {
518    imp::clear_drain_for_test();
519    set_lame_duck(false);
520    set_paused(false);
521    set_reloading(false);
522    clear_reload();
523    // The intel all-down latch is process-global too (set by a child's IntelHealth
524    // report); clear it so an all-down readiness/gauge test cannot poison a later
525    // readiness test sharing this process.
526    let _ = set_intel_all_down(false);
527    // Clear the watch-attribution latch too (set by `request_reload_from_watch`),
528    // so a watcher test cannot leak `trigger:"watch"` into a later reload test.
529    let _ = take_reload_was_watch();
530}
531
532/// RAII guard from [`test_guard`]. Resets the signal state on BOTH acquire and
533/// drop — the drop reset runs while the mutex is still held (the inner
534/// `MutexGuard` field drops after this `Drop::drop`), so a test that latches
535/// `DRAINING` cannot leak it to the next test between lock-release and the next
536/// acquire's reset.
537#[cfg(test)]
538pub struct SignalsTestGuard(#[allow(dead_code)] std::sync::MutexGuard<'static, ()>);
539
540#[cfg(test)]
541impl Drop for SignalsTestGuard {
542    fn drop(&mut self) {
543        reset_for_test();
544    }
545}
546
547/// Serialize + clean-slate a test that touches the process-global signal state.
548/// `let _g = crate::signals::test_guard();` at the top of the test, held for the
549/// whole body, so no other signals-touching test interleaves. State is reset on
550/// entry AND on drop (under the lock), so nothing leaks across tests. Recovers a
551/// poisoned lock (a panicking test should not wedge the rest of the suite).
552#[cfg(test)]
553pub fn test_guard() -> SignalsTestGuard {
554    let g = SIGNALS_TEST_LOCK
555        .lock()
556        .unwrap_or_else(|poison| poison.into_inner());
557    reset_for_test();
558    SignalsTestGuard(g)
559}