From 2b651cd56eff9d6e7c6e43a7374fc4fe65c861b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ru=CC=84dolfs=20Bundulis?= Date: Fri, 3 Jul 2026 19:00:37 +0300 Subject: [PATCH 1/4] feat: honour ANDROID_ADB_SERVER_PORT env variable --- dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt b/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt index a481f13..155718d 100644 --- a/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt +++ b/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt @@ -14,6 +14,13 @@ import java.nio.charset.StandardCharsets object AdbServer { + /** + * Default adb server port. Mirrors the adb CLI, which honors ANDROID_ADB_SERVER_PORT and + * otherwise falls back to 5037. + */ + @JvmStatic + val DEFAULT_ADB_SERVER_PORT: Int = System.getenv("ANDROID_ADB_SERVER_PORT")?.toIntOrNull() ?: 5037 + /** * Experimental API * @@ -45,7 +52,7 @@ object AdbServer { @JvmOverloads fun createDadb( adbServerHost: String = "localhost", - adbServerPort: Int = 5037, + adbServerPort: Int = DEFAULT_ADB_SERVER_PORT, deviceQuery: String = "host:transport-any", connectTimeout: Int = 0, socketTimeout: Int = 0 @@ -63,7 +70,7 @@ object AdbServer { @JvmOverloads fun listDadbs( adbServerHost: String = "localhost", - adbServerPort: Int = 5037, + adbServerPort: Int = DEFAULT_ADB_SERVER_PORT, ): List { if (!AdbBinary.tryStartServer(adbServerHost, adbServerPort)) { return emptyList() From 7ae28b951d1c6ffbf7a734fd5845738745e4b1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ru=CC=84dolfs=20Bundulis?= Date: Wed, 9 Sep 2026 19:52:19 +0300 Subject: [PATCH 2/4] fix: handle invalid ANDROID_ADB_SERVER_PORT Co-Authored-By: Claude Opus 5 --- .../main/kotlin/dadb/adbserver/AdbServer.kt | 24 +++++++- .../dadb/adbserver/AdbServerPortTest.kt | 61 +++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 dadb/src/test/kotlin/dadb/adbserver/AdbServerPortTest.kt diff --git a/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt b/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt index 155718d..e578caa 100644 --- a/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt +++ b/dadb/src/main/kotlin/dadb/adbserver/AdbServer.kt @@ -15,11 +15,29 @@ import java.nio.charset.StandardCharsets object AdbServer { /** - * Default adb server port. Mirrors the adb CLI, which honors ANDROID_ADB_SERVER_PORT and - * otherwise falls back to 5037. + * The adb server port used when ANDROID_ADB_SERVER_PORT is not set. + */ + const val STANDARD_ADB_SERVER_PORT = 5037 + + /** + * Default adb server port, honouring ANDROID_ADB_SERVER_PORT like the adb CLI does. + * + * Throws [IllegalArgumentException] if the variable holds something that is not a valid port, + * rather than silently falling back to [STANDARD_ADB_SERVER_PORT]. */ @JvmStatic - val DEFAULT_ADB_SERVER_PORT: Int = System.getenv("ANDROID_ADB_SERVER_PORT")?.toIntOrNull() ?: 5037 + val DEFAULT_ADB_SERVER_PORT: Int + get() = resolveAdbServerPort(System.getenv("ANDROID_ADB_SERVER_PORT")) + + internal fun resolveAdbServerPort(raw: String?): Int { + val trimmed = raw?.trim() + if (trimmed.isNullOrEmpty()) return STANDARD_ADB_SERVER_PORT + val port = trimmed.toIntOrNull() + require(port != null && port in 1..65535) { + "ANDROID_ADB_SERVER_PORT must be a port in 1..65535: got \"$raw\"" + } + return port + } /** * Experimental API diff --git a/dadb/src/test/kotlin/dadb/adbserver/AdbServerPortTest.kt b/dadb/src/test/kotlin/dadb/adbserver/AdbServerPortTest.kt new file mode 100644 index 0000000..00119d7 --- /dev/null +++ b/dadb/src/test/kotlin/dadb/adbserver/AdbServerPortTest.kt @@ -0,0 +1,61 @@ +package dadb.adbserver + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlin.test.assertFailsWith + +internal class AdbServerPortTest { + + @Test + fun unsetFallsBackToStandardPort() { + assertThat(AdbServer.resolveAdbServerPort(null)).isEqualTo(5037) + } + + @Test + fun emptyFallsBackToStandardPort() { + assertThat(AdbServer.resolveAdbServerPort("")).isEqualTo(5037) + } + + @Test + fun blankFallsBackToStandardPort() { + assertThat(AdbServer.resolveAdbServerPort(" ")).isEqualTo(5037) + } + + @Test + fun portIsParsed() { + assertThat(AdbServer.resolveAdbServerPort("5039")).isEqualTo(5039) + } + + @Test + fun surroundingWhitespaceIsIgnored() { + assertThat(AdbServer.resolveAdbServerPort(" 5039 ")).isEqualTo(5039) + assertThat(AdbServer.resolveAdbServerPort("\t5039\n")).isEqualTo(5039) + } + + @Test + fun boundaryPortsAreAccepted() { + assertThat(AdbServer.resolveAdbServerPort("1")).isEqualTo(1) + assertThat(AdbServer.resolveAdbServerPort("65535")).isEqualTo(65535) + } + + @Test + fun nonNumericIsRejected() { + assertFailsWith { AdbServer.resolveAdbServerPort("not-a-port") } + assertFailsWith { AdbServer.resolveAdbServerPort("5039abc") } + } + + @Test + fun outOfRangeIsRejected() { + assertFailsWith { AdbServer.resolveAdbServerPort("0") } + assertFailsWith { AdbServer.resolveAdbServerPort("-1") } + assertFailsWith { AdbServer.resolveAdbServerPort("65536") } + assertFailsWith { AdbServer.resolveAdbServerPort("99999") } + } + + @Test + fun rejectionMessageIncludesTheRawValue() { + val error = assertFailsWith { AdbServer.resolveAdbServerPort(" 99999 ") } + assertThat(error).hasMessageThat().contains("ANDROID_ADB_SERVER_PORT") + assertThat(error).hasMessageThat().contains(" 99999 ") + } +} From 9f6097bbf8c613768e904a1ed4459c7a8be29f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ru=CC=84dolfs=20Bundulis?= Date: Thu, 1 Oct 2026 07:23:34 -0400 Subject: [PATCH 3/4] feat: honour ADB_LOCAL_TRANSPORT_MAX_PORT when scanning for emulators Mirrors the adb client: the variable bounds the emulator port scan, and a bound below the first emulator port disables it entirely. Invalid values are ignored rather than rejected, as adb does. --- dadb/src/main/kotlin/dadb/Dadb.kt | 16 ++++- .../test/kotlin/dadb/EmulatorScanPortTest.kt | 58 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 dadb/src/test/kotlin/dadb/EmulatorScanPortTest.kt diff --git a/dadb/src/main/kotlin/dadb/Dadb.kt b/dadb/src/main/kotlin/dadb/Dadb.kt index 0fd68ba..e501172 100644 --- a/dadb/src/main/kotlin/dadb/Dadb.kt +++ b/dadb/src/main/kotlin/dadb/Dadb.kt @@ -240,6 +240,19 @@ interface Dadb : AutoCloseable { private const val MIN_EMULATOR_PORT = 5555 private const val MAX_EMULATOR_PORT = 5683 + /** + * Upper bound of the emulator port scan, honouring ADB_LOCAL_TRANSPORT_MAX_PORT like the + * adb client does. adb ignores invalid values here rather than failing, and a bound below + * [MIN_EMULATOR_PORT] disables the scan. + */ + internal fun resolveMaxEmulatorPort(raw: String?): Int { + val trimmed = raw?.trim() + if (trimmed.isNullOrEmpty()) return MAX_EMULATOR_PORT + val port = trimmed.toIntOrNull() + if (port == null || port !in 0..65535) return MAX_EMULATOR_PORT + return port + } + @JvmStatic @JvmOverloads fun create(host: String, port: Int, keyPair: AdbKeyPair? = AdbKeyPair.readDefault(), connectTimeout: Int = 0, socketTimeout: Int = 0, keepAlive: Boolean = false): Dadb = DadbImpl(host, port, keyPair, connectTimeout, socketTimeout, keepAlive = keepAlive) @@ -256,7 +269,8 @@ interface Dadb : AutoCloseable { val dadbs = AdbServer.listDadbs(adbServerHost = host) if (dadbs.isNotEmpty()) return dadbs - return (MIN_EMULATOR_PORT .. MAX_EMULATOR_PORT).mapNotNull { port -> + val maxEmulatorPort = resolveMaxEmulatorPort(System.getenv("ADB_LOCAL_TRANSPORT_MAX_PORT")) + return (MIN_EMULATOR_PORT .. maxEmulatorPort).mapNotNull { port -> val dadb = create(host, port, keyPair, connectTimeout, socketTimeout, keepAlive = keepAlive) val response = try { dadb.shell("echo success").allOutput diff --git a/dadb/src/test/kotlin/dadb/EmulatorScanPortTest.kt b/dadb/src/test/kotlin/dadb/EmulatorScanPortTest.kt new file mode 100644 index 0000000..b0af839 --- /dev/null +++ b/dadb/src/test/kotlin/dadb/EmulatorScanPortTest.kt @@ -0,0 +1,58 @@ +package dadb + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +internal class EmulatorScanPortTest { + + @Test + fun unsetKeepsDefaultBound() { + assertThat(Dadb.resolveMaxEmulatorPort(null)).isEqualTo(5683) + } + + @Test + fun emptyKeepsDefaultBound() { + assertThat(Dadb.resolveMaxEmulatorPort("")).isEqualTo(5683) + assertThat(Dadb.resolveMaxEmulatorPort(" ")).isEqualTo(5683) + } + + @Test + fun boundIsParsed() { + assertThat(Dadb.resolveMaxEmulatorPort("5565")).isEqualTo(5565) + } + + @Test + fun surroundingWhitespaceIsIgnored() { + assertThat(Dadb.resolveMaxEmulatorPort(" 5565 ")).isEqualTo(5565) + assertThat(Dadb.resolveMaxEmulatorPort("\t5565\n")).isEqualTo(5565) + } + + @Test + fun invalidValuesAreIgnoredRatherThanRejected() { + assertThat(Dadb.resolveMaxEmulatorPort("not-a-port")).isEqualTo(5683) + assertThat(Dadb.resolveMaxEmulatorPort("5565abc")).isEqualTo(5683) + assertThat(Dadb.resolveMaxEmulatorPort("-1")).isEqualTo(5683) + assertThat(Dadb.resolveMaxEmulatorPort("65536")).isEqualTo(5683) + } + + @Test + fun boundaryValuesAreAccepted() { + assertThat(Dadb.resolveMaxEmulatorPort("0")).isEqualTo(0) + assertThat(Dadb.resolveMaxEmulatorPort("65535")).isEqualTo(65535) + } + + @Test + fun boundBelowFirstEmulatorPortYieldsAnEmptyScan() { + listOf("0", "1", "5554").forEach { raw -> + val range = 5555..Dadb.resolveMaxEmulatorPort(raw) + assertThat(range.isEmpty()).isTrue() + } + } + + @Test + fun boundNarrowsTheScanRange() { + val range = 5555..Dadb.resolveMaxEmulatorPort("5565") + assertThat(range.toList()).hasSize(11) + assertThat(range.last).isEqualTo(5565) + } +} From baa50559056bb940b94f69ac236e16e3c4816fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ru=CC=84dolfs=20Bundulis?= Date: Thu, 1 Oct 2026 07:23:34 -0400 Subject: [PATCH 4/4] fix: skip emulator scan when a non-default local adb server is configured The scan reaches emulator ports directly and bypasses the adb server, so a worker with ANDROID_ADB_SERVER_PORT set could attach to an emulator owned by another worker's server. Only applies to loopback hosts; a remote host still scans. --- dadb/src/main/kotlin/dadb/Dadb.kt | 12 ++++++++ .../kotlin/dadb/EmulatorScanPolicyTest.kt | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 dadb/src/test/kotlin/dadb/EmulatorScanPolicyTest.kt diff --git a/dadb/src/main/kotlin/dadb/Dadb.kt b/dadb/src/main/kotlin/dadb/Dadb.kt index e501172..a716903 100644 --- a/dadb/src/main/kotlin/dadb/Dadb.kt +++ b/dadb/src/main/kotlin/dadb/Dadb.kt @@ -253,6 +253,16 @@ interface Dadb : AutoCloseable { return port } + private fun isLoopbackHost(host: String) = + host == "localhost" || host == "127.0.0.1" || host == "::1" + + /** + * Whether the emulator scan should run. Scanning bypasses the adb server entirely, so it is + * skipped when a non-default local server was explicitly asked for. + */ + internal fun shouldScanForEmulators(host: String, adbServerPort: Int) = + !isLoopbackHost(host) || adbServerPort == AdbServer.STANDARD_ADB_SERVER_PORT + @JvmStatic @JvmOverloads fun create(host: String, port: Int, keyPair: AdbKeyPair? = AdbKeyPair.readDefault(), connectTimeout: Int = 0, socketTimeout: Int = 0, keepAlive: Boolean = false): Dadb = DadbImpl(host, port, keyPair, connectTimeout, socketTimeout, keepAlive = keepAlive) @@ -269,6 +279,8 @@ interface Dadb : AutoCloseable { val dadbs = AdbServer.listDadbs(adbServerHost = host) if (dadbs.isNotEmpty()) return dadbs + if (!shouldScanForEmulators(host, AdbServer.DEFAULT_ADB_SERVER_PORT)) return emptyList() + val maxEmulatorPort = resolveMaxEmulatorPort(System.getenv("ADB_LOCAL_TRANSPORT_MAX_PORT")) return (MIN_EMULATOR_PORT .. maxEmulatorPort).mapNotNull { port -> val dadb = create(host, port, keyPair, connectTimeout, socketTimeout, keepAlive = keepAlive) diff --git a/dadb/src/test/kotlin/dadb/EmulatorScanPolicyTest.kt b/dadb/src/test/kotlin/dadb/EmulatorScanPolicyTest.kt new file mode 100644 index 0000000..257c71a --- /dev/null +++ b/dadb/src/test/kotlin/dadb/EmulatorScanPolicyTest.kt @@ -0,0 +1,29 @@ +package dadb + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +internal class EmulatorScanPolicyTest { + + @Test + fun defaultServerStillScans() { + assertThat(Dadb.shouldScanForEmulators("localhost", 5037)).isTrue() + } + + @Test + fun customLocalServerSkipsTheScan() { + assertThat(Dadb.shouldScanForEmulators("localhost", 5039)).isFalse() + } + + @Test + fun loopbackSpellingsAreTreatedAsLocal() { + assertThat(Dadb.shouldScanForEmulators("127.0.0.1", 5039)).isFalse() + assertThat(Dadb.shouldScanForEmulators("::1", 5039)).isFalse() + } + + @Test + fun remoteHostsStillScanRegardlessOfTheConfiguredPort() { + assertThat(Dadb.shouldScanForEmulators("10.0.0.5", 5039)).isTrue() + assertThat(Dadb.shouldScanForEmulators("10.0.0.5", 5037)).isTrue() + } +}