rskit_cli/signal.rs
1//! Ctrl+C / graceful shutdown over [`tokio_util::sync::CancellationToken`].
2//!
3//! rskit standardizes on `tokio_util`'s [`CancellationToken`] as its single
4//! cooperative-cancellation type: `rskit-worker` handlers and `rskit-process`
5//! already consume it directly, so the CLI layer uses the *same* type end-to-end
6//! rather than introducing a parallel wrapper. The token is re-exported here so a
7//! CLI can name it without depending on `tokio-util` directly, and [`on_ctrl_c`]
8//! installs the one-shot interrupt handler that drives it.
9
10pub use tokio_util::sync::CancellationToken;
11
12/// Install a first-Ctrl+C handler and return the cooperative cancellation token.
13///
14/// Must be called from within a Tokio runtime: a background task awaits the
15/// interrupt signal and cancels the returned token on the first Ctrl+C. Clone the
16/// token and hand it to spawned tasks, an `rskit-worker` handler, or an
17/// `rskit-process` call; holders observe `is_cancelled()` / `cancelled()` and
18/// wind down gracefully.
19#[must_use]
20pub fn on_ctrl_c() -> CancellationToken {
21 let token = CancellationToken::new();
22 let child = token.clone();
23 tokio::spawn(async move {
24 if tokio::signal::ctrl_c().await.is_ok() {
25 child.cancel();
26 }
27 });
28 token
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[tokio::test]
36 async fn cancellation_propagates_to_clones() {
37 let token = CancellationToken::new();
38 let clone = token.clone();
39
40 assert!(!token.is_cancelled());
41 assert!(!clone.is_cancelled());
42
43 token.cancel();
44
45 assert!(token.is_cancelled());
46 assert!(clone.is_cancelled());
47 }
48
49 #[tokio::test]
50 async fn on_ctrl_c_returns_a_live_uncancelled_token() {
51 let token = on_ctrl_c();
52 assert!(!token.is_cancelled());
53 // Cancelling locally still works; the interrupt task is just one source.
54 token.cancel();
55 token.cancelled().await;
56 assert!(token.is_cancelled());
57 }
58}