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}