mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
fix(agent-bridge): launch sessions through debug activity
This commit is contained in:
1 parent
c2d6d272a3
commit
a419c8097f
8 files changed
+200
-115
No files matched your search
@@ -12,10 +12,6 @@ On Android 12+, if Steam exits without a log, turn off **Restrict child processe
|
||||
|
||||
Run `tools/build_local.sh` with Docker, Java 17, the Android SDK/NDK, and `zstd` installed. It builds the ARM64 audio sinks from PulseAudio 13.0 and packages them into the APK at `app/build/outputs/apk/release/app-release.apk`. Set `DROIDDECK_PA13_SOURCE_DIR` to an existing PulseAudio 13.0 source directory to skip downloading it. To install the APK on an attached device, run `tools/deploy_local.sh`.
|
||||
|
||||
## Agent control
|
||||
|
||||
Debug APKs expose a shell-protected ADB bridge and `tools/droiddeckctl`. See [agent control](docs/agent-control.md) for setup and commands.
|
||||
|
||||
## Limits
|
||||
|
||||
Compatibility and performance vary by device; hardware validation is limited. Desktop compositing uses software rendering. Firefox sandboxing is reduced under proot. See the session logs in `Download/DroidDeck/` when diagnosing problems.
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<!-- Debug builds only. Android grants DUMP to adb shell; release APKs have no bridge. -->
|
||||
<activity
|
||||
android:name=".agent.AgentStartActivity"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:theme="@style/SessionTheme" />
|
||||
<provider
|
||||
android:name=".agent.AgentBridgeProvider"
|
||||
android:authorities="${applicationId}.agent"
|
||||
|
||||
@@ -4,16 +4,13 @@ import android.Manifest
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.util.Base64
|
||||
import com.droiddeck.launcher.BuildConfig
|
||||
import com.droiddeck.launcher.SessionActivity
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
|
||||
import com.droiddeck.launcher.session.SessionArtifacts
|
||||
@@ -23,7 +20,6 @@ import com.droiddeck.launcher.session.SessionPaths
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@@ -31,8 +27,6 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
/** A shell-only control surface, installed only by debug builds. */
|
||||
class AgentBridgeProvider : ContentProvider() {
|
||||
private val recoveryComplete = CountDownLatch(1)
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
val appContext = context?.applicationContext ?: return false
|
||||
Thread({
|
||||
@@ -56,7 +50,7 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
val response = try {
|
||||
when (method) {
|
||||
"state" -> state(context).put("ok", true)
|
||||
"start" -> start(context, arg.orEmpty(), extras)
|
||||
"start" -> error("USE_DROIDDECKCTL", "Start sessions with tools/droiddeckctl so Android launches a visible Activity")
|
||||
"stop" -> stop(context)
|
||||
"resume" -> resume(context)
|
||||
else -> error("UNKNOWN_COMMAND", "Unknown agent command '$method'")
|
||||
@@ -112,72 +106,6 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
?.maxWithOrNull(compareBy<File> { it.lastModified() }.thenBy { it.name })
|
||||
}
|
||||
|
||||
private fun start(context: Context, requested: String, extras: Bundle?): JSONObject {
|
||||
if (SessionState.running || SessionState.phase !in setOf(SessionPhase.IDLE, SessionPhase.FAILED)) {
|
||||
return error("SESSION_ACTIVE", "A session is already starting or running")
|
||||
}
|
||||
val request = request(extras)
|
||||
val mode = request?.optString("mode")?.takeIf { it.isNotBlank() } ?: requested
|
||||
val intent = Intent(context, SessionActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
val sessionMode: String
|
||||
|
||||
when (mode) {
|
||||
"steam" -> {
|
||||
sessionMode = SessionService.MODE_STEAM
|
||||
intent.putExtra(SessionService.EXTRA_MODE, sessionMode)
|
||||
}
|
||||
"desktop", SessionService.MODE_DESKTOP -> {
|
||||
sessionMode = SessionService.MODE_DESKTOP
|
||||
intent.putExtra(SessionService.EXTRA_MODE, sessionMode)
|
||||
}
|
||||
"run" -> {
|
||||
sessionMode = SessionService.MODE_RUN
|
||||
val program = request?.optString("program")?.takeIf { it.isNotBlank() }
|
||||
?: return error("PROGRAM_REQUIRED", "Run mode requires a program path")
|
||||
val args = request?.optJSONArray("programArgs") ?: JSONArray()
|
||||
val programArgs = Array(args.length()) { index ->
|
||||
args.optString(index)
|
||||
}
|
||||
intent.putExtra(SessionService.EXTRA_MODE, sessionMode)
|
||||
.putExtra(SessionService.EXTRA_PROGRAM, program)
|
||||
.putExtra(SessionService.EXTRA_PROGRAM_ARGS, programArgs)
|
||||
}
|
||||
else -> return error("INVALID_MODE", "Mode must be steam, desktop, or run")
|
||||
}
|
||||
|
||||
request?.optString("steamUi")?.takeIf { it == "desktop" }?.let {
|
||||
intent.putExtra(SessionService.EXTRA_STEAM_UI, it)
|
||||
}
|
||||
request?.optString("steamUrl")?.takeIf { it.startsWith("steam://") }?.let {
|
||||
intent.putExtra(SessionService.EXTRA_STEAM_URL, it)
|
||||
}
|
||||
|
||||
awaitRecovery()
|
||||
return try {
|
||||
SessionEvents.begin(context, sessionMode)
|
||||
SessionState.program = intent.getStringExtra(SessionService.EXTRA_PROGRAM)
|
||||
SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty()
|
||||
SessionState.steamUi = intent.getStringExtra(SessionService.EXTRA_STEAM_UI)
|
||||
SessionState.steamUrl = intent.getStringExtra(SessionService.EXTRA_STEAM_URL)
|
||||
SessionEvents.record("agent.start_requested", mapOf("mode" to sessionMode))
|
||||
context.startActivity(intent)
|
||||
JSONObject().put("ok", true).put("command", "start").put("mode", mode)
|
||||
} catch (e: Exception) {
|
||||
val message = e.message ?: e.javaClass.simpleName
|
||||
SessionEvents.fail("ACTIVITY_START_FAILED", message)
|
||||
val dir = SessionPaths.take()
|
||||
if (dir != null) {
|
||||
val appContext = context.applicationContext
|
||||
Thread({
|
||||
SessionArtifacts.collect(appContext, dir, "activity start failed")
|
||||
SessionPaths.release(appContext, dir)
|
||||
}, "agent-start-failure-artifacts").start()
|
||||
}
|
||||
error("ACTIVITY_START_FAILED", message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stop(context: Context): JSONObject {
|
||||
if (SessionState.running) {
|
||||
SessionService.stop(context)
|
||||
@@ -202,29 +130,6 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
return JSONObject().put("ok", true).put("command", "resume")
|
||||
}
|
||||
|
||||
private fun request(extras: Bundle?): JSONObject? {
|
||||
val encoded = extras?.getString(REQUEST_EXTRA) ?: return null
|
||||
return try {
|
||||
val decoded = Base64.decode(encoded, Base64.DEFAULT)
|
||||
JSONObject(String(decoded, Charsets.UTF_8))
|
||||
} catch (e: JSONException) {
|
||||
throw IllegalArgumentException("Invalid request JSON", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw IllegalArgumentException("Invalid base64 request", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun awaitRecovery() {
|
||||
try {
|
||||
if (!recoveryComplete.await(90, TimeUnit.SECONDS)) {
|
||||
throw IllegalStateException("Session artifact recovery did not finish")
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
throw IllegalStateException("Interrupted while recovering session artifacts", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun error(code: String, message: String) = JSONObject()
|
||||
.put("ok", false)
|
||||
.put("error", JSONObject().put("code", code).put("message", message))
|
||||
@@ -237,7 +142,19 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
|
||||
companion object {
|
||||
const val RESULT_JSON = "json"
|
||||
const val REQUEST_EXTRA = "request"
|
||||
private val recoveryComplete = CountDownLatch(1)
|
||||
|
||||
fun awaitRecovery() {
|
||||
try {
|
||||
if (!recoveryComplete.await(90, TimeUnit.SECONDS)) {
|
||||
throw IllegalStateException("Session artifact recovery did not finish")
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
throw IllegalStateException("Interrupted while recovering session artifacts", e)
|
||||
}
|
||||
}
|
||||
|
||||
private val STARTING_PHASES = setOf(
|
||||
SessionPhase.PREPARING,
|
||||
SessionPhase.INSTALLING_RUNTIME,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.droiddeck.launcher.agent
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.SessionActivity
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Shell-launched trampoline: Android permits the shell to start this protected debug Activity. */
|
||||
class AgentStartActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val request = decodeRequest(intent.getStringExtra(EXTRA_REQUEST))
|
||||
if (request == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
Thread({
|
||||
try {
|
||||
AgentBridgeProvider.awaitRecovery()
|
||||
runOnUiThread {
|
||||
if (!isFinishing) startSession(request)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "waiting for agent recovery", e)
|
||||
runOnUiThread { finish() }
|
||||
}
|
||||
}, "agent-start-recovery").start()
|
||||
}
|
||||
|
||||
private fun decodeRequest(encoded: String?): JSONObject? {
|
||||
if (encoded.isNullOrBlank()) {
|
||||
Log.e(TAG, "missing agent start request")
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
JSONObject(String(Base64.decode(encoded, Base64.DEFAULT), Charsets.UTF_8))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "invalid agent start request", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSession(request: JSONObject) {
|
||||
val requestedMode = request.optString("mode")
|
||||
val sessionMode = when (requestedMode) {
|
||||
"steam" -> SessionService.MODE_STEAM
|
||||
"desktop" -> SessionService.MODE_DESKTOP
|
||||
"run" -> SessionService.MODE_RUN
|
||||
else -> {
|
||||
Log.e(TAG, "invalid agent session mode: $requestedMode")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val program = if (sessionMode == SessionService.MODE_RUN) {
|
||||
request.optString("program").takeIf { it.isNotBlank() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (sessionMode == SessionService.MODE_RUN && program == null) {
|
||||
Log.e(TAG, "run mode requires a program path")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val sessionIntent = Intent(this, SessionActivity::class.java).apply {
|
||||
action = SessionService.ACTION_AGENT_START
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(SessionService.EXTRA_MODE, sessionMode)
|
||||
if (program != null) {
|
||||
val args = request.optJSONArray("programArgs") ?: JSONArray()
|
||||
putExtra(SessionService.EXTRA_PROGRAM, program)
|
||||
putExtra(SessionService.EXTRA_PROGRAM_ARGS, Array(args.length()) { index -> args.optString(index) })
|
||||
}
|
||||
if (sessionMode == SessionService.MODE_STEAM) {
|
||||
request.optString("steamUi").takeIf { it == "desktop" }?.let {
|
||||
putExtra(SessionService.EXTRA_STEAM_UI, it)
|
||||
}
|
||||
request.optString("steamUrl").takeIf { it.startsWith("steam://") }?.let {
|
||||
putExtra(SessionService.EXTRA_STEAM_URL, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
startActivity(sessionIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_REQUEST = "request"
|
||||
private const val TAG = "AgentStartActivity"
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty()
|
||||
SessionState.steamUi = intent.getStringExtra(SessionService.EXTRA_STEAM_UI)
|
||||
SessionState.steamUrl = intent.getStringExtra(SessionService.EXTRA_STEAM_URL)
|
||||
if (intent.action == SessionService.ACTION_AGENT_START) {
|
||||
SessionEvents.record("agent.start_requested", mapOf("mode" to SessionState.mode))
|
||||
}
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
// Game-tier power policy for the whole session: a sustained clock floor, the panel's
|
||||
// fastest mode, and the OS told it is in gameplay. Logged so a slow device says why.
|
||||
@@ -1192,6 +1195,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onNewIntent(intent: android.content.Intent) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent.action == SessionService.ACTION_AGENT_START) {
|
||||
if (SessionState.running || SessionState.phase !in setOf(SessionPhase.IDLE, SessionPhase.FAILED)) {
|
||||
Log.w(TAG, "ignoring agent start while session phase is ${SessionState.phase}")
|
||||
return
|
||||
}
|
||||
setIntent(intent)
|
||||
recreate()
|
||||
return
|
||||
}
|
||||
setIntent(intent)
|
||||
if (intent.action == SessionService.ACTION_HOME_GUIDE) {
|
||||
handleHomeGuideIntent(intent)
|
||||
|
||||
@@ -1011,6 +1011,7 @@ class SessionService : Service() {
|
||||
const val ACTION_STOP = "com.droiddeck.launcher.STOP_SESSION"
|
||||
const val ACTION_RESUME = "com.droiddeck.launcher.RESUME_SESSION"
|
||||
const val ACTION_HOME_GUIDE = "com.droiddeck.launcher.HOME_GUIDE"
|
||||
const val ACTION_AGENT_START = "com.droiddeck.launcher.AGENT_START"
|
||||
private const val ACTION_ACTIVITY_VISIBLE = "com.droiddeck.launcher.ACTIVITY_VISIBLE"
|
||||
private const val ACTION_ACTIVITY_HIDDEN = "com.droiddeck.launcher.ACTIVITY_HIDDEN"
|
||||
private const val ACTION_TRACK_AUXILIARY = "com.droiddeck.launcher.TRACK_AUXILIARY"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Agent control
|
||||
|
||||
The debug APK exposes a ContentProvider at `content://com.droiddeck.launcher.agent`. Android's shell-only `DUMP` permission protects it. The provider and its authority are absent from release APKs.
|
||||
The debug APK exposes a ContentProvider at `content://com.droiddeck.launcher.agent` and an Activity for starting sessions. Android's shell-only `DUMP` permission protects both. They are absent from release APKs.
|
||||
|
||||
Build and install the debug APK:
|
||||
|
||||
@@ -9,14 +9,15 @@ Build and install the debug APK:
|
||||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
The provider can also be called directly:
|
||||
The provider can also be called directly for state, stop, and resume:
|
||||
|
||||
```sh
|
||||
adb shell content call --uri content://com.droiddeck.launcher.agent --method state
|
||||
adb shell content call --uri content://com.droiddeck.launcher.agent --method start --arg steam
|
||||
adb shell content call --uri content://com.droiddeck.launcher.agent --method stop
|
||||
```
|
||||
|
||||
Start requests go through `tools/droiddeckctl`, which launches the protected debug Activity from the ADB shell. Calling the provider's `start` method directly returns an error because a background provider cannot reliably open the session screen.
|
||||
|
||||
The host CLI resolves one authorized device using `ADB_SERIAL` or `ANDROID_SERIAL` when set. Otherwise it deduplicates transports that report the same device serial and asks for an explicit serial if multiple devices remain.
|
||||
|
||||
```sh
|
||||
@@ -31,7 +32,7 @@ tools/droiddeckctl logs latest ./session-artifacts
|
||||
tools/droiddeckctl screenshot ./screen.png
|
||||
```
|
||||
|
||||
Every command writes JSON to stdout and reports its resolved ADB serial to stderr. Exit codes are 0 for success, 2 for invalid or rejected commands, 3 for ADB/device errors, 4 for session failures, 5 for timeouts, and 6 for artifact or file errors. Set `ADB` to select an adb executable or pass `--adb`; pass `--serial` before the command to select a device directly.
|
||||
Every command writes JSON to stdout and reports its resolved ADB serial to stderr. Exit codes are 0 for success, 2 for invalid or rejected commands, 3 for ADB/device errors, 4 for session failures, 5 for timeouts, and 6 for artifact or file errors. Set `ADB` to select an adb executable (or pass `--adb`); pass `--serial` before the command to select a device directly.
|
||||
|
||||
`start` accepts `steam` or `desktop`. Steam starts in Big Picture by default; `--ui desktop` selects the client's desktop UI, and `--url steam://...` passes a client URL. Use `--wait` to wait for `READY` as part of `start`. `run` accepts a program path and optional guest arguments.
|
||||
|
||||
|
||||
+62
-10
@@ -28,6 +28,9 @@ class ControlError(RuntimeError):
|
||||
def adb_path(configured: str | None) -> str:
|
||||
if configured:
|
||||
return configured
|
||||
environment_adb = os.environ.get("ADB")
|
||||
if environment_adb:
|
||||
return environment_adb
|
||||
found = shutil.which("adb")
|
||||
if found:
|
||||
return found
|
||||
@@ -83,6 +86,45 @@ def provider_call(adb: str, serial: str, method: str, arg: str | None = None, re
|
||||
return response
|
||||
|
||||
|
||||
def start_agent_activity(adb: str, serial: str, request: dict) -> None:
|
||||
encoded = base64.b64encode(json.dumps(request, separators=(",", ":")).encode()).decode()
|
||||
result = run_adb(
|
||||
adb,
|
||||
serial,
|
||||
"shell",
|
||||
"am",
|
||||
"start",
|
||||
"-W",
|
||||
"-n",
|
||||
"com.droiddeck.launcher/.agent.AgentStartActivity",
|
||||
"--es",
|
||||
"request",
|
||||
encoded,
|
||||
timeout=90,
|
||||
)
|
||||
output = (result.stdout + result.stderr).decode(errors="replace")
|
||||
if result.returncode != 0 or "status: ok" not in output.lower():
|
||||
raise ControlError(output.strip() or "The agent start Activity did not launch", "ACTIVITY_START_FAILED")
|
||||
|
||||
|
||||
def confirm_start(adb: str, serial: str, request: dict, previous: dict) -> dict:
|
||||
deadline = time.monotonic() + 95
|
||||
expected_mode = {"steam": "steam", "desktop": "lxqt", "run": "run"}[request["mode"]]
|
||||
while True:
|
||||
state = get_state(adb, serial)
|
||||
session = state.get("session", {})
|
||||
phase = session.get("phase")
|
||||
if session.get("mode") == expected_mode and session.get("id") != previous.get("id"):
|
||||
return state
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise ControlError(
|
||||
f"The agent Activity launched but no new session appeared; phase is {phase}",
|
||||
"ACTIVITY_START_FAILED",
|
||||
)
|
||||
time.sleep(min(0.25, remaining))
|
||||
|
||||
|
||||
def get_state(adb: str, serial: str) -> dict:
|
||||
response = provider_call(adb, serial, "state")
|
||||
if response.get("ok") is False:
|
||||
@@ -148,13 +190,18 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict:
|
||||
if not args.url.startswith("steam://"):
|
||||
raise ControlError("Steam URL must start with steam://", "INVALID_STEAM_URL")
|
||||
request["steamUrl"] = args.url
|
||||
response = provider_call(adb, serial, "start", args.mode, request)
|
||||
if response.get("ok") is False:
|
||||
error = response.get("error", {})
|
||||
raise ControlError(error.get("message", "start failed"), error.get("code", "START_FAILED"))
|
||||
start_agent_activity(adb, serial, request)
|
||||
started = confirm_start(adb, serial, request, current["session"])
|
||||
if args.wait:
|
||||
return add_serial(wait_for_phase(adb, serial, "READY", args.timeout), serial)
|
||||
return add_serial(response, serial)
|
||||
session = started["session"]
|
||||
return add_serial({
|
||||
"ok": True,
|
||||
"command": "start",
|
||||
"mode": args.mode,
|
||||
"sessionId": session.get("id"),
|
||||
"phase": session.get("phase"),
|
||||
}, serial)
|
||||
|
||||
if args.command == "run":
|
||||
current = get_state(adb, serial)
|
||||
@@ -162,11 +209,16 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict:
|
||||
raise ControlError("A session is already starting or running", "SESSION_ACTIVE")
|
||||
program_args = args.arguments[1:] if args.arguments[:1] == ["--"] else args.arguments
|
||||
request = {"mode": "run", "program": args.program, "programArgs": program_args}
|
||||
response = provider_call(adb, serial, "start", "run", request)
|
||||
if response.get("ok") is False:
|
||||
error = response.get("error", {})
|
||||
raise ControlError(error.get("message", "run failed"), error.get("code", "START_FAILED"))
|
||||
return add_serial(response, serial)
|
||||
start_agent_activity(adb, serial, request)
|
||||
started = confirm_start(adb, serial, request, current["session"])
|
||||
session = started["session"]
|
||||
return add_serial({
|
||||
"ok": True,
|
||||
"command": "start",
|
||||
"mode": "run",
|
||||
"sessionId": session.get("id"),
|
||||
"phase": session.get("phase"),
|
||||
}, serial)
|
||||
|
||||
if args.command == "wait":
|
||||
phases = {"ready": "READY", "idle": "IDLE", "failed": "FAILED"}
|
||||
|
||||
Reference in new issue
Block a user