Skip to main content

proc_daemon/
signal.rs

1//! Cross-platform signal handling for graceful daemon shutdown.
2//!
3//! This module provides a unified interface for handling shutdown signals
4//! across different platforms, abstracting away the platform-specific
5//! differences between Unix signals and Windows console events.
6
7use std::sync::atomic::{AtomicBool, Ordering};
8#[allow(unused_imports)]
9use tracing::{debug, info, warn};
10
11use crate::ShutdownReason;
12
13// No need for these imports
14// use crate::config::Config;
15// use crate::daemon::Daemon;
16use crate::error::{Error, Result};
17use crate::shutdown::ShutdownCoordinator;
18
19/// Cross-platform signal handler that coordinates shutdown.
20#[derive(Debug)]
21pub struct SignalHandler {
22    #[allow(dead_code)]
23    shutdown_coordinator: ShutdownCoordinator,
24    handling_signals: AtomicBool,
25}
26
27impl SignalHandler {
28    /// Create a new signal handler.
29    #[must_use]
30    pub const fn new(shutdown_coordinator: ShutdownCoordinator) -> Self {
31        Self {
32            shutdown_coordinator,
33            handling_signals: AtomicBool::new(false),
34        }
35    }
36
37    /// Start handling signals for graceful shutdown.
38    /// This will register signal handlers and wait for shutdown signals.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if signal handling is already active or if there's a problem
43    /// registering signal handlers on the platform.
44    pub async fn handle_signals(&self) -> Result<()> {
45        if self
46            .handling_signals
47            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
48            .is_err()
49        {
50            return Err(Error::invalid_state("Signal handling already started"));
51        }
52
53        info!("Starting signal handler");
54
55        // Platform-specific signal handling
56        #[cfg(unix)]
57        {
58            return self.handle_unix_signals().await;
59        }
60
61        #[cfg(windows)]
62        {
63            return self.handle_windows_signals().await;
64        }
65    }
66
67    /// Stop signal handling.
68    pub fn stop(&self) {
69        self.handling_signals.store(false, Ordering::Release);
70        debug!("Signal handling stopped");
71    }
72
73    /// Check if signal handling is active.
74    pub fn is_handling(&self) -> bool {
75        self.handling_signals.load(Ordering::Acquire)
76    }
77}
78
79// Unix-specific signal handling implementation
80#[cfg(unix)]
81impl SignalHandler {
82    async fn handle_unix_signals(&self) -> Result<()> {
83        #[cfg(all(feature = "tokio", not(feature = "async-std")))]
84        {
85            return self.handle_unix_signals_tokio().await;
86        }
87
88        #[cfg(all(feature = "async-std", not(feature = "tokio")))]
89        {
90            return self.handle_unix_signals_async_std().await;
91        }
92
93        #[cfg(not(any(feature = "tokio", feature = "async-std")))]
94        {
95            return Err(Error::runtime_with_code(
96                crate::error::ErrorCode::MissingRuntime,
97                "No runtime available for signal handling",
98            ));
99        }
100
101        #[cfg(all(feature = "tokio", feature = "async-std"))]
102        {
103            // Default to tokio when both are present
104            return self.handle_unix_signals_tokio().await;
105        }
106    }
107
108    #[cfg(feature = "tokio")]
109    async fn handle_unix_signals_tokio(&self) -> Result<()> {
110        use tokio::signal::unix::{signal, SignalKind};
111
112        // Set up signal handlers for graceful shutdown
113        let mut sigterm = signal(SignalKind::terminate()).map_err(|e| {
114            Error::signal_with_number(format!("Failed to register SIGTERM handler: {e}"), 15)
115        })?;
116
117        let mut sigint = signal(SignalKind::interrupt()).map_err(|e| {
118            Error::signal_with_number(format!("Failed to register SIGINT handler: {e}"), 2)
119        })?;
120
121        let mut sigquit = signal(SignalKind::quit()).map_err(|e| {
122            Error::signal_with_number(format!("Failed to register SIGQUIT handler: {e}"), 3)
123        })?;
124
125        let mut sighup = signal(SignalKind::hangup()).map_err(|e| {
126            Error::signal_with_number(format!("Failed to register SIGHUP handler: {e}"), 1)
127        })?;
128
129        info!("Unix signal handlers registered (SIGTERM, SIGINT, SIGQUIT, SIGHUP)");
130
131        loop {
132            tokio::select! {
133                _ = sigterm.recv() => {
134                    info!("Received SIGTERM, initiating graceful shutdown");
135                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(15)) {
136                        break;
137                    }
138                }
139                _ = sigint.recv() => {
140                    info!("Received SIGINT (Ctrl+C), initiating graceful shutdown");
141                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
142                        break;
143                    }
144                }
145                _ = sigquit.recv() => {
146                    warn!("Received SIGQUIT, initiating immediate shutdown");
147                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
148                        break;
149                    }
150                }
151                _ = sighup.recv() => {
152                    info!("Received SIGHUP, could be used for config reload (initiating shutdown for now)");
153                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(1)) {
154                        break;
155                    }
156                }
157            }
158
159            // Check if we should stop handling signals
160            if !self.is_handling() {
161                debug!("Signal handling stopped by request");
162                break;
163            }
164        }
165
166        Ok(())
167    }
168
169    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
170    async fn handle_unix_signals_async_std(&self) -> Result<()> {
171        use futures::stream::StreamExt;
172        use signal_hook::consts::{SIGHUP, SIGINT, SIGQUIT, SIGTERM};
173        use signal_hook_async_std::Signals;
174
175        let mut signals = Signals::new([SIGTERM, SIGINT, SIGQUIT, SIGHUP])
176            .map_err(|e| Error::signal(format!("Failed to register Unix signal handlers: {e}")))?;
177
178        info!("Unix signal handlers registered (SIGTERM, SIGINT, SIGQUIT, SIGHUP)");
179
180        while self.is_handling() {
181            if let Some(signal) = signals.next().await {
182                match signal {
183                    SIGTERM => {
184                        info!("Received SIGTERM, initiating graceful shutdown");
185                        if self
186                            .shutdown_coordinator
187                            .initiate_shutdown(ShutdownReason::Signal(15))
188                        {
189                            break;
190                        }
191                    }
192                    SIGINT => {
193                        info!("Received SIGINT (Ctrl+C), initiating graceful shutdown");
194                        if self
195                            .shutdown_coordinator
196                            .initiate_shutdown(ShutdownReason::Signal(2))
197                        {
198                            break;
199                        }
200                    }
201                    SIGQUIT => {
202                        warn!("Received SIGQUIT, initiating immediate shutdown");
203                        if self
204                            .shutdown_coordinator
205                            .initiate_shutdown(ShutdownReason::Signal(3))
206                        {
207                            break;
208                        }
209                    }
210                    SIGHUP => {
211                        info!("Received SIGHUP, could be used for config reload (initiating shutdown for now)");
212                        if self
213                            .shutdown_coordinator
214                            .initiate_shutdown(ShutdownReason::Signal(1))
215                        {
216                            break;
217                        }
218                    }
219                    _ => {}
220                }
221            }
222        }
223
224        Ok(())
225    }
226}
227
228// Windows-specific signal handling implementation
229#[cfg(windows)]
230impl SignalHandler {
231    async fn handle_windows_signals(&self) -> Result<()> {
232        #[cfg(feature = "tokio")]
233        {
234            self.handle_windows_signals_tokio().await
235        }
236
237        #[cfg(all(feature = "async-std", not(feature = "tokio")))]
238        {
239            self.handle_windows_signals_async_std().await
240        }
241    }
242
243    #[cfg(feature = "tokio")]
244    async fn handle_windows_signals_tokio(&self) -> Result<()> {
245        use tokio::signal::windows::{ctrl_break, ctrl_c, ctrl_close, ctrl_shutdown};
246
247        // Set up Windows console event handlers
248        let mut ctrl_c_stream = ctrl_c()
249            .map_err(|e| Error::signal(format!("Failed to register Ctrl+C handler: {}", e)))?;
250
251        let mut ctrl_break_stream = ctrl_break()
252            .map_err(|e| Error::signal(format!("Failed to register Ctrl+Break handler: {}", e)))?;
253
254        let mut ctrl_close_stream = ctrl_close()
255            .map_err(|e| Error::signal(format!("Failed to register Ctrl+Close handler: {}", e)))?;
256
257        let mut ctrl_shutdown_stream = ctrl_shutdown()
258            .map_err(|e| Error::signal(format!("Failed to register shutdown handler: {}", e)))?;
259
260        info!("Windows console event handlers registered");
261
262        loop {
263            tokio::select! {
264                _ = ctrl_c_stream.recv() => {
265                    info!("Received Ctrl+C, initiating graceful shutdown");
266                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
267                        break;
268                    }
269                }
270                _ = ctrl_break_stream.recv() => {
271                    info!("Received Ctrl+Break, initiating graceful shutdown");
272                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
273                        break;
274                    }
275                }
276                _ = ctrl_close_stream.recv() => {
277                    warn!("Received console close event, initiating immediate shutdown");
278                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(1)) {
279                        break;
280                    }
281                }
282                _ = ctrl_shutdown_stream.recv() => {
283                    warn!("Received system shutdown event, initiating immediate shutdown");
284                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(6)) {
285                        break;
286                    }
287                }
288            }
289
290            // Check if we should stop handling signals
291            if !self.is_handling() {
292                debug!("Signal handling stopped by request");
293                break;
294            }
295        }
296
297        Ok(())
298    }
299
300    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
301    async fn handle_windows_signals_async_std(&self) -> Result<()> {
302        use std::sync::atomic::{AtomicBool, Ordering};
303
304        let shutdown_flag = Arc::new(AtomicBool::new(false));
305        let shutdown_flag_clone = Arc::clone(&shutdown_flag);
306
307        // For Windows with async-std, we'll use a simpler approach
308        // Install a basic Ctrl+C handler
309        ctrlc::set_handler(move || {
310            shutdown_flag_clone.store(true, Ordering::Release);
311        })
312        .map_err(|e| Error::signal(format!("Failed to set Ctrl+C handler: {}", e)))?;
313
314        info!("Windows Ctrl+C handler registered");
315
316        // Simple polling approach for async-std
317        while self.is_handling() && !shutdown_flag.load(Ordering::Acquire) {
318            async_std::task::sleep(std::time::Duration::from_millis(100)).await;
319        }
320
321        if shutdown_flag.load(Ordering::Acquire) {
322            info!("Received Windows console event, initiating graceful shutdown");
323            self.shutdown_coordinator
324                .initiate_shutdown(ShutdownReason::Signal(2));
325        }
326
327        Ok(())
328    }
329}
330
331// Simple signal handler for async-std on Unix
332#[cfg(all(unix, feature = "async-std", not(feature = "tokio")))]
333#[allow(dead_code)]
334#[allow(clippy::missing_const_for_fn)]
335extern "C" fn handle_signal(_signal: libc::c_int) {
336    // In a real implementation, we'd need to communicate back to the async task
337    // For now, this is a placeholder
338}
339
340/// Helper function to get a human-readable description of a signal.
341#[must_use]
342pub const fn signal_description(signal: i32) -> &'static str {
343    match signal {
344        1 => "SIGHUP (Hangup)",
345        2 => "SIGINT (Interrupt/Ctrl+C)",
346        3 => "SIGQUIT (Quit)",
347        6 => "SIGABRT (Abort)",
348        9 => "SIGKILL (Kill - non-catchable)",
349        15 => "SIGTERM (Terminate)",
350        _ => "Unknown signal",
351    }
352}
353
354/// Signal handling mode
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
356pub enum SignalHandling {
357    /// Enabled - handle this signal
358    Enabled,
359    /// Disabled - do not handle this signal
360    #[default]
361    Disabled,
362}
363
364impl From<bool> for SignalHandling {
365    fn from(value: bool) -> Self {
366        if value {
367            Self::Enabled
368        } else {
369            Self::Disabled
370        }
371    }
372}
373
374impl From<SignalHandling> for bool {
375    fn from(value: SignalHandling) -> Self {
376        match value {
377            SignalHandling::Enabled => true,
378            SignalHandling::Disabled => false,
379        }
380    }
381}
382
383/// Configuration for signal handling
384#[derive(Debug, Clone)]
385pub struct SignalConfig {
386    /// SIGTERM handling for graceful shutdown
387    pub term: SignalHandling,
388    /// SIGINT (Ctrl+C) handling for graceful shutdown  
389    pub interrupt: SignalHandling,
390    /// SIGQUIT handling for immediate shutdown
391    pub quit: SignalHandling,
392    /// SIGHUP handling for configuration reload
393    pub hangup: SignalHandling,
394    /// SIGUSR1 handling for custom actions
395    pub user1: SignalHandling,
396    /// SIGUSR2 handling for custom actions
397    pub user2: SignalHandling,
398    /// Custom signal handlers with signal number and description
399    pub custom_handlers: Vec<(i32, String)>,
400}
401
402impl Default for SignalConfig {
403    fn default() -> Self {
404        Self {
405            term: SignalHandling::Enabled,
406            interrupt: SignalHandling::Enabled,
407            quit: SignalHandling::Enabled,
408            hangup: SignalHandling::Disabled, // Disabled by default
409            user1: SignalHandling::Disabled,
410            user2: SignalHandling::Disabled,
411            // Pre-allocate with a reasonable capacity to avoid reallocation
412            custom_handlers: Vec::with_capacity(4),
413        }
414    }
415}
416
417impl SignalConfig {
418    /// Create a new signal configuration with defaults.
419    #[must_use]
420    pub fn new() -> Self {
421        Self::default()
422    }
423
424    /// Enable SIGHUP handling.
425    #[must_use]
426    pub const fn with_sighup(mut self) -> Self {
427        self.hangup = SignalHandling::Enabled;
428        self
429    }
430
431    /// Enable SIGUSR1 handling.
432    #[must_use]
433    pub const fn with_sigusr1(mut self) -> Self {
434        self.user1 = SignalHandling::Enabled;
435        self
436    }
437
438    /// Enable SIGUSR2 handling.
439    #[must_use]
440    pub const fn with_sigusr2(mut self) -> Self {
441        self.user2 = SignalHandling::Enabled;
442        self
443    }
444
445    /// Add a custom signal handler.
446    #[must_use]
447    pub fn with_custom_handler<S: Into<String>>(mut self, signal: i32, description: S) -> Self {
448        self.custom_handlers.push((signal, description.into()));
449        self
450    }
451
452    /// Disable SIGINT handling.
453    #[must_use]
454    pub const fn without_sigint(mut self) -> Self {
455        self.interrupt = SignalHandling::Disabled;
456        self
457    }
458
459    /// Disable SIGTERM handling.
460    #[must_use]
461    pub const fn without_sigterm(mut self) -> Self {
462        self.term = SignalHandling::Disabled;
463        self
464    }
465
466    /// Disable SIGQUIT handling.
467    #[must_use]
468    pub const fn without_sigquit(mut self) -> Self {
469        self.quit = SignalHandling::Disabled;
470        self
471    }
472}
473
474/// Advanced signal handler with configurable signal handling.
475#[derive(Debug)]
476pub struct ConfigurableSignalHandler {
477    #[allow(dead_code)]
478    shutdown_coordinator: ShutdownCoordinator,
479    config: SignalConfig,
480    handling_signals: AtomicBool,
481}
482
483impl ConfigurableSignalHandler {
484    /// Create a new configurable signal handler.
485    #[must_use]
486    pub const fn new(shutdown_coordinator: ShutdownCoordinator, config: SignalConfig) -> Self {
487        Self {
488            shutdown_coordinator,
489            config,
490            handling_signals: AtomicBool::new(false),
491        }
492    }
493
494    /// Start handling configured signals.
495    ///
496    /// # Errors
497    ///
498    /// Returns an error if signal handling is already active or if there's a problem
499    /// registering signal handlers on the platform.
500    pub async fn handle_signals(&self) -> Result<()> {
501        if self
502            .handling_signals
503            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
504            .is_err()
505        {
506            return Err(Error::invalid_state("Signal handling already started"));
507        }
508
509        info!("Starting configurable signal handler");
510
511        // Log which signals will be handled
512        // Pre-allocate vector with exact capacity needed based on configuration
513        let mut handled_signals = Vec::with_capacity(6); // Max 6 standard signals
514        if bool::from(self.config.term) {
515            handled_signals.push("SIGTERM");
516        }
517        if bool::from(self.config.interrupt) {
518            handled_signals.push("SIGINT");
519        }
520        if bool::from(self.config.quit) {
521            handled_signals.push("SIGQUIT");
522        }
523        if bool::from(self.config.hangup) {
524            handled_signals.push("SIGHUP");
525        }
526        if bool::from(self.config.user1) {
527            handled_signals.push("SIGUSR1");
528        }
529        if bool::from(self.config.user2) {
530            handled_signals.push("SIGUSR2");
531        }
532
533        info!("Handling signals: {:?}", handled_signals);
534
535        // Platform-specific implementation
536        #[cfg(unix)]
537        {
538            self.handle_configured_unix_signals().await
539        }
540
541        #[cfg(windows)]
542        {
543            self.handle_configured_windows_signals().await
544        }
545    }
546
547    #[cfg(unix)]
548    async fn handle_configured_unix_signals(&self) -> Result<()> {
549        #[cfg(feature = "tokio")]
550        {
551            use tokio::signal::unix::{signal, SignalKind};
552
553            // For now, just handle SIGTERM and SIGINT with basic approach
554            if bool::from(self.config.term) || bool::from(self.config.interrupt) {
555                let mut sigterm = signal(SignalKind::terminate())?;
556                let mut sigint = signal(SignalKind::interrupt())?;
557
558                loop {
559                    tokio::select! {
560                        _ = sigterm.recv(), if bool::from(self.config.term) => {
561                            info!("Received SIGTERM, initiating graceful shutdown");
562                            if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(15)) {
563                                break;
564                            }
565                        }
566                        _ = sigint.recv(), if bool::from(self.config.interrupt) => {
567                            info!("Received SIGINT, initiating graceful shutdown");
568                            if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
569                                break;
570                            }
571                        }
572                    }
573
574                    if !self.handling_signals.load(Ordering::Acquire) {
575                        break;
576                    }
577                }
578            }
579        }
580
581        #[cfg(all(feature = "async-std", not(feature = "tokio")))]
582        {
583            // Simplified async-std implementation
584            while self.handling_signals.load(Ordering::Acquire) {
585                async_std::task::sleep(std::time::Duration::from_millis(100)).await;
586            }
587        }
588
589        Ok(())
590    }
591
592    #[cfg(windows)]
593    async fn handle_configured_windows_signals(&self) -> Result<()> {
594        // Simplified Windows implementation
595        #[cfg(feature = "tokio")]
596        {
597            use tokio::signal::windows::{ctrl_break, ctrl_c};
598
599            let mut ctrl_c_stream = ctrl_c()?;
600            let mut ctrl_break_stream = ctrl_break()?;
601
602            loop {
603                tokio::select! {
604                    _ = ctrl_c_stream.recv() => {
605                        info!("Received Ctrl+C, initiating graceful shutdown");
606                        if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
607                            break;
608                        }
609                    }
610                    _ = ctrl_break_stream.recv() => {
611                        info!("Received Ctrl+Break, initiating graceful shutdown");
612                        if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
613                            break;
614                        }
615                    }
616                }
617
618                if !self.handling_signals.load(Ordering::Acquire) {
619                    break;
620                }
621            }
622        }
623
624        #[cfg(all(feature = "async-std", not(feature = "tokio")))]
625        {
626            while self.handling_signals.load(Ordering::Acquire) {
627                async_std::task::sleep(std::time::Duration::from_millis(100)).await;
628            }
629        }
630
631        Ok(())
632    }
633
634    /// Stop signal handling.
635    pub fn stop(&self) {
636        self.handling_signals.store(false, Ordering::Release);
637        debug!("Configurable signal handling stopped");
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644    use crate::shutdown::ShutdownCoordinator;
645    use std::time::Duration;
646
647    #[test]
648    fn test_signal_description() {
649        assert_eq!(signal_description(15), "SIGTERM (Terminate)");
650        assert_eq!(signal_description(2), "SIGINT (Interrupt/Ctrl+C)");
651        assert_eq!(signal_description(999), "Unknown signal");
652    }
653
654    #[test]
655    fn test_signal_config() {
656        let config = SignalConfig::new()
657            .with_sighup()
658            .with_custom_handler(12, "Custom signal")
659            .without_sigint();
660
661        assert_eq!(config.interrupt, SignalHandling::Disabled);
662        assert_eq!(config.term, SignalHandling::Enabled);
663        assert_eq!(config.hangup, SignalHandling::Enabled);
664        assert_eq!(config.custom_handlers.len(), 1);
665        assert_eq!(config.custom_handlers[0].0, 12);
666    }
667
668    #[cfg(feature = "tokio")]
669    #[cfg_attr(miri, ignore)]
670    #[tokio::test]
671    async fn test_signal_handler_creation() {
672        // Add a test timeout to prevent freezing
673        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
674            let coordinator = ShutdownCoordinator::new(5000, 10000, 15000);
675            let handler = SignalHandler::new(coordinator);
676
677            assert!(!handler.is_handling());
678
679            // Note: We can't easily test the actual signal handling without
680            // sending real signals, which would be complex in a test environment
681        })
682        .await;
683
684        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
685    }
686
687    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
688    #[async_std::test]
689    async fn test_signal_handler_creation() {
690        // Add a test timeout to prevent freezing
691        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
692            let coordinator = ShutdownCoordinator::new(5000, 10000, 15000);
693            let handler = SignalHandler::new(coordinator);
694
695            assert!(!handler.is_handling());
696
697            // Note: We can't easily test the actual signal handling without
698            // sending real signals, which would be complex in a test environment
699        })
700        .await;
701
702        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
703    }
704}