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 std::time::Duration;
172
173        // Use the signal-hook crate for safe signal handling
174        let term = async_std::channel::bounded::<()>(1);
175        let _int = async_std::channel::bounded::<i32>(1);
176
177        // Register signal handlers safely using ctrlc for SIGTERM (15)
178        let term_sender = term.0.clone();
179        ctrlc::set_handler(move || {
180            let _ = term_sender.try_send(());
181        })?;
182
183        info!("Unix signal handlers registered (SIGTERM, SIGINT)");
184
185        // Poll for signals
186        loop {
187            // Wait for signals with a timeout
188            let term_recv = term.1.clone();
189
190            match async_std::future::timeout(Duration::from_millis(100), term_recv.recv()).await {
191                Ok(Ok(())) => {
192                    info!("Received SIGTERM, initiating graceful shutdown");
193                    if self
194                        .shutdown_coordinator
195                        .initiate_shutdown(ShutdownReason::Signal(15))
196                    {
197                        break;
198                    }
199                }
200                _ => {
201                    // Timeout or error, check if we should stop handling
202                    if !self.is_handling() {
203                        debug!("Signal handling stopped by request");
204                        break;
205                    }
206                }
207            }
208
209            // Brief sleep to prevent tight loop
210            async_std::task::sleep(Duration::from_millis(10)).await;
211        }
212
213        Ok(())
214    }
215}
216
217// Windows-specific signal handling implementation
218#[cfg(windows)]
219impl SignalHandler {
220    async fn handle_windows_signals(&self) -> Result<()> {
221        #[cfg(feature = "tokio")]
222        {
223            self.handle_windows_signals_tokio().await
224        }
225
226        #[cfg(all(feature = "async-std", not(feature = "tokio")))]
227        {
228            self.handle_windows_signals_async_std().await
229        }
230    }
231
232    #[cfg(feature = "tokio")]
233    async fn handle_windows_signals_tokio(&self) -> Result<()> {
234        use tokio::signal::windows::{ctrl_break, ctrl_c, ctrl_close, ctrl_shutdown};
235
236        // Set up Windows console event handlers
237        let mut ctrl_c_stream = ctrl_c()
238            .map_err(|e| Error::signal(format!("Failed to register Ctrl+C handler: {}", e)))?;
239
240        let mut ctrl_break_stream = ctrl_break()
241            .map_err(|e| Error::signal(format!("Failed to register Ctrl+Break handler: {}", e)))?;
242
243        let mut ctrl_close_stream = ctrl_close()
244            .map_err(|e| Error::signal(format!("Failed to register Ctrl+Close handler: {}", e)))?;
245
246        let mut ctrl_shutdown_stream = ctrl_shutdown()
247            .map_err(|e| Error::signal(format!("Failed to register shutdown handler: {}", e)))?;
248
249        info!("Windows console event handlers registered");
250
251        loop {
252            tokio::select! {
253                _ = ctrl_c_stream.recv() => {
254                    info!("Received Ctrl+C, initiating graceful shutdown");
255                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
256                        break;
257                    }
258                }
259                _ = ctrl_break_stream.recv() => {
260                    info!("Received Ctrl+Break, initiating graceful shutdown");
261                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
262                        break;
263                    }
264                }
265                _ = ctrl_close_stream.recv() => {
266                    warn!("Received console close event, initiating immediate shutdown");
267                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(1)) {
268                        break;
269                    }
270                }
271                _ = ctrl_shutdown_stream.recv() => {
272                    warn!("Received system shutdown event, initiating immediate shutdown");
273                    if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(6)) {
274                        break;
275                    }
276                }
277            }
278
279            // Check if we should stop handling signals
280            if !self.is_handling() {
281                debug!("Signal handling stopped by request");
282                break;
283            }
284        }
285
286        Ok(())
287    }
288
289    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
290    async fn handle_windows_signals_async_std(&self) -> Result<()> {
291        use std::sync::atomic::{AtomicBool, Ordering};
292
293        let shutdown_flag = Arc::new(AtomicBool::new(false));
294        let shutdown_flag_clone = Arc::clone(&shutdown_flag);
295
296        // For Windows with async-std, we'll use a simpler approach
297        // Install a basic Ctrl+C handler
298        ctrlc::set_handler(move || {
299            shutdown_flag_clone.store(true, Ordering::Release);
300        })
301        .map_err(|e| Error::signal(format!("Failed to set Ctrl+C handler: {}", e)))?;
302
303        info!("Windows Ctrl+C handler registered");
304
305        // Simple polling approach for async-std
306        while self.is_handling() && !shutdown_flag.load(Ordering::Acquire) {
307            async_std::task::sleep(std::time::Duration::from_millis(100)).await;
308        }
309
310        if shutdown_flag.load(Ordering::Acquire) {
311            info!("Received Windows console event, initiating graceful shutdown");
312            self.shutdown_coordinator
313                .initiate_shutdown(ShutdownReason::Signal(2));
314        }
315
316        Ok(())
317    }
318}
319
320// Simple signal handler for async-std on Unix
321#[cfg(all(unix, feature = "async-std", not(feature = "tokio")))]
322#[allow(dead_code)]
323#[allow(clippy::missing_const_for_fn)]
324extern "C" fn handle_signal(_signal: libc::c_int) {
325    // In a real implementation, we'd need to communicate back to the async task
326    // For now, this is a placeholder
327}
328
329/// Helper function to get a human-readable description of a signal.
330#[must_use]
331pub const fn signal_description(signal: i32) -> &'static str {
332    match signal {
333        1 => "SIGHUP (Hangup)",
334        2 => "SIGINT (Interrupt/Ctrl+C)",
335        3 => "SIGQUIT (Quit)",
336        6 => "SIGABRT (Abort)",
337        9 => "SIGKILL (Kill - non-catchable)",
338        15 => "SIGTERM (Terminate)",
339        _ => "Unknown signal",
340    }
341}
342
343/// Signal handling mode
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum SignalHandling {
346    /// Enabled - handle this signal
347    Enabled,
348    /// Disabled - do not handle this signal
349    Disabled,
350}
351
352impl Default for SignalHandling {
353    fn default() -> Self {
354        Self::Disabled
355    }
356}
357
358impl From<bool> for SignalHandling {
359    fn from(value: bool) -> Self {
360        if value {
361            Self::Enabled
362        } else {
363            Self::Disabled
364        }
365    }
366}
367
368impl From<SignalHandling> for bool {
369    fn from(value: SignalHandling) -> Self {
370        match value {
371            SignalHandling::Enabled => true,
372            SignalHandling::Disabled => false,
373        }
374    }
375}
376
377/// Configuration for signal handling
378#[derive(Debug, Clone)]
379pub struct SignalConfig {
380    /// SIGTERM handling for graceful shutdown
381    pub term: SignalHandling,
382    /// SIGINT (Ctrl+C) handling for graceful shutdown  
383    pub interrupt: SignalHandling,
384    /// SIGQUIT handling for immediate shutdown
385    pub quit: SignalHandling,
386    /// SIGHUP handling for configuration reload
387    pub hangup: SignalHandling,
388    /// SIGUSR1 handling for custom actions
389    pub user1: SignalHandling,
390    /// SIGUSR2 handling for custom actions
391    pub user2: SignalHandling,
392    /// Custom signal handlers with signal number and description
393    pub custom_handlers: Vec<(i32, String)>,
394}
395
396impl Default for SignalConfig {
397    fn default() -> Self {
398        Self {
399            term: SignalHandling::Enabled,
400            interrupt: SignalHandling::Enabled,
401            quit: SignalHandling::Enabled,
402            hangup: SignalHandling::Disabled, // Disabled by default
403            user1: SignalHandling::Disabled,
404            user2: SignalHandling::Disabled,
405            // Pre-allocate with a reasonable capacity to avoid reallocation
406            custom_handlers: Vec::with_capacity(4),
407        }
408    }
409}
410
411impl SignalConfig {
412    /// Create a new signal configuration with defaults.
413    #[must_use]
414    pub fn new() -> Self {
415        Self::default()
416    }
417
418    /// Enable SIGHUP handling.
419    #[must_use]
420    pub const fn with_sighup(mut self) -> Self {
421        self.hangup = SignalHandling::Enabled;
422        self
423    }
424
425    /// Enable SIGUSR1 handling.
426    #[must_use]
427    pub const fn with_sigusr1(mut self) -> Self {
428        self.user1 = SignalHandling::Enabled;
429        self
430    }
431
432    /// Enable SIGUSR2 handling.
433    #[must_use]
434    pub const fn with_sigusr2(mut self) -> Self {
435        self.user2 = SignalHandling::Enabled;
436        self
437    }
438
439    /// Add a custom signal handler.
440    #[must_use]
441    pub fn with_custom_handler<S: Into<String>>(mut self, signal: i32, description: S) -> Self {
442        self.custom_handlers.push((signal, description.into()));
443        self
444    }
445
446    /// Disable SIGINT handling.
447    #[must_use]
448    pub const fn without_sigint(mut self) -> Self {
449        self.interrupt = SignalHandling::Disabled;
450        self
451    }
452
453    /// Disable SIGTERM handling.
454    #[must_use]
455    pub const fn without_sigterm(mut self) -> Self {
456        self.term = SignalHandling::Disabled;
457        self
458    }
459
460    /// Disable SIGQUIT handling.
461    #[must_use]
462    pub const fn without_sigquit(mut self) -> Self {
463        self.quit = SignalHandling::Disabled;
464        self
465    }
466}
467
468/// Advanced signal handler with configurable signal handling.
469#[derive(Debug)]
470pub struct ConfigurableSignalHandler {
471    #[allow(dead_code)]
472    shutdown_coordinator: ShutdownCoordinator,
473    config: SignalConfig,
474    handling_signals: AtomicBool,
475}
476
477impl ConfigurableSignalHandler {
478    /// Create a new configurable signal handler.
479    #[must_use]
480    pub const fn new(shutdown_coordinator: ShutdownCoordinator, config: SignalConfig) -> Self {
481        Self {
482            shutdown_coordinator,
483            config,
484            handling_signals: AtomicBool::new(false),
485        }
486    }
487
488    /// Start handling configured signals.
489    ///
490    /// # Errors
491    ///
492    /// Returns an error if signal handling is already active or if there's a problem
493    /// registering signal handlers on the platform.
494    pub async fn handle_signals(&self) -> Result<()> {
495        if self
496            .handling_signals
497            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
498            .is_err()
499        {
500            return Err(Error::invalid_state("Signal handling already started"));
501        }
502
503        info!("Starting configurable signal handler");
504
505        // Log which signals will be handled
506        // Pre-allocate vector with exact capacity needed based on configuration
507        let mut handled_signals = Vec::with_capacity(6); // Max 6 standard signals
508        if bool::from(self.config.term) {
509            handled_signals.push("SIGTERM");
510        }
511        if bool::from(self.config.interrupt) {
512            handled_signals.push("SIGINT");
513        }
514        if bool::from(self.config.quit) {
515            handled_signals.push("SIGQUIT");
516        }
517        if bool::from(self.config.hangup) {
518            handled_signals.push("SIGHUP");
519        }
520        if bool::from(self.config.user1) {
521            handled_signals.push("SIGUSR1");
522        }
523        if bool::from(self.config.user2) {
524            handled_signals.push("SIGUSR2");
525        }
526
527        info!("Handling signals: {:?}", handled_signals);
528
529        // Platform-specific implementation
530        #[cfg(unix)]
531        {
532            self.handle_configured_unix_signals().await
533        }
534
535        #[cfg(windows)]
536        {
537            self.handle_configured_windows_signals().await
538        }
539    }
540
541    #[cfg(unix)]
542    async fn handle_configured_unix_signals(&self) -> Result<()> {
543        #[cfg(feature = "tokio")]
544        {
545            use tokio::signal::unix::{signal, SignalKind};
546
547            // For now, just handle SIGTERM and SIGINT with basic approach
548            if bool::from(self.config.term) || bool::from(self.config.interrupt) {
549                let mut sigterm = signal(SignalKind::terminate())?;
550                let mut sigint = signal(SignalKind::interrupt())?;
551
552                loop {
553                    tokio::select! {
554                        _ = sigterm.recv(), if bool::from(self.config.term) => {
555                            info!("Received SIGTERM, initiating graceful shutdown");
556                            if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(15)) {
557                                break;
558                            }
559                        }
560                        _ = sigint.recv(), if bool::from(self.config.interrupt) => {
561                            info!("Received SIGINT, initiating graceful shutdown");
562                            if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
563                                break;
564                            }
565                        }
566                    }
567
568                    if !self.handling_signals.load(Ordering::Acquire) {
569                        break;
570                    }
571                }
572            }
573        }
574
575        #[cfg(all(feature = "async-std", not(feature = "tokio")))]
576        {
577            // Simplified async-std implementation
578            while self.handling_signals.load(Ordering::Acquire) {
579                async_std::task::sleep(std::time::Duration::from_millis(100)).await;
580            }
581        }
582
583        Ok(())
584    }
585
586    #[cfg(windows)]
587    async fn handle_configured_windows_signals(&self) -> Result<()> {
588        // Simplified Windows implementation
589        #[cfg(feature = "tokio")]
590        {
591            use tokio::signal::windows::{ctrl_break, ctrl_c};
592
593            let mut ctrl_c_stream = ctrl_c()?;
594            let mut ctrl_break_stream = ctrl_break()?;
595
596            loop {
597                tokio::select! {
598                    _ = ctrl_c_stream.recv() => {
599                        info!("Received Ctrl+C, initiating graceful shutdown");
600                        if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
601                            break;
602                        }
603                    }
604                    _ = ctrl_break_stream.recv() => {
605                        info!("Received Ctrl+Break, initiating graceful shutdown");
606                        if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
607                            break;
608                        }
609                    }
610                }
611
612                if !self.handling_signals.load(Ordering::Acquire) {
613                    break;
614                }
615            }
616        }
617
618        #[cfg(all(feature = "async-std", not(feature = "tokio")))]
619        {
620            while self.handling_signals.load(Ordering::Acquire) {
621                async_std::task::sleep(std::time::Duration::from_millis(100)).await;
622            }
623        }
624
625        Ok(())
626    }
627
628    /// Stop signal handling.
629    pub fn stop(&self) {
630        self.handling_signals.store(false, Ordering::Release);
631        debug!("Configurable signal handling stopped");
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    use crate::shutdown::ShutdownCoordinator;
639    use std::time::Duration;
640
641    #[test]
642    fn test_signal_description() {
643        assert_eq!(signal_description(15), "SIGTERM (Terminate)");
644        assert_eq!(signal_description(2), "SIGINT (Interrupt/Ctrl+C)");
645        assert_eq!(signal_description(999), "Unknown signal");
646    }
647
648    #[test]
649    fn test_signal_config() {
650        let config = SignalConfig::new()
651            .with_sighup()
652            .with_custom_handler(12, "Custom signal")
653            .without_sigint();
654
655        assert_eq!(config.interrupt, SignalHandling::Disabled);
656        assert_eq!(config.term, SignalHandling::Enabled);
657        assert_eq!(config.hangup, SignalHandling::Enabled);
658        assert_eq!(config.custom_handlers.len(), 1);
659        assert_eq!(config.custom_handlers[0].0, 12);
660    }
661
662    #[cfg(feature = "tokio")]
663    #[cfg_attr(miri, ignore)]
664    #[tokio::test]
665    async fn test_signal_handler_creation() {
666        // Add a test timeout to prevent freezing
667        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
668            let coordinator = ShutdownCoordinator::new(5000, 10000);
669            let handler = SignalHandler::new(coordinator);
670
671            assert!(!handler.is_handling());
672
673            // Note: We can't easily test the actual signal handling without
674            // sending real signals, which would be complex in a test environment
675        })
676        .await;
677
678        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
679    }
680
681    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
682    #[async_std::test]
683    async fn test_signal_handler_creation() {
684        // Add a test timeout to prevent freezing
685        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
686            let coordinator = ShutdownCoordinator::new(5000, 10000);
687            let handler = SignalHandler::new(coordinator);
688
689            assert!(!handler.is_handling());
690
691            // Note: We can't easily test the actual signal handling without
692            // sending real signals, which would be complex in a test environment
693        })
694        .await;
695
696        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
697    }
698}