From b6c12da87f0b9a85d35183cd8b9459733dc7b578 Mon Sep 17 00:00:00 2001 From: Aditya Mane Date: Fri, 8 May 2026 11:57:23 +0530 Subject: [PATCH] Fix Dadb.list() throwing on host with unauthorized/offline devices (#55, #62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `host:devices` returns a mix of `device` and `unauthorized`/`offline` entries, `listDadbs` previously took every serial and called `createDadb` for each, which immediately tries to open `host:features` over a transport the unauthorized device cannot service. The throw aborts the whole `.map` block and `Dadb.list()` returns nothing — even healthy devices on the same host become invisible to dadb-based callers. Two changes: 1. `listDadbs` filters by state (`device` only) and wraps each `createDadb` call in `runCatching` so a single failing entry can't kill the whole returned list. 2. `AdbServerDadb.supportedFeatures` is made `by lazy` so simply constructing an instance no longer eagerly triggers `host:features`. This means a fresh `Dadb` for a healthy device can be constructed without being affected by the state of unrelated devices on the host. Verified end-to-end against Maestro CLI on a host with one `unauthorized` Android phone and one healthy emulator: stock dadb 1.2.9 fails with "Device was requested, but it is not connected" for the healthy emulator; with this patch the same flow runs to completion. Same hardware state, only the library version changed. Fixes #55 Fixes #62 --- .../main/kotlin/dadb/adbserver/AdbServer.kt | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt b/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt index a481f13..5d18382 100644 --- a/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt +++ b/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt @@ -72,17 +72,24 @@ object AdbServer { send(socket, "host:devices") readString(DataInputStream(socket.getInputStream())) } + // Filter the `host:devices` output to entries in `device` state only — + // unauthorized/offline lines have a serial but no usable transport, so + // calling createDadb on them throws and (without per-entry isolation) + // collapses the whole returned list. Wrap each createDadb call in + // runCatching so a single failing entry can't kill the rest. + // Fixes #55 and #62. return output.lines() .filter { it.isNotBlank() } - .mapNotNull { - val parts = it.split("\t") - if (parts.size != 2) { - null - } else { - parts[0] - } + .mapNotNull { line -> + val parts = line.split("\t") + if (parts.size != 2) return@mapNotNull null + val serial = parts[0] + val state = parts[1] + if (state != "device") return@mapNotNull null + runCatching { + createDadb(adbServerHost, adbServerPort, "host:transport:$serial") + }.getOrNull() } - .map { createDadb(adbServerHost, adbServerPort, "host:transport:${it}") } } internal fun readString(inputStream: DataInputStream): String { @@ -128,10 +135,14 @@ private class AdbServerDadb constructor( private val socketTimeout: Int = 0, ) : Dadb { - private val supportedFeatures: Set - - init { - supportedFeatures = open("host:features").use { + // Lazy initialization so that constructing an AdbServerDadb does not + // eagerly trigger a `host:features` query. The eager query throws for + // devices that are unauthorized/offline at construction time, which can + // happen during `listDadbs` enumeration on a host with an unauthorized + // neighbor. Defers the failure to first actual use of supportedFeatures. + // See #55. + private val supportedFeatures: Set by lazy { + open("host:features").use { val features = AdbServer.readString(DataInputStream(it.source.inputStream())) features.split(",").toSet() }