Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions idevice/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 0 additions & 19 deletions idevice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F>(fut: F)
where
F: std::future::Future<Output = ()> + 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;

Expand Down
16 changes: 6 additions & 10 deletions idevice/src/services/afc/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,19 @@ 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<Box<InnerFileDescriptor<'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<Box<OwnedInnerFileDescriptor>>,
Expand Down
35 changes: 3 additions & 32 deletions idevice/src/services/afc/inner_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});
Expand Down
4 changes: 3 additions & 1 deletion idevice/src/services/crashreportcopymobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 26 additions & 2 deletions tests/src/afc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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!(
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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: {:?}",
Expand All @@ -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: {:?}",
Expand All @@ -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: {:?}",
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {}",
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()) {
Expand Down
2 changes: 2 additions & 0 deletions tools/src/afc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvi
.expect("Failed to open");

let res = file.read_entire().await.expect("Failed to read");
file.close().await.expect("Failed to close");
tokio::fs::write(save, res)
.await
.expect("Failed to write to file");
Expand All @@ -169,6 +170,7 @@ pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvi
file.write_entire(&bytes)
.await
.expect("Failed to upload bytes");
file.close().await.expect("Failed to close");
}
"remove" => {
let path = sub_args.next_argument::<String>().expect("No path passed");
Expand Down
Loading