fix(sidecar): reap orphaned XCTest runner sessions at iOS init

This commit is contained in:
pj committed 2026-06-06 11:02:10 +05:30
1 parent 132b05c2fd
commit 93575e56d5
2 files changed
+40

No files matched your search

@@ -677,12 +677,33 @@ private fun pngHeight(bytes: ByteArray): Int {
(bytes[22].toInt() and 0xFF shl 8) or (bytes[23].toInt() and 0xFF)
}
internal const val IOS_XCTEST_RUNNER_BUNDLE_ID = "dev.mobile.maestro-driver-iosUITests.xctrunner"
// reapOrphanIosRunners kills XCTest runner sessions left over from a prior
// run. A sidecar that died without its shutdown hook leaves its xcodebuild
// session alive; xcodebuild later restarts its dead runner, which terminates
// the active run's session and steals the simulator's gesture daemon. Returns
// true when an orphaned xcodebuild session was found and killed.
internal fun reapOrphanIosRunners(udid: String, execute: (List<String>) -> Int): Boolean {
val killed = execute(listOf("pkill", "-f", "xcodebuild.*test-without-building.*$udid")) == 0
execute(listOf("xcrun", "simctl", "terminate", udid, IOS_XCTEST_RUNNER_BUNDLE_ID))
return killed
}
class IosDriverBackend(private val udid: String) : DriverBackend {
private lateinit var driver: maestro.drivers.IOSDriver
private lateinit var localDevice: ios.LocalIOSDevice
private val reconnectLock = java.util.concurrent.locks.ReentrantLock()
init {
val reaped = reapOrphanIosRunners(udid) { command ->
runCatching { ProcessBuilder(command).start().waitFor() }.getOrDefault(1)
}
if (reaped) {
println("terminated orphaned XCTest runner session for $udid")
// Give the killed session a beat to tear down before installing ours.
Thread.sleep(1000)
}
val wdaPort = maestro.utils.SocketUtils.nextFreePort(22000, 23000)
val tempFileHandler = maestro.utils.TempFileHandler()
val simctlDevice = device.SimctlIOSDevice(
@@ -159,6 +159,25 @@ class DriverServiceTest {
assertTrue(thrown.status.description.orEmpty().contains("only one gesture"))
}
@Test fun reapOrphanIosRunnersKillsStrayXcodebuildAndRunnerApp() {
val commands = mutableListOf<List<String>>()
val reaped = reapOrphanIosRunners("UDID-1234") { command ->
commands.add(command)
0
}
assertTrue(reaped)
assertEquals(2, commands.size)
assertEquals("pkill", commands[0][0])
assertTrue(commands[0][2].contains("test-without-building"))
assertTrue(commands[0][2].contains("UDID-1234"))
assertEquals(listOf("xcrun", "simctl", "terminate", "UDID-1234", IOS_XCTEST_RUNNER_BUNDLE_ID), commands[1])
}
@Test fun reapOrphanIosRunnersReportsNothingFound() {
val reaped = reapOrphanIosRunners("UDID-1234") { 1 }
assertEquals(false, reaped)
}
@Test fun overlappedDoubleTapLandsTwoTaps() {
val invocations = java.util.concurrent.atomic.AtomicInteger(0)
overlappedDoubleTap { invocations.incrementAndGet() }