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 std::time::Duration;
172
173 let term = async_std::channel::bounded::<()>(1);
175 let _int = async_std::channel::bounded::<i32>(1);
176
177 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 loop {
187 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 if !self.is_handling() {
203 debug!("Signal handling stopped by request");
204 break;
205 }
206 }
207 }
208
209 async_std::task::sleep(Duration::from_millis(10)).await;
211 }
212
213 Ok(())
214 }
215}
216
217#[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 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 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 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 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#[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 }
328
329#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum SignalHandling {
346 Enabled,
348 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#[derive(Debug, Clone)]
379pub struct SignalConfig {
380 pub term: SignalHandling,
382 pub interrupt: SignalHandling,
384 pub quit: SignalHandling,
386 pub hangup: SignalHandling,
388 pub user1: SignalHandling,
390 pub user2: SignalHandling,
392 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, user1: SignalHandling::Disabled,
404 user2: SignalHandling::Disabled,
405 custom_handlers: Vec::with_capacity(4),
407 }
408 }
409}
410
411impl SignalConfig {
412 #[must_use]
414 pub fn new() -> Self {
415 Self::default()
416 }
417
418 #[must_use]
420 pub const fn with_sighup(mut self) -> Self {
421 self.hangup = SignalHandling::Enabled;
422 self
423 }
424
425 #[must_use]
427 pub const fn with_sigusr1(mut self) -> Self {
428 self.user1 = SignalHandling::Enabled;
429 self
430 }
431
432 #[must_use]
434 pub const fn with_sigusr2(mut self) -> Self {
435 self.user2 = SignalHandling::Enabled;
436 self
437 }
438
439 #[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 #[must_use]
448 pub const fn without_sigint(mut self) -> Self {
449 self.interrupt = SignalHandling::Disabled;
450 self
451 }
452
453 #[must_use]
455 pub const fn without_sigterm(mut self) -> Self {
456 self.term = SignalHandling::Disabled;
457 self
458 }
459
460 #[must_use]
462 pub const fn without_sigquit(mut self) -> Self {
463 self.quit = SignalHandling::Disabled;
464 self
465 }
466}
467
468#[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 #[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 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 let mut handled_signals = Vec::with_capacity(6); 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 #[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 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 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 #[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 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 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 })
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 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 })
694 .await;
695
696 assert!(test_result.is_ok(), "Test timed out after 5 seconds");
697 }
698}