Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-restart xctest runner and idb_companion on connection error #882

Merged
merged 4 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 0 additions & 76 deletions maestro-cli/src/main/java/maestro/cli/device/DeviceService.kt
Original file line number Diff line number Diff line change
@@ -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 -> {
Expand Down Expand Up @@ -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<Device.Connected> {
return listDevices()
.filterIsInstance(Device.Connected::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -28,8 +24,6 @@ object PickDeviceInteractor {
error("Device $result is not connected")
}

DeviceService.prepareDevice(result)

result
}
}
Expand Down
137 changes: 137 additions & 0 deletions maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amanjeetsingh150 Do you know the details on why this was implemented differently than startIdbCompanion?

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<Unit, Throwable> {
val x = 0
val y = 0
val holdDelay = 50L
val asyncStub = CompanionServiceGrpc.newStub(channel)

return runCatching {
val responseObserver = BlockingStreamObserver<Idb.HIDResponse>()
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
}
}
}
19 changes: 0 additions & 19 deletions maestro-cli/src/main/java/maestro/cli/idb/IdbInstaller.kt

This file was deleted.

Loading