Skip to main content

ssh_cli/
signals.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Operating system signal handling.
3//!
4//! Registers a Ctrl+C (SIGINT) handler that signals cancellation
5//! via a shared [`AtomicBool`]. All modules that run
6//! long operations must check [`is_cancelled`] periodically.
7
8use anyhow::Result;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, OnceLock};
11
12/// Global cancellation flag. Set once at initialization.
13static CANCEL_FLAG: OnceLock<Arc<AtomicBool>> = OnceLock::new();
14
15/// Registers the Ctrl+C handler that sets the cancellation flag.
16///
17/// Must be called once before any long-running operation.
18/// Additional calls are safe and silently ignored.
19pub fn register_handler() -> Result<()> {
20    let flag = cancellation_flag();
21    let flag_clone = Arc::clone(&flag);
22
23    ctrlc::set_handler(move || {
24        flag_clone.store(true, Ordering::SeqCst);
25        tracing::debug!("cancellation signal received via Ctrl+C");
26    })?;
27
28    tracing::debug!("Ctrl+C handler registered successfully");
29
30    #[cfg(unix)]
31    {
32        let flag_term = sigterm_flag();
33        signal_hook::flag::register(signal_hook::consts::SIGTERM, flag_term)?;
34        tracing::debug!("handler SIGTERM registrado");
35    }
36
37    #[cfg(not(unix))]
38    {
39        // On Windows, SIGTERM is not natively supported.
40        // ctrlc already covers Ctrl+C (SIGINT equivalent).
41        let _ = sigterm_flag(); // Initializes OnceLock even without a handler
42    }
43
44    Ok(())
45}
46
47/// Returns `true` if the user pressed Ctrl+C.
48///
49/// Must be checked in long-running loops to allow
50/// graceful shutdown.
51///
52/// # Examples
53///
54/// ```
55/// use ssh_cli::signals::is_cancelled;
56///
57/// // Before registering a handler, returns false
58/// assert!(!is_cancelled());
59/// ```
60#[must_use]
61pub fn is_cancelled() -> bool {
62    CANCEL_FLAG
63        .get()
64        .map(|f| f.load(Ordering::SeqCst))
65        .unwrap_or(false)
66}
67
68/// Returns the shared cancellation flag pointer.
69///
70/// Useful to pass the flag to async tasks that need
71/// to check cancellation without calling [`is_cancelled`] directly.
72#[must_use]
73pub fn cancellation_flag() -> Arc<AtomicBool> {
74    Arc::clone(CANCEL_FLAG.get_or_init(|| Arc::new(AtomicBool::new(false))))
75}
76
77/// Global SIGTERM flag.
78static FLAG_SIGTERM: OnceLock<Arc<AtomicBool>> = OnceLock::new();
79
80/// Returns `true` if the process received SIGTERM.
81#[must_use]
82pub fn is_terminated() -> bool {
83    FLAG_SIGTERM
84        .get()
85        .map(|f| f.load(Ordering::SeqCst))
86        .unwrap_or(false)
87}
88
89/// Returns the Arc of the SIGTERM flag for async tasks.
90#[must_use]
91pub fn sigterm_flag() -> Arc<AtomicBool> {
92    Arc::clone(FLAG_SIGTERM.get_or_init(|| Arc::new(AtomicBool::new(false))))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use serial_test::serial;
99
100    #[test]
101    #[serial]
102    fn is_cancelled_false_before_signal() {
103        // Flag should not be set in initial state
104        // (unless another test set it; each test shares the same OnceLock)
105        // We only check that the call does not panic.
106        let _ = is_cancelled();
107    }
108
109    #[test]
110    #[serial]
111    fn cancellation_flag_returns_same_arc() {
112        let flag_a = cancellation_flag();
113        let flag_b = cancellation_flag();
114        // Both must point to the same underlying AtomicBool
115        assert!(Arc::ptr_eq(&flag_a, &flag_b));
116    }
117
118    #[test]
119    #[serial]
120    fn flag_can_be_set_and_read() {
121        let flag = cancellation_flag();
122        // We only check that the AtomicBool works correctly
123        let previous_value = flag.load(Ordering::SeqCst);
124        flag.store(previous_value, Ordering::SeqCst);
125        assert_eq!(flag.load(Ordering::SeqCst), previous_value);
126    }
127
128    #[test]
129    #[serial]
130    fn is_terminated_false_by_default() {
131        // GAP-SSH-TEST-001: serial + explicit reset avoids races with parallel tests.
132        let flag = sigterm_flag();
133        flag.store(false, Ordering::SeqCst);
134        assert!(!is_terminated());
135    }
136
137    #[test]
138    #[serial]
139    fn sigterm_flag_returns_same_arc() {
140        let a = sigterm_flag();
141        let b = sigterm_flag();
142        assert!(Arc::ptr_eq(&a, &b));
143    }
144
145    #[test]
146    #[serial]
147    fn is_terminated_true_after_set() {
148        let flag = sigterm_flag();
149        flag.store(true, Ordering::SeqCst);
150        assert!(is_terminated());
151        flag.store(false, Ordering::SeqCst); // Reset so other tests are not affected
152    }
153
154    #[test]
155    #[serial]
156    fn is_cancelled_false_after_reset() {
157        let flag = cancellation_flag();
158        flag.store(true, Ordering::SeqCst);
159        assert!(is_cancelled());
160        flag.store(false, Ordering::SeqCst);
161        assert!(!is_cancelled());
162    }
163}