From 8eda3ae60f1a858b1c53c91118537bba2207b42a Mon Sep 17 00:00:00 2001 From: Om Narayan Date: Sat, 8 Nov 2025 18:30:22 +0530 Subject: [PATCH 1/2] Add --driver-host-port CLI option to enable concurrent multi-device testing Introduces a new global --driver-host-port option that allows users to specify custom driver ports for both Android and iOS, enabling parallel test execution on multiple devices/simulators without port conflicts. This addresses the limitation where running tests concurrently would fail due to all instances attempting to use the default port. Changes: - Added --driver-host-port global option to App.kt - Modified TestCommand.kt to respect user-specified port in selectPort() - Updated all commands (RecordCommand, StudioCommand, QueryCommand, PrintHierarchyCommand, StartDeviceCommand) to pass custom port to session manager - Works for both Android devices and iOS simulators Usage: maestro --driver-host-port 7005 --device test flow.yaml Credit: This implementation builds upon the approach proposed by @avinash-bharti in #2339, implementing the port configuration feature without the additional session management changes. Resolves #1485, #1853, #2082, #2556, #2703 Related to #1818 --- maestro-cli/src/main/java/maestro/cli/App.kt | 6 ++++++ .../java/maestro/cli/command/PrintHierarchyCommand.kt | 2 +- .../src/main/java/maestro/cli/command/QueryCommand.kt | 2 +- .../src/main/java/maestro/cli/command/RecordCommand.kt | 2 +- .../main/java/maestro/cli/command/StartDeviceCommand.kt | 2 +- .../src/main/java/maestro/cli/command/StudioCommand.kt | 2 +- .../src/main/java/maestro/cli/command/TestCommand.kt | 9 +++++++-- 7 files changed, 18 insertions(+), 7 deletions(-) diff --git a/maestro-cli/src/main/java/maestro/cli/App.kt b/maestro-cli/src/main/java/maestro/cli/App.kt index bb96b4ab6f..30caeda764 100644 --- a/maestro-cli/src/main/java/maestro/cli/App.kt +++ b/maestro-cli/src/main/java/maestro/cli/App.kt @@ -90,6 +90,12 @@ class App { @Option(names = ["--port"], hidden = true) var port: Int? = null + @Option( + names = ["--driver-host-port"], + description = ["AndroidDriver host port for instrumentation communication (default: 7001)"] + ) + var driverHostPort: Int? = null + @Option( names = ["--device", "--udid"], description = ["(Optional) Device ID to run on explicitly, can be a comma separated list of IDs: --device \"Emulator_1,Emulator_2\" "], diff --git a/maestro-cli/src/main/java/maestro/cli/command/PrintHierarchyCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/PrintHierarchyCommand.kt index d3d71337c9..a8341ddd83 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/PrintHierarchyCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/PrintHierarchyCommand.kt @@ -109,7 +109,7 @@ class PrintHierarchyCommand : Runnable { MaestroSessionManager.newSession( host = parent?.host, port = parent?.port, - driverHostPort = null, + driverHostPort = parent?.driverHostPort, teamId = appleTeamId, deviceId = parent?.deviceId, platform = parent?.platform, diff --git a/maestro-cli/src/main/java/maestro/cli/command/QueryCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/QueryCommand.kt index 2c6d016f1c..af1c47207b 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/QueryCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/QueryCommand.kt @@ -73,7 +73,7 @@ class QueryCommand : Runnable { MaestroSessionManager.newSession( host = parent?.host, port = parent?.port, - driverHostPort = null, + driverHostPort = parent?.driverHostPort, deviceId = parent?.deviceId, platform = parent?.platform, teamId = appleTeamId, diff --git a/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt index 59f00c1330..00dbee5fa7 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt @@ -130,7 +130,7 @@ class RecordCommand : Callable { return MaestroSessionManager.newSession( host = parent?.host, port = parent?.port, - driverHostPort = null, + driverHostPort = parent?.driverHostPort, deviceId = deviceId, teamId = appleTeamId, platform = parent?.platform, diff --git a/maestro-cli/src/main/java/maestro/cli/command/StartDeviceCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/StartDeviceCommand.kt index 2f6341c7df..ee30bd1de7 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/StartDeviceCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/StartDeviceCommand.kt @@ -99,7 +99,7 @@ class StartDeviceCommand : Callable { PrintUtils.message(if (p == Platform.IOS) "Launching simulator..." else "Launching emulator...") DeviceService.startDevice( device = device, - driverHostPort = parent?.port + driverHostPort = parent?.driverHostPort ) } } catch (e: LocaleValidationIosException) { diff --git a/maestro-cli/src/main/java/maestro/cli/command/StudioCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/StudioCommand.kt index 6797cc75b6..310f6ae0d7 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/StudioCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/StudioCommand.kt @@ -75,7 +75,7 @@ class StudioCommand : Callable { MaestroSessionManager.newSession( host = parent?.host, port = parent?.port, - driverHostPort = null, + driverHostPort = parent?.driverHostPort, teamId = appleTeamId, deviceId = parent?.deviceId, platform = parent?.platform, diff --git a/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt index 2eac9dae77..fdb2818831 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt @@ -503,11 +503,16 @@ class TestCommand : Callable { } } - private fun selectPort(effectiveShards: Int): Int = - if (effectiveShards == 1) 7001 + private fun selectPort(effectiveShards: Int): Int { + // If user specified driver host port via CLI, use it + parent?.driverHostPort?.let { return it } + + // Otherwise use default behavior + return if (effectiveShards == 1) 7001 else (7001..7128).shuffled().find { port -> usedPorts.putIfAbsent(port, true) == null } ?: error("No available ports found") + } private fun runSingleFlow( maestro: Maestro, From aabc7c9ea199653e30dd148f3eadd755c3469f9e Mon Sep 17 00:00:00 2001 From: Om Narayan Date: Sun, 30 Nov 2025 01:33:01 +0530 Subject: [PATCH 2/2] fix: detect port conflicts and show clear error messages Previously, when port 7001 (or a user-specified port) was already in use, Maestro would fail with obscure connection errors. This made it difficult for users to understand the root cause. This change adds port availability checks before attempting to use a port: - If the default port 7001 is occupied, show a clear error suggesting --driver-host-port - If a user-specified port is occupied, show a clear error asking to use a different port - For sharded runs, skip unavailable ports when selecting from the port range We missed this earlier because our internal tooling checks port availability before passing --driver-host-port to Maestro - apologies for the oversight. Fixes #2703 --- .../java/maestro/cli/command/TestCommand.kt | 20 +++++++++++++++---- .../main/java/maestro/cli/util/SocketUtils.kt | 8 ++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt index fdb2818831..f7a12b0cc3 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt @@ -51,6 +51,7 @@ import maestro.cli.util.CiUtils import maestro.cli.util.EnvUtils import maestro.cli.util.FileUtils.isWebFlow import maestro.cli.util.PrintUtils +import maestro.cli.util.isPortAvailable import maestro.cli.insights.TestAnalysisManager import maestro.cli.view.greenBox import maestro.cli.view.box @@ -505,12 +506,23 @@ class TestCommand : Callable { private fun selectPort(effectiveShards: Int): Int { // If user specified driver host port via CLI, use it - parent?.driverHostPort?.let { return it } + parent?.driverHostPort?.let { port -> + if (!isPortAvailable(port)) { + throw CliError("Port $port is already in use. Please specify a different port with --driver-host-port") + } + return port + } // Otherwise use default behavior - return if (effectiveShards == 1) 7001 - else (7001..7128).shuffled().find { port -> - usedPorts.putIfAbsent(port, true) == null + if (effectiveShards == 1) { + if (!isPortAvailable(7001)) { + throw CliError("Default port 7001 is already in use. Use --driver-host-port to specify a different port") + } + return 7001 + } + + return (7001..7128).shuffled().find { port -> + usedPorts.putIfAbsent(port, true) == null && isPortAvailable(port) } ?: error("No available ports found") } diff --git a/maestro-cli/src/main/java/maestro/cli/util/SocketUtils.kt b/maestro-cli/src/main/java/maestro/cli/util/SocketUtils.kt index fcbab81721..5291b2633c 100644 --- a/maestro-cli/src/main/java/maestro/cli/util/SocketUtils.kt +++ b/maestro-cli/src/main/java/maestro/cli/util/SocketUtils.kt @@ -9,4 +9,12 @@ fun getFreePort(): Int { } catch (ignore: Exception) {} } ServerSocket(0).use { return it.localPort } +} + +fun isPortAvailable(port: Int): Boolean { + return try { + ServerSocket(port).use { true } + } catch (e: Exception) { + false + } } \ No newline at end of file