Skip to main content

torrust_server_lib/
signals.rs

1//! This module contains functions to handle signals.
2use derive_more::Display;
3use tokio_util::sync::CancellationToken;
4use torrust_net_primitives::service_binding::ServiceBinding;
5use tracing::instrument;
6
7/// This is the message that the "launcher" spawned task sends to the main
8/// application process to notify the service was successfully started.
9///
10#[derive(Debug)]
11pub struct Started {
12    pub service_binding: ServiceBinding,
13    pub address: std::net::SocketAddr,
14}
15
16/// This is the message that the "launcher" spawned task receives from the main
17/// application process to notify the service to shutdown.
18///
19#[derive(Copy, Clone, Debug, Display)]
20pub enum Halted {
21    Normal,
22}
23
24/// Resolves when the injected cancellation token is cancelled.
25///
26/// Unlike [`global_shutdown_signal`], this function never subscribes to an
27/// operating-system signal. The caller owns the cancellation lifecycle.
28#[instrument(skip(cancellation_token))]
29pub async fn cancellation_signal(cancellation_token: CancellationToken) {
30    cancellation_token.cancelled().await;
31}
32
33/// Resolves on `ctrl_c` or the `terminate` signal.
34///
35/// # Panics
36///
37/// Will panic if the `ctrl_c` or `terminate` signal resolves with an error.
38#[instrument(skip())]
39pub async fn global_shutdown_signal() {
40    let ctrl_c = async {
41        tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
42    };
43
44    #[cfg(unix)]
45    let terminate = async {
46        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
47            .expect("failed to install signal handler")
48            .recv()
49            .await;
50    };
51
52    #[cfg(not(unix))]
53    let terminate = std::future::pending::<()>();
54
55    tokio::select! {
56        () = ctrl_c => {tracing::warn!("caught interrupt signal (ctrl-c), halting...");},
57        () = terminate => {tracing::warn!("caught interrupt signal (terminate), halting...");}
58    }
59}
60
61/// Resolves when the `stop_receiver` or the `global_shutdown_signal()` resolves.
62///
63/// # Panics
64///
65/// Will panic if the `stop_receiver` resolves with an error.
66#[instrument(skip(rx_halt))]
67pub async fn shutdown_signal(rx_halt: tokio::sync::oneshot::Receiver<Halted>) {
68    let halt = async {
69        match rx_halt.await {
70            Ok(signal) => signal,
71            Err(err) => panic!("Failed to install stop signal: {err}"),
72        }
73    };
74
75    tokio::select! {
76        signal = halt => { tracing::debug!("Halt signal processed: {}", signal) },
77        () = global_shutdown_signal() => { tracing::debug!("Global shutdown signal processed") }
78    }
79}
80
81/// Same as `shutdown_signal()`, but shows a message when it resolves.
82#[instrument(skip(rx_halt))]
83pub async fn shutdown_signal_with_message(rx_halt: tokio::sync::oneshot::Receiver<Halted>, message: String) {
84    shutdown_signal(rx_halt).await;
85
86    tracing::info!("{message}");
87}
88
89#[cfg(test)]
90mod tests {
91    use tokio::sync::oneshot;
92    use tokio_util::sync::CancellationToken;
93
94    use super::{Halted, cancellation_signal, shutdown_signal};
95
96    #[tokio::test]
97    async fn cancellation_signal_should_resolve_when_the_token_is_cancelled() {
98        let cancellation_token = CancellationToken::new();
99        let wait_task = tokio::spawn(cancellation_signal(cancellation_token.clone()));
100
101        tokio::task::yield_now().await;
102
103        assert!(!wait_task.is_finished());
104
105        cancellation_token.cancel();
106
107        wait_task.await.expect("cancellation wait task should complete");
108    }
109
110    #[tokio::test]
111    async fn shutdown_signal_should_resolve_when_it_receives_a_legacy_halted_message() {
112        let (halt_sender, halt_receiver) = oneshot::channel();
113        let wait_task = tokio::spawn(shutdown_signal(halt_receiver));
114
115        halt_sender
116            .send(Halted::Normal)
117            .expect("legacy halt receiver should still be waiting");
118
119        wait_task.await.expect("legacy shutdown wait task should complete");
120    }
121}