Skip to main content

dcp/shutdown/
signals.rs

1//! Signal handling for graceful shutdown.
2//!
3//! Provides cross-platform signal handling for SIGTERM, SIGINT, and SIGHUP.
4
5use std::sync::Arc;
6use tokio::sync::broadcast;
7
8use super::ShutdownCoordinator;
9
10/// Signal types that can be handled
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Signal {
13    /// SIGTERM - graceful shutdown
14    Term,
15    /// SIGINT - interrupt (Ctrl+C)
16    Int,
17    /// SIGHUP - reload configuration
18    Hup,
19}
20
21/// Signal handler configuration
22pub struct SignalHandler {
23    /// Shutdown coordinator
24    coordinator: Arc<ShutdownCoordinator>,
25    /// Config reload channel
26    reload_tx: broadcast::Sender<()>,
27}
28
29impl SignalHandler {
30    /// Create a new signal handler
31    pub fn new(coordinator: Arc<ShutdownCoordinator>) -> Self {
32        let (reload_tx, _) = broadcast::channel(16);
33        Self {
34            coordinator,
35            reload_tx,
36        }
37    }
38
39    /// Subscribe to config reload notifications
40    pub fn subscribe_reload(&self) -> broadcast::Receiver<()> {
41        self.reload_tx.subscribe()
42    }
43
44    /// Trigger a config reload
45    pub fn trigger_reload(&self) {
46        let _ = self.reload_tx.send(());
47    }
48
49    /// Get the shutdown coordinator
50    pub fn coordinator(&self) -> &Arc<ShutdownCoordinator> {
51        &self.coordinator
52    }
53
54    /// Setup signal handlers (Unix)
55    #[cfg(unix)]
56    pub async fn setup_handlers(self: Arc<Self>) {
57        use tokio::signal::unix::{signal, SignalKind};
58
59        let handler = Arc::clone(&self);
60
61        // SIGTERM handler
62        let term_handler = Arc::clone(&handler);
63        tokio::spawn(async move {
64            let mut sigterm =
65                signal(SignalKind::terminate()).expect("Failed to setup SIGTERM handler");
66            loop {
67                sigterm.recv().await;
68                log_signal(Signal::Term);
69                term_handler.coordinator.shutdown();
70            }
71        });
72
73        // SIGINT handler (Ctrl+C)
74        let int_handler = Arc::clone(&handler);
75        tokio::spawn(async move {
76            let mut sigint =
77                signal(SignalKind::interrupt()).expect("Failed to setup SIGINT handler");
78            loop {
79                sigint.recv().await;
80                log_signal(Signal::Int);
81                int_handler.coordinator.shutdown();
82            }
83        });
84
85        // SIGHUP handler (config reload)
86        let hup_handler = Arc::clone(&handler);
87        tokio::spawn(async move {
88            let mut sighup = signal(SignalKind::hangup()).expect("Failed to setup SIGHUP handler");
89            loop {
90                sighup.recv().await;
91                log_signal(Signal::Hup);
92                hup_handler.trigger_reload();
93            }
94        });
95    }
96
97    /// Setup signal handlers (Windows)
98    #[cfg(windows)]
99    pub async fn setup_handlers(self: Arc<Self>) {
100        use tokio::signal::ctrl_c;
101
102        let handler = Arc::clone(&self);
103
104        // Ctrl+C handler (equivalent to SIGINT)
105        tokio::spawn(async move {
106            loop {
107                if ctrl_c().await.is_ok() {
108                    log_signal(Signal::Int);
109                    handler.coordinator.shutdown();
110                }
111            }
112        });
113    }
114
115    /// Wait for shutdown signal
116    pub async fn wait_for_shutdown(&self) {
117        let mut rx = self.coordinator.subscribe();
118        let _ = rx.recv().await;
119    }
120}
121
122/// Log signal receipt
123fn log_signal(signal: Signal) {
124    match signal {
125        Signal::Term => eprintln!("Received SIGTERM, initiating graceful shutdown..."),
126        Signal::Int => eprintln!("Received SIGINT, initiating graceful shutdown..."),
127        Signal::Hup => eprintln!("Received SIGHUP, reloading configuration..."),
128    }
129}
130
131/// Setup default signal handlers
132pub async fn setup_default_handlers(coordinator: Arc<ShutdownCoordinator>) -> Arc<SignalHandler> {
133    let handler = Arc::new(SignalHandler::new(coordinator));
134    handler.clone().setup_handlers().await;
135    handler
136}
137
138/// Convenience function to wait for Ctrl+C
139pub async fn wait_for_ctrl_c() {
140    #[cfg(unix)]
141    {
142        use tokio::signal::unix::{signal, SignalKind};
143        let mut sigint = signal(SignalKind::interrupt()).expect("Failed to setup SIGINT handler");
144        let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM handler");
145
146        tokio::select! {
147            _ = sigint.recv() => {
148                log_signal(Signal::Int);
149            }
150            _ = sigterm.recv() => {
151                log_signal(Signal::Term);
152            }
153        }
154    }
155
156    #[cfg(windows)]
157    {
158        use tokio::signal::ctrl_c;
159        let _ = ctrl_c().await;
160        log_signal(Signal::Int);
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::time::Duration;
168
169    #[tokio::test]
170    async fn test_signal_handler_creation() {
171        let coord = Arc::new(ShutdownCoordinator::default());
172        let handler = SignalHandler::new(coord);
173
174        assert!(!handler.coordinator().is_shutdown());
175    }
176
177    #[tokio::test]
178    async fn test_reload_subscription() {
179        let coord = Arc::new(ShutdownCoordinator::default());
180        let handler = SignalHandler::new(coord);
181
182        let mut rx = handler.subscribe_reload();
183
184        // Trigger reload
185        handler.trigger_reload();
186
187        // Should receive notification
188        let result = tokio::time::timeout(Duration::from_millis(100), rx.recv()).await;
189        assert!(result.is_ok());
190    }
191
192    #[tokio::test]
193    async fn test_wait_for_shutdown() {
194        let coord = Arc::new(ShutdownCoordinator::default());
195        let handler = Arc::new(SignalHandler::new(Arc::clone(&coord)));
196
197        let handler_clone = Arc::clone(&handler);
198
199        // Spawn task to trigger shutdown
200        tokio::spawn(async move {
201            tokio::time::sleep(Duration::from_millis(10)).await;
202            handler_clone.coordinator().shutdown();
203        });
204
205        // Wait for shutdown
206        let result =
207            tokio::time::timeout(Duration::from_millis(100), handler.wait_for_shutdown()).await;
208        assert!(result.is_ok());
209    }
210}