Skip to content

Commit

Permalink
Add IdbRunner class to start and stop idb (#903)
Browse files Browse the repository at this point in the history
* Add IdbRunner class to start and stop idb

* Tidy-ups
  • Loading branch information
berikv authored Mar 15, 2023
1 parent 9b3506f commit 2498ccd
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 149 deletions.
120 changes: 1 addition & 119 deletions maestro-cli/src/main/java/maestro/cli/idb/IdbCompanion.kt
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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<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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
139 changes: 139 additions & 0 deletions maestro-client/src/main/java/maestro/LocalIdbRunner.kt
Original file line number Diff line number Diff line change
@@ -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<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
}
}

companion object {
val logger = DebugLogStore.loggerFor(LocalIdbRunner::class.java)
}
}
20 changes: 5 additions & 15 deletions maestro-ios/src/main/java/ios/idb/IdbIOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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<Boolean, Throwable> {
Expand Down
9 changes: 9 additions & 0 deletions maestro-ios/src/main/java/ios/idb/IdbRunner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ios.idb

import io.grpc.ManagedChannel

interface IdbRunner {
fun stop(channel: ManagedChannel)

fun start(): ManagedChannel
}

0 comments on commit 2498ccd

Please sign in to comment.