mermaid_cli/app/
lifecycle.rs1use tokio::sync::mpsc;
9
10use mermaid_domain::{Msg, RuntimeSignal};
11
12pub struct RuntimeLifecycle {
14 rx: mpsc::UnboundedReceiver<RuntimeSignal>,
15}
16
17impl RuntimeLifecycle {
18 #[must_use]
19 pub fn new() -> Self {
20 let (tx, rx) = mpsc::unbounded_channel();
21 spawn_signal_tasks(tx);
22 Self { rx }
23 }
24
25 pub async fn next_msg(&mut self) -> Option<Msg> {
26 self.rx.recv().await.map(Msg::RuntimeSignal)
27 }
28}
29
30impl Default for RuntimeLifecycle {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36fn spawn_signal_tasks(tx: mpsc::UnboundedSender<RuntimeSignal>) {
37 let ctrl_c_tx = tx.clone();
38 tokio::spawn(async move {
39 loop {
44 if tokio::signal::ctrl_c().await.is_err() {
45 break;
46 }
47 if ctrl_c_tx.send(RuntimeSignal::Interrupt).is_err() {
48 break; }
50 }
51 });
52
53 spawn_unix_signal_tasks(tx);
54}
55
56#[cfg(unix)]
57fn spawn_unix_signal_tasks(tx: mpsc::UnboundedSender<RuntimeSignal>) {
58 use tokio::signal::unix::{SignalKind, signal};
59
60 let terminate_tx = tx.clone();
61 tokio::spawn(async move {
62 if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
63 while sigterm.recv().await.is_some() {
64 if terminate_tx.send(RuntimeSignal::Terminate).is_err() {
65 break;
66 }
67 }
68 }
69 });
70
71 tokio::spawn(async move {
72 if let Ok(mut sighup) = signal(SignalKind::hangup()) {
73 while sighup.recv().await.is_some() {
74 if tx.send(RuntimeSignal::Hangup).is_err() {
75 break;
76 }
77 }
78 }
79 });
80}
81
82#[cfg(not(unix))]
83fn spawn_unix_signal_tasks(_tx: mpsc::UnboundedSender<RuntimeSignal>) {}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[tokio::test]
90 async fn lifecycle_wraps_signal_as_reducer_msg() {
91 let (tx, rx) = mpsc::unbounded_channel();
92 let mut lifecycle = RuntimeLifecycle { rx };
93 tx.send(RuntimeSignal::Terminate).expect("send signal");
94
95 let msg = lifecycle.next_msg().await.expect("signal msg");
96 assert!(matches!(msg, Msg::RuntimeSignal(RuntimeSignal::Terminate)));
97 }
98}