Skip to main content

autom8/
signal.rs

1//! Signal handling infrastructure for graceful shutdown.
2//!
3//! This module provides a thread-safe mechanism for handling SIGINT (Ctrl+C)
4//! signals, allowing the main loop to check for shutdown requests without
5//! blocking.
6
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9
10use crate::error::{Autom8Error, Result};
11
12/// Handles SIGINT signals for graceful shutdown.
13///
14/// `SignalHandler` registers a handler for SIGINT that sets an internal flag
15/// when triggered. The main loop can check this flag using `is_shutdown_requested()`
16/// without blocking.
17///
18/// # Thread Safety
19///
20/// `SignalHandler` is thread-safe and can be cloned to share across threads.
21/// The underlying shutdown flag uses atomic operations.
22///
23/// # Example
24///
25/// ```ignore
26/// let handler = SignalHandler::new()?;
27///
28/// // In main loop
29/// loop {
30///     if handler.is_shutdown_requested() {
31///         // Clean up and exit
32///         break;
33///     }
34///     // Continue processing
35/// }
36/// ```
37#[derive(Clone)]
38pub struct SignalHandler {
39    shutdown_flag: Arc<AtomicBool>,
40}
41
42impl SignalHandler {
43    /// Creates a new `SignalHandler` and registers the SIGINT handler.
44    ///
45    /// The handler will set an internal flag when SIGINT is received (e.g., when
46    /// the user presses Ctrl+C). This flag can be checked using `is_shutdown_requested()`.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the signal handler cannot be registered.
51    pub fn new() -> Result<Self> {
52        let shutdown_flag = Arc::new(AtomicBool::new(false));
53        let flag_clone = Arc::clone(&shutdown_flag);
54
55        ctrlc::set_handler(move || {
56            flag_clone.store(true, Ordering::SeqCst);
57        })
58        .map_err(|e| Autom8Error::SignalHandler(e.to_string()))?;
59
60        Ok(Self { shutdown_flag })
61    }
62
63    /// Checks if a shutdown has been requested (non-blocking).
64    ///
65    /// Returns `true` if SIGINT has been received since the handler was created,
66    /// `false` otherwise.
67    ///
68    /// This method is safe to call from any thread and does not block.
69    pub fn is_shutdown_requested(&self) -> bool {
70        self.shutdown_flag.load(Ordering::SeqCst)
71    }
72
73    /// Resets the shutdown flag to false.
74    ///
75    /// This can be useful for testing or if you need to clear the flag after
76    /// handling a shutdown request.
77    #[cfg(test)]
78    pub fn reset(&self) {
79        self.shutdown_flag.store(false, Ordering::SeqCst);
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_handler_can_be_created() {
89        // Note: In a real test environment, we can only register the handler once.
90        // Subsequent calls will fail, but that's expected behavior for ctrlc.
91        // We test the initial state instead.
92        let shutdown_flag = Arc::new(AtomicBool::new(false));
93        let handler = SignalHandler {
94            shutdown_flag: shutdown_flag.clone(),
95        };
96
97        // Handler should start with shutdown not requested
98        assert!(!handler.is_shutdown_requested());
99    }
100
101    #[test]
102    fn test_is_shutdown_requested_returns_false_initially() {
103        let shutdown_flag = Arc::new(AtomicBool::new(false));
104        let handler = SignalHandler { shutdown_flag };
105
106        assert!(!handler.is_shutdown_requested());
107    }
108
109    #[test]
110    fn test_is_shutdown_requested_returns_true_when_flag_set() {
111        let shutdown_flag = Arc::new(AtomicBool::new(false));
112        let handler = SignalHandler {
113            shutdown_flag: shutdown_flag.clone(),
114        };
115
116        // Simulate signal being received
117        shutdown_flag.store(true, Ordering::SeqCst);
118
119        assert!(handler.is_shutdown_requested());
120    }
121
122    #[test]
123    fn test_handler_is_thread_safe() {
124        let shutdown_flag = Arc::new(AtomicBool::new(false));
125        let handler = SignalHandler {
126            shutdown_flag: shutdown_flag.clone(),
127        };
128
129        // Clone handler for use in another thread
130        let handler_clone = handler.clone();
131
132        // Set flag from main thread
133        shutdown_flag.store(true, Ordering::SeqCst);
134
135        // Clone should see the same state
136        assert!(handler_clone.is_shutdown_requested());
137        assert!(handler.is_shutdown_requested());
138    }
139
140    #[test]
141    fn test_handler_clone_shares_state() {
142        let shutdown_flag = Arc::new(AtomicBool::new(false));
143        let handler1 = SignalHandler {
144            shutdown_flag: shutdown_flag.clone(),
145        };
146        let handler2 = handler1.clone();
147
148        // Both should see initial state
149        assert!(!handler1.is_shutdown_requested());
150        assert!(!handler2.is_shutdown_requested());
151
152        // Set via the underlying flag
153        shutdown_flag.store(true, Ordering::SeqCst);
154
155        // Both clones should see updated state
156        assert!(handler1.is_shutdown_requested());
157        assert!(handler2.is_shutdown_requested());
158    }
159
160    #[test]
161    fn test_reset_clears_shutdown_flag() {
162        let shutdown_flag = Arc::new(AtomicBool::new(true));
163        let handler = SignalHandler { shutdown_flag };
164
165        assert!(handler.is_shutdown_requested());
166
167        handler.reset();
168
169        assert!(!handler.is_shutdown_requested());
170    }
171}