mermaid_cli/app/
lifecycle.rs1use tokio::sync::mpsc;
9
10use crate::domain::{Msg, RuntimeSignal};
11
12pub 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 loop {
43 if tokio::signal::ctrl_c().await.is_err() {
44 break;
45 }
46 if ctrl_c_tx.send(RuntimeSignal::Interrupt).is_err() {
47 break; }
49 }
50 });
51
52 spawn_unix_signal_tasks(tx);
53}
54
55#[cfg(unix)]
56fn spawn_unix_signal_tasks(tx: mpsc::UnboundedSender<RuntimeSignal>) {
57 use tokio::signal::unix::{SignalKind, signal};
58
59 let terminate_tx = tx.clone();
60 tokio::spawn(async move {
61 if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
62 while sigterm.recv().await.is_some() {
63 if terminate_tx.send(RuntimeSignal::Terminate).is_err() {
64 break;
65 }
66 }
67 }
68 });
69
70 tokio::spawn(async move {
71 if let Ok(mut sighup) = signal(SignalKind::hangup()) {
72 while sighup.recv().await.is_some() {
73 if tx.send(RuntimeSignal::Hangup).is_err() {
74 break;
75 }
76 }
77 }
78 });
79}
80
81#[cfg(not(unix))]
82fn spawn_unix_signal_tasks(_tx: mpsc::UnboundedSender<RuntimeSignal>) {}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[tokio::test]
89 async fn lifecycle_wraps_signal_as_reducer_msg() {
90 let (tx, rx) = mpsc::unbounded_channel();
91 let mut lifecycle = RuntimeLifecycle { rx };
92 tx.send(RuntimeSignal::Terminate).expect("send signal");
93
94 let msg = lifecycle.next_msg().await.expect("signal msg");
95 assert!(matches!(msg, Msg::RuntimeSignal(RuntimeSignal::Terminate)));
96 }
97}