Skip to content

Make Android ADB async and talk to the ADB server over its smartsocket protocol - #8

Merged
jkelleyrtp merged 9 commits into
mainfrom
devin/1787339789-adb-socket-transport
Aug 21, 2026
Merged

Make Android ADB async and talk to the ADB server over its smartsocket protocol#8
jkelleyrtp merged 9 commits into
mainfrom
devin/1787339789-adb-socket-transport

Conversation

@jkelleyrtp

@jkelleyrtp jkelleyrtp commented Aug 21, 2026

Copy link
Copy Markdown
Member

Summary

Two changes to the Android backend, previously split across #7 and this PR and now combined here.

1. ADB calls are async with a timeout. Every one-shot ADB invocation used std::process::Command::output() with no timeout, so each call held a thread until adb exited. All of them are now async and time-bounded (30s default), and the async-ness propagates through AndroidAccessibility, the Android session/serve layers, and the CLI — AccessibilityReader::capture_screen is future-returning on all platforms as a result. Emulator discovery and screenshot writes moved to tokio::fs; the long-lived screenrecord child and the synchronous native paths stayed as they were.

2. AdbClient no longer forks adb at all. It opens a TCP socket to the adb server (127.0.0.1:5037, honoring ANDROID_ADB_SERVER_PORT) and speaks the smartsocket protocol directly, so a tap is a socket round-trip instead of a process, and a timeout cancels by dropping the socket rather than killing a child.

New transport.rs implements the slice we need — hand-rolled rather than pulling in droidrun-adb/adbutils-rs, since it's a small, stable subset:

write   "{len:04x}" + service            // service capped at 1024 bytes
read    "OKAY" | "FAIL" + {len:04x} + msg
shell   host:tport:serial:<s> -> OKAY + 8-byte tid -> shell,v2,raw:<cmd>
binary  host:tport:... -> exec:<cmd>     // unframed, read to EOF

shell v2 packets are [id: u8][len: u32 LE][payload] with ids decoded through a #[repr(u8)] ShellPacketId; unknown ids, packets over 1 MiB, and accumulated output over 64 MiB are rejected before allocating.

Two behavior changes worth knowing:

  • shell exit status now propagates. adb shell never returned the device command's exit code; shell v2 does, so shell()/shell_raw() now error on a non-zero device command instead of silently returning its output.
  • command(&[&str]) is gone. Those were host services, not shell commands, so they're typed now: wait_for_device() (host:wait-for-any-device / host-serial:<s>:wait-for-any-device), server_version(), server_features(). connected_devices() is host:devices.

The adb binary is still used for exactly two things: start-server when nothing is listening (one retry, then an error naming both the missing server and the missing binary), and the long-lived screenrecord child, which is unchanged.

Fallout of (1) and (2): the Android workers no longer need threads of their own. spawn_ax_worker and spawn_input_worker each ran an OS thread hosting a current-thread runtime and block_on-ing every command — scaffolding that only existed because ADB blocked. Both are now plain tokio::spawn tasks on the ambient runtime (the input worker's startup handshake becomes a oneshot rather than a sync_channel), and the two emulator probe examples lost the same nested-runtime pattern.

Testing

Unit tests drive a mock adb server over a real TcpListener: request strings and the 8-byte tid read, host:devices state filtering, stdout/stderr split across packets with exit status, non-zero exit surfacing stderr, FAIL messages, truncated status/payload/header, EOF before the exit packet, oversized declared lengths, unknown packet ids, binary exec: passthrough with embedded NULs, and request timeout. They bind real sockets, so they carry #[cfg_attr(miri, ignore)] — miri's isolation has no socket syscalls — while the framing/parsing tests still run under it.

android-e2e passes against a real emulator in CI, which caught the one bug the mocks couldn't: connected_devices() still did .lines().skip(1) to drop the adb devices CLI banner, but host:devices doesn't send one, so the first (and in CI, only) device was discarded and everything reported "No Android devices connected". The mock had the banner baked into its payload, which hid it; it now sends raw server output.

cargo fmt, clippy -D warnings, cargo test --workspace --lib --bins, and cli_smoke all pass. The smoke tests that simulate a missing adb now also point ANDROID_ADB_SERVER_PORT at a dead port, since a running local server would otherwise satisfy the client without any binary on PATH.

One unrelated fix rides along: CI's clippy flags result_large_err in generated tonic code, so the generated protocol module carries a scoped allow.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/d88fc07ea8064b38a4b4e5a7481ffebc
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/d88fc07ea8064b38a4b4e5a7481ffebc?variant=devin-insiders
Requested by: @jkelleyrtp

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@staging-devin-ai-integration
staging-devin-ai-integration Bot changed the base branch from devin/1787275698-async-adb to main August 21, 2026 21:14
@staging-devin-ai-integration staging-devin-ai-integration Bot changed the title Talk to the ADB server over its smartsocket protocol instead of spawning adb Make Android ADB async and talk to the ADB server over its smartsocket protocol Aug 21, 2026
@jkelleyrtp
jkelleyrtp merged commit df024fb into main Aug 21, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant