From be05c8a42c5817acdcc1ba7cb86ef911e64ab449 Mon Sep 17 00:00:00 2001 From: Max Larsson Date: Wed, 6 May 2026 22:06:33 +0200 Subject: [PATCH] Remove rt-multi-thread dependency from AFC file descriptor drop AFC's close-on-drop used block_in_place + block_on to synchronously send a FileClose packet, requiring tokio's rt-multi-thread feature. This is heavyweight for a best-effort cleanup that already did nothing on wasm and single-threaded runtimes. Replace with a simple no-op drop that warns (debug_assert + println) if .close().await wasn't called. The device reclaims FDs when the AFC session ends regardless. Also fix the afc tool to explicitly close file descriptors after use. --- idevice/Cargo.toml | 7 ++-- idevice/src/lib.rs | 19 ---------- idevice/src/services/afc/file.rs | 16 ++++----- idevice/src/services/afc/inner_file.rs | 35 ++----------------- idevice/src/services/crashreportcopymobile.rs | 4 ++- tests/src/afc.rs | 28 +++++++++++++-- tools/src/afc.rs | 2 ++ 7 files changed, 43 insertions(+), 68 deletions(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 9cb2b2c3..bf29405b 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -77,11 +77,10 @@ web-time = { version = "1", optional = true } [target.'cfg(not(windows))'.dependencies] libc = "0.2" -# Native-only tokio features: `fs` (used by `utils::installation` helpers) and -# `rt-multi-thread` (used by AFC's close-on-drop fallback). Both are rejected -# by tokio on wasm32-unknown-unknown. +# Native-only tokio feature: `fs` is used by `utils::installation` helpers. +# Rejected by tokio on wasm32-unknown-unknown. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { version = "1", features = ["fs", "rt-multi-thread"] } +tokio = { version = "1", features = ["fs"] } # wasm32-unknown-unknown: forward `js` / `wasm_js` features through to the # transitive `getrandom` crates so Web Crypto is used for randomness, and diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index c218df42..bbe214a8 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -42,25 +42,6 @@ pub(crate) mod time { pub use wasmtimer::tokio::*; } -/// Spawn a `'static + Send` future on whatever async executor is current. -/// `tokio::spawn` on native; `wasm_bindgen_futures::spawn_local` on wasm32 -/// (the latter doesn't require Send but accepts Send futures fine). Returns -/// `()` so callers don't see a JoinHandle — for tasks that need cancellation, -/// pair the spawn with an explicit `tokio::sync::Notify` / channel signal. -#[allow(dead_code)] -pub(crate) fn spawn(fut: F) -where - F: std::future::Future + Send + 'static, -{ - #[cfg(not(target_arch = "wasm32"))] - { - tokio::spawn(fut); - } - #[cfg(target_arch = "wasm32")] - { - wasm_bindgen_futures::spawn_local(fut); - } -} #[cfg(any(feature = "core_device_proxy", feature = "remote_pairing"))] pub mod tunnel; diff --git a/idevice/src/services/afc/file.rs b/idevice/src/services/afc/file.rs index d91a2d04..15025bc5 100644 --- a/idevice/src/services/afc/file.rs +++ b/idevice/src/services/afc/file.rs @@ -14,11 +14,9 @@ use crate::{ /// Handle for an open file on the device. /// -/// This handle implements **async `Drop`** to automatically close the file for users who forget, -/// but only on a **Tokio multi-threaded runtime** -/// -/// -/// Consider calling `.close()`, it won't drop if expliclity closed +/// Does **not** close the file descriptor on drop. Callers must invoke +/// [`.close()`](Self::close) to release the device-side FD. +/// The FD is reclaimed automatically when the AFC session ends. #[derive(Debug)] pub struct FileDescriptor<'a> { inner: Pin>>, @@ -26,11 +24,9 @@ pub struct FileDescriptor<'a> { /// Owned handle for an open file on the device. /// -/// This handle implements **async `Drop`** to automatically close the file for users who forget, -/// but only on a **Tokio multi-threaded runtime** -/// -/// -/// Consider calling `.close()`, it won't drop if expliclity closed +/// Does **not** close the file descriptor on drop. Callers must invoke +/// [`.close()`](Self::close) to release the device-side FD. +/// The FD is reclaimed automatically when the AFC session ends. #[derive(Debug)] pub struct OwnedFileDescriptor { inner: Pin>, diff --git a/idevice/src/services/afc/inner_file.rs b/idevice/src/services/afc/inner_file.rs index f5cd0a8f..b85d7663 100644 --- a/idevice/src/services/afc/inner_file.rs +++ b/idevice/src/services/afc/inner_file.rs @@ -444,39 +444,10 @@ crate::impl_trait_to_structs!(AsyncSeek for InnerFileDescriptor<'_>, OwnedInnerF crate::impl_trait_to_structs!(Drop for InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; { fn drop(&mut self) { + self.pending_fut = None; if !self.dropped { - // The pending_fut (if Some) holds a Pin<&mut Self> derived from a - // raw pointer to this struct. Dropping it here ensures that - // mutable reference is released before we create a second one via - // Pin::new_unchecked(self) below. Two live &mut Self to the same - // struct is UB under Stacked Borrows even if neither is actively - // dereferenced. - self.pending_fut = None; - - // Best-effort close-on-drop only works on a multi-thread tokio - // runtime. On wasm32 there's no such runtime; on a current-thread - // runtime `block_in_place` would panic. In both cases the caller - // must invoke `.close().await` explicitly to release the FD. - #[cfg(not(target_arch = "wasm32"))] - { - let handle = tokio::runtime::Handle::current(); - - if matches!( - handle.runtime_flavor(), - tokio::runtime::RuntimeFlavor::CurrentThread - ) { - return; - } - - tokio::task::block_in_place(move || { - handle.block_on(async move { - unsafe { Pin::new_unchecked(self) } - .close_inner() - .await - .ok(); - }) - }); - } + debug_assert!(false, "AFC file descriptor for {:?} dropped without calling .close().await", self.path); + println!("error: AFC file descriptor dropped without calling .close().await ({})", self.path); } } }); diff --git a/idevice/src/services/crashreportcopymobile.rs b/idevice/src/services/crashreportcopymobile.rs index 5b7912ee..785d848b 100644 --- a/idevice/src/services/crashreportcopymobile.rs +++ b/idevice/src/services/crashreportcopymobile.rs @@ -85,7 +85,9 @@ impl CrashReportCopyMobileClient { .open(format!("/{log}"), crate::afc::opcode::AfcFopenMode::RdOnly) .await?; - f.read_entire().await + let data = f.read_entire().await?; + f.close().await?; + Ok(data) } /// Removes a specified crash log file from the device. diff --git a/tests/src/afc.rs b/tests/src/afc.rs index a3eb176f..79002a5d 100644 --- a/tests/src/afc.rs +++ b/tests/src/afc.rs @@ -42,10 +42,12 @@ async fn roundtrip( { let mut f = client.open(path, AfcFopenMode::WrOnly).await?; f.write_all(data).await?; + f.close().await?; } let mut f = client.open(path, AfcFopenMode::RdOnly).await?; let mut buf = Vec::new(); f.read_to_end(&mut buf).await?; + f.close().await?; if buf != data { return Err(idevice::IdeviceError::UnexpectedResponse(format!( "roundtrip mismatch: wrote {} bytes, read {} bytes, content differs", @@ -147,10 +149,12 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur { let mut f = client.open(&path, AfcFopenMode::WrOnly).await?; f.write_entire(data).await?; + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::RdOnly).await?; let got = f.read_entire().await?; + f.close().await?; if got != data { return Err(idevice::IdeviceError::UnexpectedResponse( "read_entire content mismatch".into(), @@ -172,11 +176,13 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur { let mut f = client.open(&path, AfcFopenMode::WrOnly).await?; f.write_all(data).await?; + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::RdOnly).await?; let mut buf = Vec::new(); f.read_to_end(&mut buf).await?; + f.close().await?; if buf != data { return Err(idevice::IdeviceError::UnexpectedResponse( "async read content mismatch".into(), @@ -191,13 +197,15 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur run_test!("afc: zero-length file", success, failure, async { let path = p("empty.bin"); { - let _ = client.open(&path, AfcFopenMode::WrOnly).await?; - // drop without writing — file exists but is empty + let f = client.open(&path, AfcFopenMode::WrOnly).await?; + // close without writing — file exists but is empty + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::RdOnly).await?; let mut buf = Vec::new(); let n = f.read_to_end(&mut buf).await?; + f.close().await?; if n != 0 { return Err(idevice::IdeviceError::UnexpectedResponse( "expected 0 bytes from empty file".into(), @@ -214,16 +222,19 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur let mut f = client.open(&path, AfcFopenMode::WrOnly).await?; f.write_all(b"first\n").await?; f.flush().await?; + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::Append).await?; f.write_all(b"second\n").await?; f.flush().await?; + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::RdOnly).await?; let mut buf = Vec::new(); f.read_to_end(&mut buf).await?; + f.close().await?; let s = String::from_utf8_lossy(&buf); if s != "first\nsecond\n" { return Err(idevice::IdeviceError::UnexpectedResponse(format!( @@ -277,11 +288,13 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur { let mut f = client.open(&path, AfcFopenMode::WrOnly).await?; f.write_all(data).await?; + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::RdOnly).await?; f.seek(std::io::SeekFrom::Start(10)).await?; let pos = f.seek_tell().await?; + f.close().await?; if pos != 10 { return Err(idevice::IdeviceError::UnexpectedResponse(format!( "seek_tell returned {pos}, expected 10" @@ -309,6 +322,7 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur } let mut tail = [0u8; 5]; f.read_exact(&mut tail).await?; // reads 5..10 + f.close().await?; if &tail != b"56789" { return Err(idevice::IdeviceError::UnexpectedResponse(format!( "seek_current read wrong bytes: {:?}", @@ -329,6 +343,7 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur f.seek(std::io::SeekFrom::End(-5)).await?; let mut tail = Vec::new(); f.read_to_end(&mut tail).await?; + f.close().await?; if &tail != b"BCDEF" { return Err(idevice::IdeviceError::UnexpectedResponse(format!( "seek_end wrong bytes: {:?}", @@ -347,11 +362,13 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur f.write_all(b"start").await?; f.seek(std::io::SeekFrom::Start(0)).await?; f.write_all(b"over").await?; + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::RdOnly).await?; let mut buf = Vec::new(); f.read_to_end(&mut buf).await?; + f.close().await?; if &buf != b"overt" { return Err(idevice::IdeviceError::UnexpectedResponse(format!( "seek overwrite wrong content: {:?}", @@ -383,6 +400,7 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur } let mut rest = Vec::new(); f.read_to_end(&mut rest).await?; + f.close().await?; if rest != b"fghijklmnopqrstuvwxyz" { return Err(idevice::IdeviceError::UnexpectedResponse( "partial read tail mismatch".into(), @@ -413,6 +431,7 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur } collected.push(b[0]); } + f.close().await?; if collected != data { return Err(idevice::IdeviceError::UnexpectedResponse( "one-byte read mismatch".into(), @@ -474,11 +493,13 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur let chunk = vec![i; 100]; f.write_all(&chunk).await?; } + f.close().await?; } { let mut f = client.open(&path, AfcFopenMode::RdOnly).await?; let mut buf = Vec::new(); f.read_to_end(&mut buf).await?; + f.close().await?; if buf.len() != 1000 { return Err(idevice::IdeviceError::UnexpectedResponse(format!( "expected 1000 bytes, got {}", @@ -523,6 +544,7 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur let mut f = client.open(&dst, AfcFopenMode::RdOnly).await?; let mut buf = Vec::new(); f.read_to_end(&mut buf).await?; + f.close().await?; if buf != data { return Err(idevice::IdeviceError::UnexpectedResponse( "renamed file content mismatch".into(), @@ -586,10 +608,12 @@ pub async fn run_tests(provider: &dyn IdeviceProvider, success: &mut u32, failur { let mut fa = client.open(&f1, AfcFopenMode::WrOnly).await?; fa.write_all(b"a").await?; + fa.close().await?; } { let mut fb = client.open(&f2, AfcFopenMode::WrOnly).await?; fb.write_all(b"b").await?; + fb.close().await?; } let entries = client.list_dir(&dir).await?; if !entries.contains(&"a.txt".to_string()) || !entries.contains(&"b.txt".to_string()) { diff --git a/tools/src/afc.rs b/tools/src/afc.rs index f08719bd..a7721993 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -152,6 +152,7 @@ pub async fn main(arguments: &CollectedArguments, provider: Box { let path = sub_args.next_argument::().expect("No path passed");