Skip to main content

harmont_cli/orchestrator/
signal.rs

1//! Bridges OS signals to the orchestrator's `CancellationToken`.
2//!
3//! Today's hm process: a single tokio runtime serving one CLI command.
4//! Ctrl-C should: (1) flip the token so runners drain quickly; (2)
5//! exit with code 130 (sigint).
6
7// Pedantic-bucket nags accepted at module scope:
8// - `exit`: force-exit on second Ctrl-C is the documented UX, matching
9//   the legacy executor. The user has explicitly asked us to die.
10#![allow(clippy::exit)]
11
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use tokio_util::sync::CancellationToken;
16
17/// Spawn a tokio task that listens for SIGINT (Ctrl-C) and flips
18/// the token. Returns a handle; aborting the handle is sufficient
19/// cleanup since the runtime tears down on process exit.
20///
21/// On second Ctrl-C, the task force-exits with code 130 — same UX
22/// as the legacy executor.
23#[must_use = "drop the JoinHandle to leak the listener; bind to a `_` to tie its lifetime to the caller scope"]
24pub fn install_ctrlc(token: CancellationToken) -> tokio::task::JoinHandle<()> {
25    tokio::spawn(async move {
26        let armed = Arc::new(AtomicBool::new(false));
27        loop {
28            match tokio::signal::ctrl_c().await {
29                Ok(()) => {
30                    if armed.swap(true, Ordering::SeqCst) {
31                        tracing::warn!("\nforce-exit on second Ctrl-C");
32                        std::process::exit(130);
33                    }
34                    tracing::info!("\ncancelling… (Ctrl-C again to force)");
35                    token.cancel();
36                }
37                Err(_) => return,
38            }
39        }
40    })
41}