mecomp_tui/
termination.rs

1#[cfg(unix)]
2use tokio::signal::unix::signal;
3use tokio::sync::broadcast;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Interrupted {
7    OsSigInt,
8    OsSigQuit,
9    OsSigTerm,
10    UserInt,
11}
12
13#[derive(Debug, Clone)]
14pub struct Terminator {
15    interrupt_tx: broadcast::Sender<Interrupted>,
16}
17
18impl Terminator {
19    #[must_use]
20    pub const fn new(interrupt_tx: broadcast::Sender<Interrupted>) -> Self {
21        Self { interrupt_tx }
22    }
23
24    /// Send an interrupt signal to the application.
25    ///
26    /// # Errors
27    ///
28    /// Fails if the interrupt signal cannot be sent (e.g. the receiver has been dropped)
29    pub fn terminate(&mut self, interrupted: Interrupted) -> anyhow::Result<()> {
30        self.interrupt_tx.send(interrupted)?;
31
32        Ok(())
33    }
34}
35
36#[cfg(unix)]
37async fn terminate_by_unix_signal(mut terminator: Terminator) {
38    let mut interrupt_signal = signal(tokio::signal::unix::SignalKind::interrupt())
39        .expect("failed to create interrupt signal stream");
40    let mut term_signal = signal(tokio::signal::unix::SignalKind::terminate())
41        .expect("failed to create terminate signal stream");
42    let mut quit_signal = signal(tokio::signal::unix::SignalKind::quit())
43        .expect("failed to create quit signal stream");
44
45    tokio::select! {
46        _ = interrupt_signal.recv() => {
47            terminator
48                .terminate(Interrupted::OsSigInt)
49                .expect("failed to send interrupt signal");
50        }
51        _ = term_signal.recv() => {
52            terminator
53                .terminate(Interrupted::OsSigTerm)
54                .expect("failed to send terminate signal");
55        }
56        _ = quit_signal.recv() => {
57            terminator
58                .terminate(Interrupted::OsSigQuit)
59                .expect("failed to send quit signal");
60        }
61    }
62}
63
64// create a broadcast channel for retrieving the application kill signal
65#[allow(clippy::module_name_repetitions)]
66#[must_use]
67pub fn create_termination() -> (Terminator, broadcast::Receiver<Interrupted>) {
68    let (tx, rx) = broadcast::channel(1);
69    let terminator = Terminator::new(tx);
70
71    #[cfg(unix)]
72    tokio::spawn(terminate_by_unix_signal(terminator.clone()));
73
74    (terminator, rx)
75}
76
77#[cfg(test)]
78mod test {
79    use std::time::Duration;
80
81    use super::*;
82    use pretty_assertions::assert_eq;
83    use rstest::rstest;
84
85    #[rstest]
86    #[timeout(Duration::from_secs(1))]
87    #[tokio::test]
88    async fn test_terminate() {
89        let (mut terminator, mut rx) = create_termination();
90
91        terminator
92            .terminate(Interrupted::UserInt)
93            .expect("failed to send interrupt signal");
94
95        assert_eq!(rx.recv().await, Ok(Interrupted::UserInt));
96    }
97}