Skip to main content

ssh_cli/
signals.rs

1//! Tratamento de sinais do sistema operacional.
2//!
3//! Registra um handler para Ctrl+C (SIGINT) que sinaliza cancelamento
4//! via um [`AtomicBool`] compartilhado. Todos os módulos que executam
5//! operações longas devem verificar [`cancelado`] periodicamente.
6
7use anyhow::Result;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::{Arc, OnceLock};
10
11/// Flag global de cancelamento. Definida uma única vez na inicialização.
12static FLAG_CANCELAMENTO: OnceLock<Arc<AtomicBool>> = OnceLock::new();
13
14/// Registra o handler de Ctrl+C que marca a flag de cancelamento.
15///
16/// Deve ser chamada uma única vez, antes de qualquer operação longa.
17/// Chamadas adicionais são seguras e ignoradas silenciosamente.
18pub fn registrar_handler() -> Result<()> {
19    let flag = obter_flag();
20    let flag_clone = Arc::clone(&flag);
21
22    ctrlc::set_handler(move || {
23        flag_clone.store(true, Ordering::SeqCst);
24        tracing::debug!("sinal de cancelamento recebido via Ctrl+C");
25    })?;
26
27    tracing::debug!("handler de Ctrl+C registrado com sucesso");
28
29    #[cfg(unix)]
30    {
31        let flag_term = obter_flag_sigterm();
32        signal_hook::flag::register(signal_hook::consts::SIGTERM, flag_term)?;
33        tracing::debug!("handler SIGTERM registrado");
34    }
35
36    #[cfg(not(unix))]
37    {
38        // No Windows, SIGTERM não é suportado nativamente.
39        // ctrlc já cobre Ctrl+C (equivalente a SIGINT).
40        let _ = obter_flag_sigterm(); // Inicializa OnceLock mesmo sem handler
41    }
42
43    Ok(())
44}
45
46/// Retorna `true` se o usuário pressionou Ctrl+C.
47///
48/// Deve ser verificado em loops de operações longas para permitir
49/// encerramento gracioso.
50///
51/// # Examples
52///
53/// ```
54/// use ssh_cli::signals::cancelado;
55///
56/// // Antes de registrar handler, retorna false
57/// assert!(!cancelado());
58/// ```
59#[must_use]
60pub fn cancelado() -> bool {
61    FLAG_CANCELAMENTO
62        .get()
63        .map(|f| f.load(Ordering::SeqCst))
64        .unwrap_or(false)
65}
66
67/// Retorna o ponteiro compartilhado da flag de cancelamento.
68///
69/// Útil para passar a flag para tarefas assíncronas que precisam
70/// verificar cancelamento sem chamar [`cancelado`] diretamente.
71#[must_use]
72pub fn obter_flag() -> Arc<AtomicBool> {
73    Arc::clone(FLAG_CANCELAMENTO.get_or_init(|| Arc::new(AtomicBool::new(false))))
74}
75
76/// Flag global para SIGTERM.
77static FLAG_SIGTERM: OnceLock<Arc<AtomicBool>> = OnceLock::new();
78
79/// Retorna `true` se o processo recebeu SIGTERM.
80#[must_use]
81pub fn terminado() -> bool {
82    FLAG_SIGTERM
83        .get()
84        .map(|f| f.load(Ordering::SeqCst))
85        .unwrap_or(false)
86}
87
88/// Retorna o Arc do flag de SIGTERM para uso em tarefas async.
89#[must_use]
90pub fn obter_flag_sigterm() -> Arc<AtomicBool> {
91    Arc::clone(FLAG_SIGTERM.get_or_init(|| Arc::new(AtomicBool::new(false))))
92}
93
94#[cfg(test)]
95mod testes {
96    use super::*;
97    use serial_test::serial;
98
99    #[test]
100    #[serial]
101    fn cancelado_retorna_false_antes_de_sinal() {
102        // A flag não deve estar marcada em estado inicial
103        // (a menos que outro teste tenha ativado, mas cada test usa a mesma OnceLock)
104        // Verificamos apenas que a chamada não panics.
105        let _ = cancelado();
106    }
107
108    #[test]
109    #[serial]
110    fn obter_flag_retorna_mesmo_arc() {
111        let flag_a = obter_flag();
112        let flag_b = obter_flag();
113        // Ambos devem apontar para o mesmo AtomicBool subjacente
114        assert!(Arc::ptr_eq(&flag_a, &flag_b));
115    }
116
117    #[test]
118    #[serial]
119    fn flag_pode_ser_marcada_e_lida() {
120        let flag = obter_flag();
121        // Apenas verificamos que o AtomicBool funciona corretamente
122        let valor_anterior = flag.load(Ordering::SeqCst);
123        flag.store(valor_anterior, Ordering::SeqCst);
124        assert_eq!(flag.load(Ordering::SeqCst), valor_anterior);
125    }
126
127    #[test]
128    #[serial]
129    fn terminado_false_por_padrao() {
130        // GAP-SSH-TEST-001: serial + reset explícito evita race com testes paralelos.
131        let flag = obter_flag_sigterm();
132        flag.store(false, Ordering::SeqCst);
133        assert!(!terminado());
134    }
135
136    #[test]
137    #[serial]
138    fn obter_flag_sigterm_retorna_mesmo_arc() {
139        let a = obter_flag_sigterm();
140        let b = obter_flag_sigterm();
141        assert!(Arc::ptr_eq(&a, &b));
142    }
143
144    #[test]
145    #[serial]
146    fn terminado_verdadeiro_apos_set() {
147        let flag = obter_flag_sigterm();
148        flag.store(true, Ordering::SeqCst);
149        assert!(terminado());
150        flag.store(false, Ordering::SeqCst); // Reset para não afetar outros testes
151    }
152
153    #[test]
154    #[serial]
155    fn cancelado_false_apos_reset() {
156        let flag = obter_flag();
157        flag.store(true, Ordering::SeqCst);
158        assert!(cancelado());
159        flag.store(false, Ordering::SeqCst);
160        assert!(!cancelado());
161    }
162}