Skip to main content

mermaid_cli/app/
lifecycle.rs

1//! Process lifecycle signal handling.
2//!
3//! Crossterm raw mode turns a typed Ctrl+C into a key event, but OS
4//! signals can still arrive from `kill`, terminal close, or a process
5//! manager. This module converts those signals into reducer messages
6//! so shutdown follows the same path as `/quit`.
7
8use tokio::sync::mpsc;
9
10use crate::domain::{Msg, RuntimeSignal};
11
12/// Small signal stream consumed by the app main loops.
13pub struct RuntimeLifecycle {
14    rx: mpsc::UnboundedReceiver<RuntimeSignal>,
15}
16
17impl RuntimeLifecycle {
18    pub fn new() -> Self {
19        let (tx, rx) = mpsc::unbounded_channel();
20        spawn_signal_tasks(tx);
21        Self { rx }
22    }
23
24    pub async fn next_msg(&mut self) -> Option<Msg> {
25        self.rx.recv().await.map(Msg::RuntimeSignal)
26    }
27}
28
29impl Default for RuntimeLifecycle {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35fn spawn_signal_tasks(tx: mpsc::UnboundedSender<RuntimeSignal>) {
36    let ctrl_c_tx = tx.clone();
37    tokio::spawn(async move {
38        if tokio::signal::ctrl_c().await.is_ok() {
39            let _ = ctrl_c_tx.send(RuntimeSignal::Interrupt);
40        }
41    });
42
43    spawn_unix_signal_tasks(tx);
44}
45
46#[cfg(unix)]
47fn spawn_unix_signal_tasks(tx: mpsc::UnboundedSender<RuntimeSignal>) {
48    use tokio::signal::unix::{SignalKind, signal};
49
50    let terminate_tx = tx.clone();
51    tokio::spawn(async move {
52        if let Ok(mut sigterm) = signal(SignalKind::terminate())
53            && sigterm.recv().await.is_some()
54        {
55            let _ = terminate_tx.send(RuntimeSignal::Terminate);
56        }
57    });
58
59    tokio::spawn(async move {
60        if let Ok(mut sighup) = signal(SignalKind::hangup())
61            && sighup.recv().await.is_some()
62        {
63            let _ = tx.send(RuntimeSignal::Hangup);
64        }
65    });
66}
67
68#[cfg(not(unix))]
69fn spawn_unix_signal_tasks(_tx: mpsc::UnboundedSender<RuntimeSignal>) {}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[tokio::test]
76    async fn lifecycle_wraps_signal_as_reducer_msg() {
77        let (tx, rx) = mpsc::unbounded_channel();
78        let mut lifecycle = RuntimeLifecycle { rx };
79        tx.send(RuntimeSignal::Terminate).expect("send signal");
80
81        let msg = lifecycle.next_msg().await.expect("signal msg");
82        assert!(matches!(msg, Msg::RuntimeSignal(RuntimeSignal::Terminate)));
83    }
84}