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}