From 2498ccda0f91ce73ea1edf69e86440f4fd37daf6 Mon Sep 17 00:00:00 2001 From: Berik Visschers Date: Wed, 15 Mar 2023 17:39:17 +0100 Subject: [PATCH] Add IdbRunner class to start and stop idb (#903) * Add IdbRunner class to start and stop idb * Tidy-ups --- .../main/java/maestro/cli/idb/IdbCompanion.kt | 120 +-------------- .../cli/session/MaestroSessionManager.kt | 26 ++-- .../src/main/java/maestro/LocalIdbRunner.kt | 139 ++++++++++++++++++ .../src/main/java/ios/idb/IdbIOSDevice.kt | 20 +-- .../src/main/java/ios/idb/IdbRunner.kt | 9 ++ 5 files changed, 165 insertions(+), 149 deletions(-) create mode 100644 maestro-client/src/main/java/maestro/LocalIdbRunner.kt create mode 100644 maestro-ios/src/main/java/ios/idb/IdbRunner.kt diff --git a/maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt b/maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt index ca453c0246..327cce3f69 100644 --- a/maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt +++ b/maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt @@ -1,27 +1,11 @@ package maestro.cli.idb -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.runCatching -import idb.CompanionServiceGrpc -import idb.HIDEventKt -import idb.Idb -import idb.hIDEvent -import idb.point -import io.grpc.ManagedChannel -import io.grpc.ManagedChannelBuilder -import ios.grpc.BlockingStreamObserver import maestro.cli.device.Device -import maestro.debuglog.DebugLogStore import maestro.utils.MaestroTimer import java.net.Socket -import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread object IdbCompanion { - private val logger = DebugLogStore.loggerFor(IdbCompanion::class.java) - - // TODO: Understand why this is a separate method from strartIdbCompanion + // TODO: Understand why this is a separate method from idbRunner.start() fun setup(device: Device.Connected) { val idbProcessBuilder = ProcessBuilder("idb_companion", "--udid", device.instanceId) idbProcessBuilder.start() @@ -32,106 +16,4 @@ object IdbCompanion { Socket(idbHost, idbPort).use { true } } } - - fun startIdbCompanion(host: String, port: Int, deviceId: String): ManagedChannel { - logger.info("startIDBCompanion on $deviceId") - - // idb is associated with a device, it can't be assumed that a running idb_companion is - // associated with the device under test: Shut down before starting a fresh idb if needed. - if (isSocketAvailable(host, port)) { - ProcessBuilder(listOf("killall", "idb_companion")).start().waitFor() - } - - val idbProcessBuilder = ProcessBuilder("idb_companion", "--udid", deviceId) - DebugLogStore.logOutputOf(idbProcessBuilder) - val idbProcess = idbProcessBuilder.start() - - Runtime.getRuntime().addShutdownHook(thread(start = false) { - idbProcess.destroy() - }) - - logger.warning("Waiting for idb service to start..") - MaestroTimer.retryUntilTrue(timeoutMs = 60000, delayMs = 100) { - Socket(host, port).use { true } - } || error("idb_companion did not start in time") - - - // The first time a simulator boots up, it can - // take 10's of seconds to complete. - logger.warning("Waiting for Simulator to boot..") - MaestroTimer.retryUntilTrue(timeoutMs = 120000, delayMs = 100) { - val process = ProcessBuilder("xcrun", "simctl", "bootstatus", deviceId) - .start() - process - .waitFor(1000, TimeUnit.MILLISECONDS) - process.exitValue() == 0 - } || error("Simulator failed to boot") - - val channel = ManagedChannelBuilder.forAddress(host, port) - .usePlaintext() - .build() - - // Test if idb can get accessibility info elements with non-zero frame width - logger.warning("Waiting for successful taps") - MaestroTimer.retryUntilTrue(timeoutMs = 20000, delayMs = 100) { - testPressAction(channel) is Ok - } || error("idb_companion is not able dispatch successful tap events") - - logger.warning("Simulator ready") - - return channel - } - - private fun testPressAction(channel: ManagedChannel): Result { - val x = 0 - val y = 0 - val holdDelay = 50L - val asyncStub = CompanionServiceGrpc.newStub(channel) - - return runCatching { - val responseObserver = BlockingStreamObserver() - val stream = asyncStub.hid(responseObserver) - - val pressAction = HIDEventKt.hIDPressAction { - touch = HIDEventKt.hIDTouch { - point = point { - this.x = x.toDouble() - this.y = y.toDouble() - } - } - } - - stream.onNext( - hIDEvent { - press = HIDEventKt.hIDPress { - action = pressAction - direction = Idb.HIDEvent.HIDDirection.DOWN - } - } - ) - - Thread.sleep(holdDelay) - - stream.onNext( - hIDEvent { - press = HIDEventKt.hIDPress { - action = pressAction - direction = Idb.HIDEvent.HIDDirection.UP - } - } - ) - stream.onCompleted() - - responseObserver.awaitResult() - } - } - - - private fun isSocketAvailable(host: String, port: Int): Boolean { - return try { - Socket(host, port).use { true } - } catch (_: Exception) { - false - } - } } diff --git a/maestro-cli/src/main/java/maestro/cli/session/MaestroSessionManager.kt b/maestro-cli/src/main/java/maestro/cli/session/MaestroSessionManager.kt index 0fb7a2e1ae..353c0edaf3 100644 --- a/maestro-cli/src/main/java/maestro/cli/session/MaestroSessionManager.kt +++ b/maestro-cli/src/main/java/maestro/cli/session/MaestroSessionManager.kt @@ -26,12 +26,12 @@ import ios.LocalIOSDevice import ios.idb.IdbIOSDevice import ios.simctl.SimctlIOSDevice import ios.xctest.XCTestIOSDevice +import maestro.LocalIdbRunner import maestro.Maestro import maestro.cli.device.Device import maestro.cli.device.PickDeviceInteractor import maestro.cli.device.Platform import maestro.cli.idb.IdbCompanion -import maestro.cli.idb.IdbCompanion.startIdbCompanion import maestro.debuglog.IOSDriverLogger import maestro.drivers.IOSDriver import org.slf4j.LoggerFactory @@ -186,13 +186,11 @@ object MaestroSessionManager { val idbIOSDevice = IdbIOSDevice( deviceId = selectedDevice.device.instanceId, - startCompanion = { - startIdbCompanion( - selectedDevice.host ?: defaultHost, - selectedDevice.port ?: defaultIdbPort, - selectedDevice.device.instanceId, - ) - }, + idbRunner = LocalIdbRunner( + selectedDevice.host ?: defaultHost, + selectedDevice.port ?: defaultIdbPort, + selectedDevice.device.instanceId, + ) ) Maestro.ios( @@ -305,13 +303,11 @@ object MaestroSessionManager { val idbIOSDevice = IdbIOSDevice( deviceId = deviceId, - startCompanion = { - startIdbCompanion( - host ?: defaultHost, - port ?: defaultIdbPort, - device.instanceId, - ) - }, + idbRunner = LocalIdbRunner( + host ?: defaultHost, + port ?: defaultIdbPort, + device.instanceId, + ) ) val xcTestInstaller = LocalXCTestInstaller( diff --git a/maestro-client/src/main/java/maestro/LocalIdbRunner.kt b/maestro-client/src/main/java/maestro/LocalIdbRunner.kt new file mode 100644 index 0000000000..014f20d7f9 --- /dev/null +++ b/maestro-client/src/main/java/maestro/LocalIdbRunner.kt @@ -0,0 +1,139 @@ +package maestro + +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.runCatching +import idb.CompanionServiceGrpc +import idb.HIDEventKt +import idb.Idb +import idb.hIDEvent +import idb.point +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import ios.grpc.BlockingStreamObserver +import ios.idb.IdbRunner +import maestro.debuglog.DebugLogStore +import maestro.utils.MaestroTimer +import java.net.Socket +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import kotlin.concurrent.thread + +class LocalIdbRunner( + val host: String, + val port: Int, + val deviceId: String, +): IdbRunner { + override fun stop(channel: ManagedChannel) { + channel.shutdownNow() + + if (!channel.awaitTermination(10, TimeUnit.SECONDS)) { + throw TimeoutException("Couldn't close Maestro iOS driver due to gRPC timeout") + } + } + + override fun start(): ManagedChannel { + logger.info("startIDBCompanion on $deviceId") + + // idb is associated with a device, it can't be assumed that a running idb_companion is + // associated with the device under test: Shut down before starting a fresh idb if needed. + if (isSocketAvailable(host, port)) { + ProcessBuilder(listOf("killall", "idb_companion")).start().waitFor() + } + + val idbProcessBuilder = ProcessBuilder("idb_companion", "--udid", deviceId) + DebugLogStore.logOutputOf(idbProcessBuilder) + val idbProcess = idbProcessBuilder.start() + + Runtime.getRuntime().addShutdownHook(thread(start = false) { + idbProcess.destroy() + }) + + logger.warning("Waiting for idb service to start..") + MaestroTimer.retryUntilTrue(timeoutMs = 60000, delayMs = 100) { + Socket(host, port).use { true } + } || error("idb_companion did not start in time") + + + // The first time a simulator boots up, it can + // take 10's of seconds to complete. + logger.warning("Waiting for Simulator to boot..") + MaestroTimer.retryUntilTrue(timeoutMs = 120000, delayMs = 100) { + val process = ProcessBuilder("xcrun", "simctl", "bootstatus", deviceId) + .start() + process + .waitFor(1000, TimeUnit.MILLISECONDS) + process.exitValue() == 0 + } || error("Simulator failed to boot") + + val channel = ManagedChannelBuilder.forAddress(host, port) + .usePlaintext() + .build() + + // Test if idb can get accessibility info elements with non-zero frame width + logger.warning("Waiting for successful taps") + MaestroTimer.retryUntilTrue(timeoutMs = 20000, delayMs = 100) { + testPressAction(channel) is Ok + } || error("idb_companion is not able dispatch successful tap events") + + logger.warning("Simulator ready") + + return channel + } + + private fun testPressAction(channel: ManagedChannel): Result { + val x = 0 + val y = 0 + val holdDelay = 50L + val asyncStub = CompanionServiceGrpc.newStub(channel) + + return runCatching { + val responseObserver = BlockingStreamObserver() + val stream = asyncStub.hid(responseObserver) + + val pressAction = HIDEventKt.hIDPressAction { + touch = HIDEventKt.hIDTouch { + point = point { + this.x = x.toDouble() + this.y = y.toDouble() + } + } + } + + stream.onNext( + hIDEvent { + press = HIDEventKt.hIDPress { + action = pressAction + direction = Idb.HIDEvent.HIDDirection.DOWN + } + } + ) + + Thread.sleep(holdDelay) + + stream.onNext( + hIDEvent { + press = HIDEventKt.hIDPress { + action = pressAction + direction = Idb.HIDEvent.HIDDirection.UP + } + } + ) + stream.onCompleted() + + responseObserver.awaitResult() + } + } + + private fun isSocketAvailable(host: String, port: Int): Boolean { + return try { + Socket(host, port).use { true } + } catch (_: Exception) { + false + } + } + + companion object { + val logger = DebugLogStore.loggerFor(LocalIdbRunner::class.java) + } +} diff --git a/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt b/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt index 32156ec461..be4e0b1797 100644 --- a/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt @@ -66,14 +66,12 @@ import okio.source import java.io.File import java.io.InputStream import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException import java.util.zip.GZIPInputStream class IdbIOSDevice( override val deviceId: String?, - private val startCompanion: () -> (ManagedChannel), -) : IOSDevice { + private val idbRunner: IdbRunner, +) : IOSDevice, AutoCloseable { private var channel: ManagedChannel? = null private lateinit var blockingStub: CompanionServiceGrpc.CompanionServiceBlockingStub @@ -84,8 +82,8 @@ class IdbIOSDevice( } private fun restartCompanion() { - channel?.let { closeChannel(it) } - channel = startCompanion() + channel?.let { idbRunner.stop(it) } + channel = idbRunner.start() blockingStub = CompanionServiceGrpc.newBlockingStub(channel) asyncStub = CompanionServiceGrpc.newStub(channel) } @@ -487,15 +485,7 @@ class IdbIOSDevice( } override fun close() { - channel?.let { closeChannel(it) } - } - - private fun closeChannel(channel: ManagedChannel) { - channel.shutdownNow() - - if (!channel.awaitTermination(10, TimeUnit.SECONDS)) { - throw TimeoutException("Couldn't close Maestro iOS driver due to gRPC timeout") - } + channel?.let { idbRunner.stop(it) } } override fun isScreenStatic(): Result { diff --git a/maestro-ios/src/main/java/ios/idb/IdbRunner.kt b/maestro-ios/src/main/java/ios/idb/IdbRunner.kt new file mode 100644 index 0000000000..1c4c1a92ad --- /dev/null +++ b/maestro-ios/src/main/java/ios/idb/IdbRunner.kt @@ -0,0 +1,9 @@ +package ios.idb + +import io.grpc.ManagedChannel + +interface IdbRunner { + fun stop(channel: ManagedChannel) + + fun start(): ManagedChannel +}