1use std::sync::atomic::{AtomicBool, Ordering};
8#[allow(unused_imports)]
9use tracing::{debug, info, warn};
10
11use crate::ShutdownReason;
12
13use crate::error::{Error, Result};
17use crate::shutdown::ShutdownCoordinator;
18
19#[derive(Debug)]
21pub struct SignalHandler {
22 #[allow(dead_code)]
23 shutdown_coordinator: ShutdownCoordinator,
24 handling_signals: AtomicBool,
25}
26
27impl SignalHandler {
28 #[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 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 #[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 pub fn stop(&self) {
69 self.handling_signals.store(false, Ordering::Release);
70 debug!("Signal handling stopped");
71 }
72
73 pub fn is_handling(&self) -> bool {
75 self.handling_signals.load(Ordering::Acquire)
76 }
77}
78
79#[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 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 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 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#[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 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 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 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 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#[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 }
339
340#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
356pub enum SignalHandling {
357 Enabled,
359 #[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#[derive(Debug, Clone)]
385pub struct SignalConfig {
386 pub term: SignalHandling,
388 pub interrupt: SignalHandling,
390 pub quit: SignalHandling,
392 pub hangup: SignalHandling,
394 pub user1: SignalHandling,
396 pub user2: SignalHandling,
398 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, user1: SignalHandling::Disabled,
410 user2: SignalHandling::Disabled,
411 custom_handlers: Vec::with_capacity(4),
413 }
414 }
415}
416
417impl SignalConfig {
418 #[must_use]
420 pub fn new() -> Self {
421 Self::default()
422 }
423
424 #[must_use]
426 pub const fn with_sighup(mut self) -> Self {
427 self.hangup = SignalHandling::Enabled;
428 self
429 }
430
431 #[must_use]
433 pub const fn with_sigusr1(mut self) -> Self {
434 self.user1 = SignalHandling::Enabled;
435 self
436 }
437
438 #[must_use]
440 pub const fn with_sigusr2(mut self) -> Self {
441 self.user2 = SignalHandling::Enabled;
442 self
443 }
444
445 #[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 #[must_use]
454 pub const fn without_sigint(mut self) -> Self {
455 self.interrupt = SignalHandling::Disabled;
456 self
457 }
458
459 #[must_use]
461 pub const fn without_sigterm(mut self) -> Self {
462 self.term = SignalHandling::Disabled;
463 self
464 }
465
466 #[must_use]
468 pub const fn without_sigquit(mut self) -> Self {
469 self.quit = SignalHandling::Disabled;
470 self
471 }
472}
473
474#[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 #[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 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 let mut handled_signals = Vec::with_capacity(6); 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 #[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 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 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 #[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 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 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 })
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 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 })
700 .await;
701
702 assert!(test_result.is_ok(), "Test timed out after 5 seconds");
703 }
704}