Skip to main content

decdn_cli/commands/
interrupt.rs

1//! Graceful Ctrl-C for `decdn fetch` and `decdn bundle pull`.
2//!
3//! The first Ctrl-C resolves `Interrupt::wait`: the command stops its
4//! transfers, writes what it must keep (the bundle skip-cache, the progress
5//! indicators' cleanup), and returns [`Interrupted`], which exits with status
6//! 130. A second Ctrl-C, or one that arrives after the command stops watching,
7//! exits at once with the same status.
8//!
9//! Stopping mid-transfer loses nothing already paid for: every fetch resumes
10//! from its on-disk `.partial` on the next run.
11
12/// The error a command returns when Ctrl-C stopped it. `main` prints a resume
13/// hint and exits with status 130 (128 + SIGINT), the shell convention for an
14/// interrupted command.
15#[derive(Debug)]
16pub struct Interrupted;
17
18impl std::fmt::Display for Interrupted {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        f.write_str("interrupted")
21    }
22}
23
24impl std::error::Error for Interrupted {}
25
26/// The exit status for an interrupted command: 128 + SIGINT.
27pub const INTERRUPTED_EXIT: u8 = 130;
28
29/// The exit status for a fetch that gave up after its no-progress limit:
30/// `EX_TEMPFAIL`, since a rerun resumes it.
31pub const GAVE_UP_EXIT: u8 = 75;
32
33/// A handle that resolves on the first Ctrl-C. Dropping it hands Ctrl-C back
34/// to an immediate exit.
35pub(crate) struct Interrupt {
36    /// Fires on the first Ctrl-C while this handle lives.
37    rx: tokio::sync::oneshot::Receiver<()>,
38    /// Whether `rx` has fired; a oneshot is not polled again after it resolves.
39    fired: bool,
40}
41
42impl Interrupt {
43    /// Start watching Ctrl-C for the rest of the process.
44    pub(crate) fn watch() -> Self {
45        let (tx, rx) = tokio::sync::oneshot::channel();
46        tokio::spawn(async move {
47            let mut tx = Some(tx);
48            // An `Err` means no handler could be installed, so SIGINT keeps its
49            // default action and there is nothing to watch.
50            while tokio::signal::ctrl_c().await.is_ok() {
51                if let Some(tx) = tx.take()
52                    && tx.send(()).is_ok()
53                {
54                    continue;
55                }
56                // Nothing graceful left to do: remove the tab indicator that
57                // `exit` would skip, then quit.
58                super::tab_progress::remove_now();
59                std::process::exit(i32::from(INTERRUPTED_EXIT));
60            }
61        });
62        Self { rx, fired: false }
63    }
64
65    /// A handle fired by the returned sender in place of a Ctrl-C.
66    #[cfg(test)]
67    pub(crate) fn manual() -> (tokio::sync::oneshot::Sender<()>, Self) {
68        let (tx, rx) = tokio::sync::oneshot::channel();
69        (tx, Self { rx, fired: false })
70    }
71
72    /// Resolve once the first Ctrl-C has arrived. Pends forever when no handler
73    /// could be installed.
74    pub(crate) async fn wait(&mut self) {
75        if self.fired {
76            return;
77        }
78        match (&mut self.rx).await {
79            Ok(()) => self.fired = true,
80            Err(_) => std::future::pending::<()>().await,
81        }
82    }
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used)]
87mod tests;