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#[deprecated(
20    since = "0.5.0",
21    note = "inject a `tokio_util::sync::CancellationToken` instead of sending `Halted` over a oneshot channel; removal is planned for torrust-server-lib 0.6.0"
22)]
23#[derive(Copy, Clone, Debug, Display)]
24pub enum Halted {
25    Normal,
26}
27
28/// Resolves when the injected cancellation token is cancelled.
29///
30/// Unlike [`global_shutdown_signal`], this function never subscribes to an
31/// operating-system signal. The caller owns the cancellation lifecycle.
32#[instrument(skip(cancellation_token))]
33pub async fn cancellation_signal(cancellation_token: CancellationToken) {
34    cancellation_token.cancelled().await;
35}
36
37/// Resolves on `ctrl_c` or the `terminate` signal.
38///
39/// # Panics
40///
41/// Will panic if the `ctrl_c` or `terminate` signal resolves with an error.
42#[deprecated(
43    since = "0.5.0",
44    note = "libraries must not subscribe to SIGINT/SIGTERM; the executable should subscribe and cancel a `CancellationToken` passed to `cancellation_signal`; removal is planned for torrust-server-lib 0.6.0"
45)]
46#[instrument(skip())]
47pub async fn global_shutdown_signal() {
48    let ctrl_c = async {
49        tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
50    };
51
52    #[cfg(unix)]
53    let terminate = async {
54        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
55            .expect("failed to install signal handler")
56            .recv()
57            .await;
58    };
59
60    #[cfg(not(unix))]
61    let terminate = std::future::pending::<()>();
62
63    tokio::select! {
64        () = ctrl_c => {tracing::warn!("caught interrupt signal (ctrl-c), halting...");},
65        () = terminate => {tracing::warn!("caught interrupt signal (terminate), halting...");}
66    }
67}
68
69/// Resolves when the `stop_receiver` or the `global_shutdown_signal()` resolves.
70///
71/// # Panics
72///
73/// Will panic if the `stop_receiver` resolves with an error.
74#[deprecated(
75    since = "0.5.0",
76    note = "use `cancellation_signal` with an injected `CancellationToken`; removal is planned for torrust-server-lib 0.6.0"
77)]
78#[allow(
79    deprecated,
80    reason = "the legacy function is implemented with the other legacy symbols until they are removed"
81)]
82#[instrument(skip(rx_halt))]
83pub async fn shutdown_signal(rx_halt: tokio::sync::oneshot::Receiver<Halted>) {
84    let halt = async {
85        match rx_halt.await {
86            Ok(signal) => signal,
87            Err(err) => panic!("Failed to install stop signal: {err}"),
88        }
89    };
90
91    tokio::select! {
92        signal = halt => { tracing::debug!("Halt signal processed: {}", signal) },
93        () = global_shutdown_signal() => { tracing::debug!("Global shutdown signal processed") }
94    }
95}
96
97/// Same as `shutdown_signal()`, but shows a message when it resolves.
98#[deprecated(
99    since = "0.5.0",
100    note = "use `cancellation_signal` with an injected `CancellationToken` and log the message yourself; removal is planned for torrust-server-lib 0.6.0"
101)]
102#[allow(
103    deprecated,
104    reason = "the legacy function is implemented with the other legacy symbols until they are removed"
105)]
106#[instrument(skip(rx_halt))]
107pub async fn shutdown_signal_with_message(rx_halt: tokio::sync::oneshot::Receiver<Halted>, message: String) {
108    shutdown_signal(rx_halt).await;
109
110    tracing::info!("{message}");
111}
112
113#[cfg(test)]
114mod tests {
115    use tokio::sync::oneshot;
116    use tokio_util::sync::CancellationToken;
117
118    #[allow(deprecated, reason = "the test covers the deprecated legacy API until it is removed")]
119    use super::{Halted, cancellation_signal, shutdown_signal};
120
121    #[tokio::test]
122    async fn cancellation_signal_should_resolve_when_the_token_is_cancelled() {
123        let cancellation_token = CancellationToken::new();
124        let wait_task = tokio::spawn(cancellation_signal(cancellation_token.clone()));
125
126        tokio::task::yield_now().await;
127
128        assert!(!wait_task.is_finished());
129
130        cancellation_token.cancel();
131
132        wait_task.await.expect("cancellation wait task should complete");
133    }
134
135    #[allow(deprecated, reason = "the test covers the deprecated legacy API until it is removed")]
136    #[tokio::test]
137    async fn shutdown_signal_should_resolve_when_it_receives_a_legacy_halted_message() {
138        let (halt_sender, halt_receiver) = oneshot::channel();
139        let wait_task = tokio::spawn(shutdown_signal(halt_receiver));
140
141        halt_sender
142            .send(Halted::Normal)
143            .expect("legacy halt receiver should still be waiting");
144
145        wait_task.await.expect("legacy shutdown wait task should complete");
146    }
147}