nexus_rt/shutdown.rs
1//! Cooperative shutdown for event loops.
2//!
3//! [`Shutdown`] is a handler parameter that accesses the world's shutdown
4//! flag directly — no resource registration needed. Handlers trigger
5//! shutdown via [`Shutdown::trigger`]:
6//!
7//! ```
8//! use nexus_rt::{WorldBuilder, IntoHandler, Handler};
9//! use nexus_rt::shutdown::Shutdown;
10//!
11//! fn on_fatal(shutdown: Shutdown, _event: ()) {
12//! shutdown.trigger();
13//! }
14//!
15//! let mut world = WorldBuilder::new().build();
16//! let mut handler = on_fatal.into_handler(world.registry());
17//! handler.run(&mut world, ());
18//! assert!(world.shutdown_handle().is_shutdown());
19//! ```
20//!
21//! The event loop owns a [`ShutdownHandle`] obtained from
22//! [`World::shutdown_handle`](crate::World::shutdown_handle) and checks it
23//! each iteration:
24//!
25//! ```
26//! use nexus_rt::WorldBuilder;
27//!
28//! let mut world = WorldBuilder::new().build();
29//! let shutdown = world.shutdown_handle();
30//!
31//! // typical event loop
32//! while !shutdown.is_shutdown() {
33//! // poll drivers ...
34//! # break;
35//! }
36//! ```
37//!
38//! # Signal Support (Linux only)
39//!
40//! With the `signals` feature, `ShutdownHandle::enable_signals` registers
41//! SIGINT and SIGTERM handlers that flip the shutdown flag automatically.
42//! Targets Linux infrastructure — not supported on Windows.
43
44use std::sync::Arc;
45use std::sync::atomic::{AtomicBool, Ordering};
46
47/// Handler parameter for cooperative shutdown.
48///
49/// Accesses the world's shutdown flag directly — not a resource.
50/// Uses [`Relaxed`](Ordering::Relaxed) ordering — the flag is checked
51/// once per poll iteration, not on a hot path requiring memory fencing.
52/// Holds a reference to World's `AtomicBool` shutdown flag.
53/// Lifetime-bound to the World borrow — cannot escape the dispatch frame.
54pub struct Shutdown<'w>(pub(crate) &'w AtomicBool);
55
56impl Shutdown<'_> {
57 /// Returns `true` if shutdown has been triggered.
58 #[inline(always)]
59 pub fn is_shutdown(&self) -> bool {
60 self.0.load(Ordering::Relaxed)
61 }
62
63 /// Trigger shutdown. The event loop will exit after the current
64 /// dispatch completes.
65 pub fn trigger(&self) {
66 self.0.store(true, Ordering::Relaxed);
67 }
68}
69
70impl std::fmt::Debug for Shutdown<'_> {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_tuple("Shutdown")
73 .field(&self.is_shutdown())
74 .finish()
75 }
76}
77
78/// External handle for the event loop to check shutdown status.
79///
80/// Shares the same [`AtomicBool`] as the world's shutdown flag.
81/// Obtained via [`World::shutdown_handle`](crate::World::shutdown_handle).
82pub struct ShutdownHandle {
83 flag: Arc<AtomicBool>,
84}
85
86impl ShutdownHandle {
87 pub(crate) fn new(flag: Arc<AtomicBool>) -> Self {
88 Self { flag }
89 }
90
91 /// Returns `true` if shutdown has been triggered.
92 pub fn is_shutdown(&self) -> bool {
93 self.flag.load(Ordering::Relaxed)
94 }
95
96 /// Trigger shutdown from outside the event loop.
97 pub fn shutdown(&self) {
98 self.flag.store(true, Ordering::Relaxed);
99 }
100
101 /// Register SIGINT and SIGTERM handlers that trigger shutdown.
102 ///
103 /// Unix/Linux only. Uses [`signal_hook::flag::register`] — the signal
104 /// handler simply flips the shared [`AtomicBool`]. Safe to call
105 /// multiple times (subsequent calls are no-ops at the OS level for
106 /// the same signal).
107 ///
108 /// # Errors
109 ///
110 /// Returns an error if the OS rejects the signal registration.
111 ///
112 /// # Platform Support
113 ///
114 /// Targets Linux infrastructure. Not supported on Windows.
115 #[cfg(feature = "signals")]
116 pub fn enable_signals(&self) -> std::io::Result<()> {
117 signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&self.flag))?;
118 signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&self.flag))?;
119 Ok(())
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn handle_not_shutdown_by_default() {
129 let world = crate::WorldBuilder::new().build();
130 let handle = world.shutdown_handle();
131 assert!(!handle.is_shutdown());
132 }
133
134 #[test]
135 fn shutdown_param_triggers() {
136 let world = crate::WorldBuilder::new().build();
137 let handle = world.shutdown_handle();
138 let shutdown = Shutdown(world.shutdown_flag());
139
140 assert!(!handle.is_shutdown());
141 shutdown.trigger();
142 assert!(handle.is_shutdown());
143 }
144
145 #[test]
146 fn handle_can_trigger_shutdown() {
147 let world = crate::WorldBuilder::new().build();
148 let handle = world.shutdown_handle();
149 assert!(!handle.is_shutdown());
150 handle.shutdown();
151 assert!(handle.is_shutdown());
152 }
153
154 #[test]
155 fn shutdown_in_handler() {
156 use crate::{Handler, IntoHandler};
157
158 fn trigger_shutdown(shutdown: Shutdown, _event: ()) {
159 shutdown.trigger();
160 }
161
162 let mut world = crate::WorldBuilder::new().build();
163 let handle = world.shutdown_handle();
164
165 let mut handler = trigger_shutdown.into_handler(world.registry());
166 assert!(!handle.is_shutdown());
167 handler.run(&mut world, ());
168 assert!(handle.is_shutdown());
169 }
170}