diff --git a/maestro-cli/src/main/java/maestro/cli/device/DeviceService.kt b/maestro-cli/src/main/java/maestro/cli/device/DeviceService.kt index aaacbad90f..7327455db1 100644 --- a/maestro-cli/src/main/java/maestro/cli/device/DeviceService.kt +++ b/maestro-cli/src/main/java/maestro/cli/device/DeviceService.kt @@ -1,28 +1,15 @@ package maestro.cli.device -import com.github.michaelbull.result.Ok import dadb.Dadb -import io.grpc.ManagedChannelBuilder -import ios.idb.IdbIOSDevice import ios.simctl.Simctl import ios.simctl.Simctl.SimctlError import ios.simctl.SimctlList import maestro.cli.CliError -import maestro.cli.session.SessionStore import maestro.cli.util.EnvUtils -import maestro.debuglog.DebugLogStore import maestro.utils.MaestroTimer import java.io.File -import java.net.Socket -import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread object DeviceService { - - private val logger = DebugLogStore.loggerFor(DeviceService::class.java) - private const val idbHost = "localhost" - private const val idbPort = 10882 - fun startDevice(device: Device.AvailableForLaunch): Device.Connected { when (device.platform) { Platform.IOS -> { @@ -77,69 +64,6 @@ object DeviceService { } } - fun prepareDevice(device: Device.Connected) { - if (device.platform == Platform.IOS) { - startIdbCompanion(device) - } - } - - private fun isIdbCompanionRunning(): Boolean { - return try { - Socket(idbHost, idbPort).use { true } - } catch (_: Exception) { - false - } - } - - private fun startIdbCompanion(device: Device.Connected) { - logger.info("startIDBCompanion on $device") - - if (isIdbCompanionRunning() && SessionStore.activeSessions().isEmpty()) { - error("idb_companion is already running. Stop idb_companion and run maestro again") - } - - val idbProcessBuilder = ProcessBuilder("idb_companion", "--udid", device.instanceId) - DebugLogStore.logOutputOf(idbProcessBuilder) - val idbProcess = idbProcessBuilder.start() - - Runtime.getRuntime().addShutdownHook(thread(start = false) { - idbProcess.destroy() - }) - - val channel = ManagedChannelBuilder.forAddress(idbHost, idbPort) - .usePlaintext() - .build() - - IdbIOSDevice(channel, device.instanceId).use { iosDevice -> - logger.warning("Waiting for idb service to start..") - MaestroTimer.retryUntilTrue(timeoutMs = 60000, delayMs = 100) { - Socket(idbHost, idbPort).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", device.instanceId) - .start() - process - .waitFor(1000, TimeUnit.MILLISECONDS) - process.exitValue() == 0 - } || error("Simulator failed to boot") - - // Test if idb can get accessibility info elements with non-zero frame with - logger.warning("Waiting for successful taps") - MaestroTimer.retryUntilTrue(timeoutMs = 20000, delayMs = 100) { - val tapResult = iosDevice - .tap(0, 0) - - tapResult is Ok - } || error("idb_companion is not able dispatch successful tap events") - - logger.warning("Simulator ready") - } - } - fun listConnectedDevices(): List { return listDevices() .filterIsInstance(Device.Connected::class.java) diff --git a/maestro-cli/src/main/java/maestro/cli/device/PickDeviceInteractor.kt b/maestro-cli/src/main/java/maestro/cli/device/PickDeviceInteractor.kt index 12a3511289..b37a2b0c8d 100644 --- a/maestro-cli/src/main/java/maestro/cli/device/PickDeviceInteractor.kt +++ b/maestro-cli/src/main/java/maestro/cli/device/PickDeviceInteractor.kt @@ -6,14 +6,10 @@ object PickDeviceInteractor { fun pickDevice(deviceId: String? = null): Device.Connected { if (deviceId != null) { - val device = DeviceService.listConnectedDevices() + return DeviceService.listConnectedDevices() .find { it.instanceId == deviceId } ?: throw CliError("Device with id $deviceId is not connected") - - DeviceService.prepareDevice(device) - - return device } return pickDeviceInternal() @@ -28,8 +24,6 @@ object PickDeviceInteractor { error("Device $result is not connected") } - DeviceService.prepareDevice(result) - result } } diff --git a/maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt b/maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt new file mode 100644 index 0000000000..ca453c0246 --- /dev/null +++ b/maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt @@ -0,0 +1,137 @@ +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 + fun setup(device: Device.Connected) { + val idbProcessBuilder = ProcessBuilder("idb_companion", "--udid", device.instanceId) + idbProcessBuilder.start() + + val idbHost = "localhost" + val idbPort = 10882 + MaestroTimer.retryUntilTrue(timeoutMs = 30000, delayMs = 100) { + 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/idb/IdbInstaller.kt b/maestro-cli/src/main/java/maestro/cli/idb/IdbInstaller.kt deleted file mode 100644 index c27af94a45..0000000000 --- a/maestro-cli/src/main/java/maestro/cli/idb/IdbInstaller.kt +++ /dev/null @@ -1,19 +0,0 @@ -package maestro.cli.idb - -import maestro.cli.device.Device -import maestro.utils.MaestroTimer -import java.net.Socket - -object IdbInstaller { - - fun setup(device: Device.Connected) { - val idbProcessBuilder = ProcessBuilder("idb_companion", "--udid", device.instanceId) - idbProcessBuilder.start() - - val idbHost = "localhost" - val idbPort = 10882 - MaestroTimer.retryUntilTrue(timeoutMs = 30000, delayMs = 100) { - Socket(idbHost, idbPort).use { true } - } - } -} \ No newline at end of file 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 52b31b400f..374c415138 100644 --- a/maestro-cli/src/main/java/maestro/cli/session/MaestroSessionManager.kt +++ b/maestro-cli/src/main/java/maestro/cli/session/MaestroSessionManager.kt @@ -30,7 +30,8 @@ import maestro.Maestro import maestro.cli.device.Device import maestro.cli.device.PickDeviceInteractor import maestro.cli.device.Platform -import maestro.cli.idb.IdbInstaller +import maestro.cli.idb.IdbCompanion +import maestro.cli.idb.IdbCompanion.startIdbCompanion import maestro.debuglog.IOSDriverLogger import maestro.drivers.IOSDriver import org.slf4j.LoggerFactory @@ -44,8 +45,8 @@ import kotlin.concurrent.thread object MaestroSessionManager { private const val defaultHost = "localhost" - private const val idbPort = 10882 - private const val xcTestPort = 22087 + private const val defaultIdbPort = 10882 + private const val defaultXcTestPort = 22087 private val executor = Executors.newScheduledThreadPool(1) private val logger = LoggerFactory.getLogger(MaestroSessionManager::class.java) @@ -54,10 +55,7 @@ object MaestroSessionManager { host: String?, port: Int?, deviceId: String?, - - // needed for experimental web support isStudio: Boolean = false, - block: (MaestroSession) -> T, ): T { val selectedDevice = selectDevice(host, port, deviceId) @@ -142,7 +140,7 @@ object MaestroSessionManager { private fun createMaestro( selectedDevice: SelectedDevice, connectToExistingSession: Boolean, - isStudio: Boolean + isStudio: Boolean, ): MaestroSession { return when { selectedDevice.device != null -> MaestroSession( @@ -158,23 +156,21 @@ object MaestroSessionManager { ) } Platform.IOS -> { - val channel = ManagedChannelBuilder.forAddress(defaultHost, idbPort) - .usePlaintext() - .build() - val xcTestInstaller = LocalXCTestInstaller( logger = IOSDriverLogger(), deviceId = selectedDevice.device.instanceId, ) - // TODO (as) remove this flag with a proper factory - val installNetworkInterceptor = isStudio - val xcTestDriverClient = XCTestDriverClient(defaultHost, xcTestPort, installNetworkInterceptor) { + + val xcTestDriverClient = XCTestDriverClient( + host = defaultHost, + port = defaultXcTestPort, + restoreConnection = { if (SessionStore.activeSessions().isNotEmpty()) { - IdbInstaller.setup(selectedDevice.device) + IdbCompanion.setup(selectedDevice.device) return@XCTestDriverClient xcTestInstaller.setup() } return@XCTestDriverClient false - } + }) val xcTestDevice = XCTestIOSDevice( deviceId = selectedDevice.device.instanceId, @@ -186,17 +182,24 @@ object MaestroSessionManager { val simctlIOSDevice = SimctlIOSDevice( deviceId = selectedDevice.device.instanceId, - xcTestDevice = xcTestDevice, + ) + + val idbIOSDevice = IdbIOSDevice( + deviceId = selectedDevice.device.instanceId, + startCompanion = { + startIdbCompanion( + selectedDevice.host ?: defaultHost, + selectedDevice.port ?: defaultIdbPort, + selectedDevice.device.instanceId, + ) + }, ) Maestro.ios( driver = IOSDriver( LocalIOSDevice( deviceId = selectedDevice.device.instanceId, - idbIOSDevice = IdbIOSDevice( - channel = channel, - deviceId = selectedDevice.device.instanceId, - ), + idbIOSDevice = idbIOSDevice, xcTestDevice = xcTestDevice, simctlIOSDevice = simctlIOSDevice, ) @@ -222,7 +225,6 @@ object MaestroSessionManager { selectedDevice.port, selectedDevice.deviceId, !connectToExistingSession, - isStudio ), device = null, ) @@ -254,7 +256,7 @@ object MaestroSessionManager { private fun isIOS(host: String?, port: Int?): Boolean { return try { - val channel = ManagedChannelBuilder.forAddress(host ?: defaultHost, port ?: idbPort) + val channel = ManagedChannelBuilder.forAddress(host ?: defaultHost, port ?: defaultIdbPort) .usePlaintext() .build() @@ -298,24 +300,34 @@ object MaestroSessionManager { port: Int?, deviceId: String?, openDriver: Boolean, - isStudio: Boolean, ): Maestro { - val channel = ManagedChannelBuilder.forAddress(host ?: defaultHost, port ?: idbPort) - .usePlaintext() - .build() val device = PickDeviceInteractor.pickDevice(deviceId) - val idbIOSDevice = IdbIOSDevice(channel, device.instanceId) + + val idbIOSDevice = IdbIOSDevice( + deviceId = deviceId, + startCompanion = { + startIdbCompanion( + host ?: defaultHost, + port ?: defaultIdbPort, + device.instanceId, + ) + }, + ) + val xcTestInstaller = LocalXCTestInstaller( logger = IOSDriverLogger(), deviceId = device.instanceId, ) - val xcTestDriverClient = XCTestDriverClient(defaultHost, xcTestPort, isStudio) { - if (SessionStore.activeSessions().isNotEmpty()) { - IdbInstaller.setup(device) - return@XCTestDriverClient xcTestInstaller.setup() + val xcTestDriverClient = XCTestDriverClient( + host = defaultHost, + port = defaultXcTestPort, + restoreConnection = { + if (SessionStore.activeSessions().isNotEmpty()) { + return@XCTestDriverClient xcTestInstaller.setup() + } + return@XCTestDriverClient false } - return@XCTestDriverClient false - } + ) val xcTestDevice = XCTestIOSDevice( deviceId = device.instanceId, client = xcTestDriverClient, @@ -325,7 +337,6 @@ object MaestroSessionManager { ) val simctlIOSDevice = SimctlIOSDevice( deviceId = device.instanceId, - xcTestDevice = xcTestDevice, ) val iosDriver = IOSDriver( diff --git a/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt b/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt index 6950152085..32156ec461 100644 --- a/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt @@ -71,12 +71,24 @@ import java.util.concurrent.TimeoutException import java.util.zip.GZIPInputStream class IdbIOSDevice( - private val channel: ManagedChannel, override val deviceId: String?, + private val startCompanion: () -> (ManagedChannel), ) : IOSDevice { - private val blockingStub = CompanionServiceGrpc.newBlockingStub(channel) - private val asyncStub = CompanionServiceGrpc.newStub(channel) + private var channel: ManagedChannel? = null + private lateinit var blockingStub: CompanionServiceGrpc.CompanionServiceBlockingStub + private lateinit var asyncStub: CompanionServiceGrpc.CompanionServiceStub + + init { + restartCompanion() + } + + private fun restartCompanion() { + channel?.let { closeChannel(it) } + channel = startCompanion() + blockingStub = CompanionServiceGrpc.newBlockingStub(channel) + asyncStub = CompanionServiceGrpc.newStub(channel) + } override fun open() { ensureGrpcChannel() @@ -88,7 +100,7 @@ class IdbIOSDevice( } override fun deviceInfo(): Result { - return runCatching { + return runWithRestartRecovery { val response = blockingStub.describe(targetDescriptionRequest {}) val screenDimensions = response.targetDescription.screenDimensions @@ -102,7 +114,7 @@ class IdbIOSDevice( } override fun contentDescriptor(): Result { - return runCatching { + return runWithRestartRecovery { val accessibilityResponse = blockingStub.accessibilityInfo(accessibilityInfoRequest {}) val accessibilityNode: XCUIElement = mapper.readValue(accessibilityResponse.json) accessibilityNode @@ -118,7 +130,7 @@ class IdbIOSDevice( } override fun pressKey(code: Int): Result { - return runCatching { + return runWithRestartRecovery { val responseObserver = BlockingStreamObserver() val stream = asyncStub.hid(responseObserver) @@ -132,7 +144,7 @@ class IdbIOSDevice( } override fun pressButton(code: Int): Result { - return runCatching { + return runWithRestartRecovery { val responseObserver = BlockingStreamObserver() val stream = asyncStub.hid(responseObserver) @@ -157,7 +169,7 @@ class IdbIOSDevice( y: Int, holdDelay: Long ): Result { - return runCatching { + return runWithRestartRecovery { val responseObserver = BlockingStreamObserver() val stream = asyncStub.hid(responseObserver) @@ -196,7 +208,7 @@ class IdbIOSDevice( override fun input( text: String, ): Result { - return runCatching { + return runWithRestartRecovery { val responseObserver = BlockingStreamObserver() val stream = asyncStub.hid(responseObserver) @@ -210,7 +222,7 @@ class IdbIOSDevice( } override fun install(stream: InputStream): Result { - return runCatching { + return runWithRestartRecovery { val responseObserver = BlockingStreamObserver() val requestStream = asyncStub.install(responseObserver) @@ -243,7 +255,7 @@ class IdbIOSDevice( } override fun uninstall(id: String): Result { - return runCatching { + return runWithRestartRecovery { try { blockingStub.uninstall( uninstallRequest { @@ -259,7 +271,7 @@ class IdbIOSDevice( } override fun pullAppState(id: String, file: File): Result { - return runCatching { + return runWithRestartRecovery { val observer = BlockingStreamObserver() asyncStub.pull(pullRequest { container = fileContainer { @@ -274,7 +286,7 @@ class IdbIOSDevice( } override fun pushAppState(id: String, file: File): Result { - return runCatching { + return runWithRestartRecovery { val observer = BlockingStreamObserver() val stream = asyncStub.push(observer) @@ -320,7 +332,7 @@ class IdbIOSDevice( Thread.sleep(1500) // deletes app data, including container folder - val result = runCatching { + val result = runWithRestartRecovery { blockingStub.rm(rmRequest { container = fileContainer { kind = Idb.FileContainer.Kind.APPLICATION @@ -340,7 +352,7 @@ class IdbIOSDevice( "tmp" ) - runCatching { + runWithRestartRecovery { paths.forEach { path -> blockingStub.mkdir(mkdirRequest { container = fileContainer { @@ -356,13 +368,13 @@ class IdbIOSDevice( } override fun clearKeychain(): Result { - return runCatching { + return runWithRestartRecovery { blockingStub.clearKeychain(clearKeychainRequest { }) } } override fun launch(id: String): Result { - return runCatching { + return runWithRestartRecovery { val responseObserver = BlockingStreamObserver() val stream = asyncStub.launch(responseObserver) stream.onNext( @@ -379,7 +391,7 @@ class IdbIOSDevice( } override fun stop(id: String): Result { - return runCatching { + return runWithRestartRecovery { val responseObserver = BlockingStreamObserver() asyncStub.terminate( terminateRequest { @@ -393,7 +405,7 @@ class IdbIOSDevice( } override fun openLink(link: String): Result { - return runCatching { + return runWithRestartRecovery { blockingStub.openUrl(openUrlRequest { url = link }) @@ -409,7 +421,7 @@ class IdbIOSDevice( val future = CompletableFuture() val bufferedOut = out.buffer() val compressedData = Buffer() - return runCatching { + return runWithRestartRecovery { val request = asyncStub.record(object : StreamObserver { override fun onNext(value: RecordResponse) { if (value.payload.compression == Idb.Payload.Compression.GZIP) { @@ -463,7 +475,7 @@ class IdbIOSDevice( } override fun setLocation(latitude: Double, longitude: Double): Result { - return runCatching { + return runWithRestartRecovery { blockingStub.setLocation(setLocationRequest { location = location { this.latitude = latitude; this.longitude = longitude } }) @@ -471,10 +483,14 @@ class IdbIOSDevice( } override fun isShutdown(): Boolean { - return channel.isShutdown + return channel?.isShutdown ?: true } override fun close() { + channel?.let { closeChannel(it) } + } + + private fun closeChannel(channel: ManagedChannel) { channel.shutdownNow() if (!channel.awaitTermination(10, TimeUnit.SECONDS)) { @@ -490,6 +506,17 @@ class IdbIOSDevice( TODO("Not yet implemented") } + private infix fun T.runWithRestartRecovery(block: T.() -> V): Result { + return runCatching { + try { + block() + } catch (e: StatusRuntimeException) { + restartCompanion() + block() + } + } + } + companion object { // 4Mb, the default max read for gRPC private const val CHUNK_SIZE = 1024 * 1024 * 3 diff --git a/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt b/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt index 91bd2a3a61..b5d6bb3b92 100644 --- a/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt @@ -7,14 +7,12 @@ import hierarchy.XCUIElement import ios.IOSDevice import ios.IOSScreenRecording import ios.device.DeviceInfo -import ios.xctest.XCTestIOSDevice import okio.Sink import java.io.File import java.io.InputStream class SimctlIOSDevice( override val deviceId: String, - private val xcTestDevice: XCTestIOSDevice, ) : IOSDevice { override fun open() { TODO("Not yet implemented") @@ -123,7 +121,6 @@ class SimctlIOSDevice( override fun setPermissions(id: String, permissions: Map) { Simctl.setPermissions(deviceId, id, permissions) - xcTestDevice.restartXCTestRunnerService() } override fun close() { diff --git a/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt b/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt index be46f1ee85..e663ea601c 100644 --- a/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt @@ -32,7 +32,7 @@ class XCTestIOSDevice( restartXCTestRunnerService() } - fun restartXCTestRunnerService() { + private fun restartXCTestRunnerService() { logger.info("[Start] Uninstalling xctest ui runner app on $deviceId") installer.killAndUninstall() logger.info("[Done] Uninstalling xctest ui runner app on $deviceId") diff --git a/maestro-xcuitest-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt b/maestro-xcuitest-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt index 92fed05cb0..4dc4fd89d9 100644 --- a/maestro-xcuitest-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt +++ b/maestro-xcuitest-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt @@ -19,7 +19,6 @@ import java.util.concurrent.TimeUnit class XCTestDriverClient( private val host: String = "localhost", private val port: Int = 22087, - private val installNetworkInterceptor: Boolean, private val restoreConnection: () -> Boolean = { false } ) { @@ -31,53 +30,12 @@ class XCTestDriverClient( }) } - private val okHttpClient by lazy { - if (installNetworkInterceptor) { - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .addInterceptor(Interceptor { - val request = it.request() - try { - it.proceed(request) - } catch (connectException: IOException) { - if (restoreConnection()) { - it.proceed(request) - } else { - throw XCTestDriverUnreachable("Failed to reach out XCUITest Server") - } - } - }).addNetworkInterceptor(Interceptor { - val request = it.request() - try { - it.proceed(request) - } catch (connectException: IOException) { - if (restoreConnection() || isShuttingDown) { - Response.Builder() - .request(it.request()) - .protocol(Protocol.HTTP_1_1) - .code(200) - .build() - } else { - throw XCTestDriverUnreachable("Failed to reach out XCUITest Server") - } - } - }).build() - } else { - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .addInterceptor(Interceptor { - try { - val request = it.request() - it.proceed(request) - } catch (exception: IOException) { - throw XCTestDriverUnreachable("Failed to reach out XCUITest Server") - } - }) - .build() - } - } + private val okHttpClient = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .addRetryOnErrorInterceptor() + .addReturnOkOnShutdownInterceptor() + .build() class XCTestDriverUnreachable(message: String) : IOException(message) @@ -177,4 +135,36 @@ class XCTestDriverClient( return okHttpClient.newCall(requestBuilder.build()).execute() } + + private fun OkHttpClient.Builder.addRetryOnErrorInterceptor() = addInterceptor(Interceptor { + val request = it.request() + try { + it.proceed(request) + } catch (connectException: IOException) { + if (restoreConnection()) { + it.proceed(request) + } else { + throw XCTestDriverUnreachable("Failed to reach out XCUITest Server") + } + } + }) + + private fun OkHttpClient.Builder.addReturnOkOnShutdownInterceptor() = addNetworkInterceptor(Interceptor { + val request = it.request() + try { + it.proceed(request) + } catch (connectException: IOException) { + // Fake an Ok response when shutting down and receiving an error + // to prevent a stack trace in the cli when running maestro studio. + if (isShuttingDown) { + Response.Builder() + .request(it.request()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .build() + } else { + throw XCTestDriverUnreachable("Failed to reach out XCUITest Server") + } + } + }) }