Skip to main content

mtp_rs/
cancel.rs

1//! Cooperative cancellation for long-running MTP operations.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5
6/// Cooperative cancellation handle for long-running MTP operations.
7///
8/// Cheap to clone (`Arc`-backed). `Send + Sync`. Once cancelled, the token
9/// stays cancelled (no reset). Construct a fresh `CancelToken` per
10/// logical operation.
11///
12/// A cheap-to-clone handle that callers pass into operations
13/// like [`Storage::list_objects`](crate::mtp::Storage::list_objects) so they can
14/// be aborted from another task. The operation checks the token between USB
15/// roundtrips; when set, it returns [`Error::Cancelled`](crate::Error::Cancelled)
16/// within one roundtrip's worth of latency instead of running to completion.
17///
18/// # When to use this
19///
20/// MTP list and recursive delete on large folders consist of many small
21/// per-object USB transactions (`GetObjectInfo` per handle). A 1k-photo
22/// `/DCIM/Camera` listing on Android can take 15+ seconds over USB 2.0. Without
23/// cooperative cancel, a user-initiated cancel only stops issuing _new_
24/// transactions after the current bulk call's _entire_ for-loop completes. By
25/// then the device may already be wedged from a follow-up op.
26///
27/// `CancelToken` gives every per-handle iteration a fast way to bail.
28///
29/// # Mid-data-phase cancel
30///
31/// For streaming downloads, see
32/// [`FileDownload::cancel`](crate::mtp::FileDownload::cancel); that path uses
33/// the USB Still Image Class (SIC) class-cancel mechanism to abort a long
34/// bulk-IN transfer mid-stream. Per-handle list/delete operations don't need
35/// that: each `GetObjectInfo` / `DeleteObject` USB roundtrip completes in
36/// milliseconds, so checking the token between roundtrips is sufficient and
37/// safe (no half-finished transfer to drain).
38///
39/// # Example
40///
41/// ```rust,no_run
42/// use mtp_rs::{CancelToken, mtp::MtpDevice};
43///
44/// # async fn example() -> Result<(), mtp_rs::Error> {
45/// let device = MtpDevice::open_first().await?;
46/// let storages = device.storages().await?;
47/// let storage = &storages[0];
48///
49/// let cancel = CancelToken::new();
50///
51/// // Fire the cancel from another task after 500 ms.
52/// let cancel_for_task = cancel.clone();
53/// tokio::spawn(async move {
54///     tokio::time::sleep(std::time::Duration::from_millis(500)).await;
55///     cancel_for_task.cancel();
56/// });
57///
58/// match storage.list_objects_with_cancel(None, Some(&cancel)).await {
59///     Ok(objects) => println!("Listed {} objects", objects.len()),
60///     Err(mtp_rs::Error::Cancelled) => println!("Cancelled mid-listing"),
61///     Err(e) => return Err(e),
62/// }
63/// # Ok(())
64/// # }
65/// ```
66#[derive(Debug, Clone, Default)]
67pub struct CancelToken {
68    cancelled: Arc<AtomicBool>,
69}
70
71impl CancelToken {
72    /// Creates a fresh, not-yet-cancelled token.
73    #[must_use]
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Wraps an externally-owned `Arc<AtomicBool>`.
79    ///
80    /// Use this when the caller already has its own cancellation flag (for
81    /// example, threaded through several layers from a host application) and
82    /// wants the same atomic to drive an mtp-rs operation, with no second
83    /// polling task. The token shares ownership of the inner atomic, so
84    /// flipping the original flag flips the token (and vice versa).
85    #[must_use]
86    pub fn from_arc(cancelled: Arc<AtomicBool>) -> Self {
87        Self { cancelled }
88    }
89
90    /// Marks the token as cancelled. Subsequent [`is_cancelled`] calls return
91    /// `true`. Cooperative consumers (operations holding `Option<&CancelToken>`)
92    /// bail at their next check point with `Err(Error::Cancelled)`.
93    ///
94    /// Idempotent: calling on an already-cancelled token is a no-op.
95    ///
96    /// [`is_cancelled`]: Self::is_cancelled
97    pub fn cancel(&self) {
98        self.cancelled.store(true, Ordering::Release);
99    }
100
101    /// Returns `true` once [`cancel`] has been called on this token (or any
102    /// clone of it).
103    ///
104    /// [`cancel`]: Self::cancel
105    #[must_use]
106    pub fn is_cancelled(&self) -> bool {
107        self.cancelled.load(Ordering::Acquire)
108    }
109}
110
111/// Helper: checks an optional cancel token and returns `Err(Error::Cancelled)`
112/// when set. Use at iteration boundaries inside long operations.
113#[inline]
114pub(crate) fn bail_if_cancelled(token: Option<&CancelToken>) -> Result<(), crate::PtpError> {
115    if let Some(t) = token {
116        if t.is_cancelled() {
117            return Err(crate::PtpError::Cancelled);
118        }
119    }
120    Ok(())
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn new_token_is_not_cancelled() {
129        let token = CancelToken::new();
130        assert!(!token.is_cancelled());
131    }
132
133    #[test]
134    fn cancel_flips_the_flag() {
135        let token = CancelToken::new();
136        token.cancel();
137        assert!(token.is_cancelled());
138    }
139
140    #[test]
141    fn cancel_is_visible_through_clones() {
142        let token = CancelToken::new();
143        let clone = token.clone();
144        token.cancel();
145        assert!(clone.is_cancelled());
146    }
147
148    #[test]
149    fn cancel_is_idempotent() {
150        let token = CancelToken::new();
151        token.cancel();
152        token.cancel();
153        assert!(token.is_cancelled());
154    }
155
156    #[test]
157    fn bail_if_cancelled_returns_ok_for_none() {
158        assert!(bail_if_cancelled(None).is_ok());
159    }
160
161    #[test]
162    fn bail_if_cancelled_returns_ok_for_unset_token() {
163        let token = CancelToken::new();
164        assert!(bail_if_cancelled(Some(&token)).is_ok());
165    }
166
167    #[test]
168    fn bail_if_cancelled_returns_cancelled_for_set_token() {
169        let token = CancelToken::new();
170        token.cancel();
171        let err = bail_if_cancelled(Some(&token)).unwrap_err();
172        assert!(matches!(err, crate::PtpError::Cancelled));
173    }
174}