feat(sidecar): DriverService implementing v0.1 RPCs

DriverService delegates each RPC to a pluggable DriverBackend so
real Maestro integration can land in a follow-up without changing
the gRPC layer. StubDriverBackend satisfies the contract and lets
the Go side talk to a running sidecar today.
This commit is contained in:
pj committed 2026-04-18 00:02:08 +07:00
1 parent d9800b648f
commit 125a060a69
2 files changed
+156

No files matched your search

@@ -0,0 +1,56 @@
package dev.uatu.sidecar
interface DriverBackend {
fun launch(bundleId: String, clearState: Boolean)
fun terminate(bundleId: String)
fun tap(x: Int, y: Int)
fun inputText(text: String)
fun screenshot(): Triple<ByteArray, Int, Int>
fun hierarchy(): String
fun waitForIdle(durationMillis: Long)
fun healthy(): Boolean
}
/**
* StubDriverBackend records calls but takes no real device action. Real
* Maestro integration arrives in a follow-up; v0.1 wires the gRPC plumbing
* end-to-end so the Go side can be exercised against a running sidecar
* even before Maestro is plugged in.
*/
class StubDriverBackend(private val platform: String) : DriverBackend {
@Volatile var launchCount: Int = 0
private set
@Volatile var lastBundleId: String? = null
private set
@Volatile var lastTap: Pair<Int, Int>? = null
private set
@Volatile var lastInputText: String? = null
private set
override fun launch(bundleId: String, clearState: Boolean) {
launchCount++
lastBundleId = bundleId
}
override fun terminate(bundleId: String) {
lastBundleId = null
}
override fun tap(x: Int, y: Int) {
lastTap = x to y
}
override fun inputText(text: String) {
lastInputText = text
}
override fun screenshot(): Triple<ByteArray, Int, Int> = Triple(ByteArray(0), 0, 0)
override fun hierarchy(): String = "{\"children\":[],\"platform\":\"$platform\"}"
override fun waitForIdle(durationMillis: Long) {
if (durationMillis > 0) Thread.sleep(durationMillis)
}
override fun healthy(): Boolean = true
}
@@ -0,0 +1,100 @@
package dev.uatu.sidecar
import com.google.protobuf.ByteString
import dev.uatu.driver.v1.DriverGrpc
import dev.uatu.driver.v1.Duration
import dev.uatu.driver.v1.Empty
import dev.uatu.driver.v1.HealthStatus
import dev.uatu.driver.v1.HierarchyJSON
import dev.uatu.driver.v1.Image
import dev.uatu.driver.v1.LaunchRequest
import dev.uatu.driver.v1.Point
import dev.uatu.driver.v1.Text
import io.grpc.stub.StreamObserver
import java.util.concurrent.atomic.AtomicReference
class DriverService(
private val platform: String = "android",
private val serial: String? = null,
private val backend: DriverBackend = StubDriverBackend(platform),
) : DriverGrpc.DriverImplBase() {
private val launchedBundleId = AtomicReference<String?>(null)
override fun launch(request: LaunchRequest, responseObserver: StreamObserver<Empty>) {
runRpc(responseObserver) {
backend.launch(request.bundleId, request.clearState)
launchedBundleId.set(request.bundleId)
Empty.getDefaultInstance()
}
}
override fun terminate(request: Empty, responseObserver: StreamObserver<Empty>) {
runRpc(responseObserver) {
launchedBundleId.get()?.let { backend.terminate(it) }
launchedBundleId.set(null)
Empty.getDefaultInstance()
}
}
override fun tap(request: Point, responseObserver: StreamObserver<Empty>) {
runRpc(responseObserver) {
backend.tap(request.x, request.y)
Empty.getDefaultInstance()
}
}
override fun inputText(request: Text, responseObserver: StreamObserver<Empty>) {
runRpc(responseObserver) {
backend.inputText(request.value)
Empty.getDefaultInstance()
}
}
override fun screenshot(request: Empty, responseObserver: StreamObserver<Image>) {
runRpc(responseObserver) {
val (png, width, height) = backend.screenshot()
Image.newBuilder()
.setPng(ByteString.copyFrom(png))
.setWidth(width)
.setHeight(height)
.build()
}
}
override fun hierarchy(request: Empty, responseObserver: StreamObserver<HierarchyJSON>) {
runRpc(responseObserver) {
HierarchyJSON.newBuilder().setJson(backend.hierarchy()).build()
}
}
override fun waitForIdle(request: Duration, responseObserver: StreamObserver<Empty>) {
runRpc(responseObserver) {
backend.waitForIdle(request.millis)
Empty.getDefaultInstance()
}
}
override fun health(request: Empty, responseObserver: StreamObserver<HealthStatus>) {
runRpc(responseObserver) {
HealthStatus.newBuilder()
.setReady(backend.healthy())
.setVersion(VERSION)
.setPlatform(platform)
.build()
}
}
private inline fun <T> runRpc(observer: StreamObserver<T>, block: () -> T) {
try {
observer.onNext(block())
observer.onCompleted()
} catch (cause: Exception) {
observer.onError(cause)
}
}
companion object {
const val VERSION: String = "0.0.1"
}
}