Skip to main content

romm_api/core/
interrupt.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3
4use thiserror::Error;
5use tokio::sync::Notify;
6
7use crate::error::{DownloadError, RommError};
8
9#[derive(Debug, Error)]
10#[error("operation cancelled by user")]
11pub struct CancelledByUser;
12
13#[derive(Clone, Debug)]
14pub struct InterruptContext {
15    cancelled: Arc<AtomicBool>,
16    notify: Arc<Notify>,
17}
18
19impl InterruptContext {
20    pub fn new() -> Self {
21        let this = Self {
22            cancelled: Arc::new(AtomicBool::new(false)),
23            notify: Arc::new(Notify::new()),
24        };
25        let watcher = this.clone();
26        tokio::spawn(async move {
27            if tokio::signal::ctrl_c().await.is_ok() {
28                watcher.cancel();
29            }
30        });
31        this
32    }
33
34    pub fn cancel(&self) {
35        self.cancelled.store(true, Ordering::SeqCst);
36        self.notify.notify_waiters();
37    }
38
39    pub fn is_cancelled(&self) -> bool {
40        self.cancelled.load(Ordering::SeqCst)
41    }
42
43    pub async fn cancelled(&self) {
44        if self.is_cancelled() {
45            return;
46        }
47        self.notify.notified().await;
48    }
49}
50
51impl Default for InterruptContext {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57pub fn cancelled_download_error() -> DownloadError {
58    DownloadError::Cancelled(CancelledByUser)
59}
60
61/// Legacy alias used during migration from `anyhow`.
62pub fn cancelled_error() -> DownloadError {
63    cancelled_download_error()
64}
65
66pub fn is_cancelled_download(err: &DownloadError) -> bool {
67    matches!(err, DownloadError::Cancelled(_))
68}
69
70pub fn is_cancelled_error(err: &RommError) -> bool {
71    err.is_cancelled()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn cancelled_error_is_classified() {
80        let err = RommError::from(cancelled_download_error());
81        assert!(is_cancelled_error(&err));
82        assert!(is_cancelled_download(&cancelled_download_error()));
83    }
84
85    #[tokio::test]
86    async fn context_cancel_sets_flag() {
87        let ctx = InterruptContext::new();
88        assert!(!ctx.is_cancelled());
89        ctx.cancel();
90        assert!(ctx.is_cancelled());
91    }
92}