diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index 58faa6d..7203d7c 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -31,6 +31,11 @@ interface DriverBackend { // serialized pair from the same on-device frame. Backends may override // to fuse the two reads more tightly when their native API allows. fun snapshot(): SnapshotSample = SnapshotSample(hierarchy(), screenshot()) + + // close releases device-side resources on shutdown. The iOS backend must + // stop its XCTest runner here: an orphaned runner session auto-restarts + // later and hijacks the simulator's gesture daemon mid-run. + fun close() {} } data class SnapshotSample( @@ -613,6 +618,11 @@ class MaestroDriverBackend(private val serial: String?) : DriverBackend { override fun healthy() = runCatching { driver.contentDescriptor(false); true }.getOrElse { false } override fun metrics(bundleId: String) = readProcMetrics(serial, bundleId) + + override fun close() { + runCatching { driver.close() } + runCatching { dadb.close() } + } } private fun buildDadb(serial: String?): dadb.Dadb { @@ -832,6 +842,13 @@ class IosDriverBackend(private val udid: String) : DriverBackend { override fun healthy() = runCatching { driver.contentDescriptor(false); true }.getOrElse { false } override fun metrics(bundleId: String) = MetricsSample(0.0, 0L, 0L) + + // close stops the XCTest runner session (kills the xcodebuild process and + // uninstalls the runner app). Skipping this leaves an orphaned session + // that xcodebuild later restarts, killing the next run's session. + override fun close() { + runCatching { driver.close() } + } } private fun keyCodeToMaestro(adbKeyCode: String): maestro.KeyCode? { diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt index c5c81eb..cfe11d0 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverService.kt @@ -186,6 +186,15 @@ class DriverService( } } + // shutdown runs on the JVM shutdown path (SIGTERM from the runner). It + // terminates the app under test so the simulator is not left showing a + // stale session, then closes the backend so the iOS XCTest runner process + // dies with us instead of being orphaned. + fun shutdown() { + runCatching { launchedBundleId.getAndSet(null)?.let { backend.terminate(it) } } + runCatching { backend.close() } + } + private inline fun runRpc(observer: StreamObserver, block: () -> T) { try { observer.onNext(block()) diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt index 4aeed6c..5ed8f6f 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/Main.kt @@ -30,6 +30,7 @@ class SidecarServer( fun stop() { grpcServer?.shutdown() + service.shutdown() shutdownLatch.countDown() } }