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}