mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
205 lines
11 KiB
Markdown
205 lines
11 KiB
Markdown
# Agent control
|
|
|
|
`tools/droiddeckctl` drives DroidDeck on a real device over ADB, for agents and scripts that test
|
|
the app end to end: start and stop sessions, launch and quit Steam games, wait for a game to have
|
|
focus, send input, inspect the app's UI and gamescope's windows, run commands inside the session,
|
|
and collect logs, screenshots and recordings. `tools/droiddeck-scenario` repeats a scripted test
|
|
and reports every run.
|
|
|
|
## How it works
|
|
|
|
- **The provider** `content://com.droiddeck.launcher.agent` (`agent/AgentBridgeProvider.kt`)
|
|
answers each command with JSON. It and the session-start Activity (`AgentStartActivity`) are in
|
|
every build and require `android.permission.DUMP`, held by the ADB shell and privileged or
|
|
explicitly granted callers. The provider checks the calling permission before decoding requests.
|
|
- **The guest agent** `droiddeck-agent` runs inside every session (started by
|
|
`droiddeck-session`). It runs commands with the session's own environment, publishes gamescope's
|
|
focus, and talks to the Steam client's DevTools, which only accepts guest processes. The app and
|
|
the agent exchange request and answer files under the session root (`files/session/agent`).
|
|
- **The UI dump** reads the app's Compose semantics from inside the app (`agent/AgentUi.kt`), so it
|
|
works on every display and window. UI Automator, by contrast, dumps whichever window has focus,
|
|
which on a dual-screen device is often another app's.
|
|
|
|
## Access
|
|
|
|
Player controls and observations work over authorized ADB on any build: `state`, `start`, `stop`,
|
|
`resume`, `wait`, `launch`, `quit`, `focus`, `input`, `ui`, `logs`, `screenshot`, `record`,
|
|
`displays`, `wake`, `glasses` (except `glasses raw`), and `access`.
|
|
|
|
**Setup > Diagnostics > Debugging tools** is off by default in every build, including debug builds.
|
|
It enables `run`, `guest`, `cdp`, all `env` operations, `override`, `prefs`, and `glasses raw`. The
|
|
toggle switches directly without a confirmation dialog. The bridge refuses to change it through `ui`
|
|
or preference writes. The existing preference key, `setting-agent-commands` test tag, JSON command
|
|
names, and `AGENT_COMMANDS_DISABLED` error code are unchanged. Refusals exit 7; `droiddeckctl access`
|
|
reports whether commands are enabled.
|
|
|
|
Disabling the toggle clears queued and persistent agent environment settings and installed binary
|
|
overrides. Disabled settings cannot apply at the next session start. Already running commands and
|
|
effects already applied to a session are not undone; stop and restart that session to discard them.
|
|
|
|
Authorized command execution runs inside the runtime containing Steam's saved login. CDP evaluates
|
|
JavaScript in the authenticated Steam client, and preferences can contain private values. These
|
|
capabilities remain unrestricted when enabled: the toggle is an explicit opt-in, not credential
|
|
isolation or a sandbox against the authorized caller. Main already supported arbitrary guest
|
|
programs through `run`; restricting credential access would require addressing that route too.
|
|
ADB access remains a trust boundary, and ADB input can operate the device's own toggle.
|
|
|
|
The helper exchanges private files with the app and adds no network listener. The guarded Steam
|
|
DevTools port is bound to loopback and accepts only peers with a fresh private registration for a
|
|
live socket descriptor, matching socket inode, destination port, and address family. The existing
|
|
x86_64 Decky loader exception remains: that mode does not enable the guest-only acceptance guard.
|
|
|
|
Requests are limited to 256 KiB of encoded JSON. Guest responses and DevTools messages are limited
|
|
to 1 MiB; provider replies above 384 Ki characters return `RESPONSE_TOO_LARGE` to avoid overflowing
|
|
Binder. Command output retains at most 256 KiB per stream while continuing to drain both streams.
|
|
Timeouts must be finite, positive, and no greater than 600 seconds, and terminate the command's
|
|
process group. Override installation refuses path traversal and symbolic links.
|
|
|
|
Automatic process logs record environment keys and argument counts rather than values or full
|
|
argument lists. UI nodes marked as passwords redact text, editable text, descriptions, and state
|
|
text. This does not redact arbitrary authorized command results, CDP data, preferences, or screen
|
|
captures. Scenario directories are owner-only and JSON reports are written with mode 0600. Review
|
|
artifacts before sharing; a live `logs` pull copies the current folder without a fresh export scrub.
|
|
|
|
## Device selection and output
|
|
|
|
The CLI resolves one authorized device using `ADB_SERIAL` or `ANDROID_SERIAL` when set. Otherwise it
|
|
de-duplicates transports that report the same device serial and asks for an explicit serial if
|
|
multiple devices remain. Pass `--serial` before the command to choose directly; set `ADB` (or pass
|
|
`--adb`) to choose an adb executable. `tools/deploy_local.sh` uses the same resolver.
|
|
|
|
Every command writes one JSON object to stdout and its resolved serial to stderr. Exit codes: 0
|
|
success, 2 invalid or rejected command, 3 ADB or device error, 4 session error, 5 timeout, 6
|
|
artifact or file error, 7 debugging tools disabled.
|
|
|
|
## Commands
|
|
|
|
```sh
|
|
tools/droiddeckctl state
|
|
tools/droiddeckctl start steam --wait
|
|
tools/droiddeckctl start steam --wait --reuse
|
|
tools/droiddeckctl start steam --steamvr --wait
|
|
tools/droiddeckctl start desktop
|
|
tools/droiddeckctl run /usr/bin/foo -- arg1 arg2
|
|
tools/droiddeckctl wait ready|idle|failed
|
|
tools/droiddeckctl stop
|
|
tools/droiddeckctl resume
|
|
|
|
tools/droiddeckctl launch 489830 --wait
|
|
tools/droiddeckctl wait focused 489830
|
|
tools/droiddeckctl wait game
|
|
tools/droiddeckctl quit 489830
|
|
tools/droiddeckctl focus
|
|
|
|
tools/droiddeckctl input key ENTER
|
|
tools/droiddeckctl input text 'hello world'
|
|
tools/droiddeckctl input tap 640 360
|
|
tools/droiddeckctl input swipe 100 600 100 100 --duration 400
|
|
tools/droiddeckctl input pad a
|
|
tools/droiddeckctl input stick left 0 -1 --hold 500
|
|
tools/droiddeckctl input trigger right 1.0
|
|
|
|
tools/droiddeckctl ui dump
|
|
tools/droiddeckctl ui click rail-steam
|
|
tools/droiddeckctl ui click --match "Play"
|
|
tools/droiddeckctl ui set-text some-field --text abc
|
|
|
|
tools/droiddeckctl guest -- xprop -root GAMESCOPE_FOCUSED_APP
|
|
tools/droiddeckctl cdp targets
|
|
tools/droiddeckctl cdp eval 'SteamClient.Apps.GetMyRunningApps?.()'
|
|
tools/droiddeckctl env set DXVK_HUD=fps
|
|
tools/droiddeckctl env list
|
|
tools/droiddeckctl env clear [next|persistent|all]
|
|
tools/droiddeckctl override gamescope ./gamescope
|
|
tools/droiddeckctl prefs get session
|
|
tools/droiddeckctl prefs set session some_key true --type bool
|
|
|
|
tools/droiddeckctl logs latest ./session-artifacts
|
|
tools/droiddeckctl screenshot ./screen.png --display 4
|
|
tools/droiddeckctl record ./clip.mp4 --seconds 8 --display 0
|
|
tools/droiddeckctl displays
|
|
tools/droiddeckctl wake
|
|
|
|
tools/droiddeckctl glasses status
|
|
tools/droiddeckctl glasses set refreshHz 120
|
|
tools/droiddeckctl glasses set 3d on
|
|
tools/droiddeckctl glasses simulate --seconds 6 --yaw 15
|
|
tools/droiddeckctl glasses stop-simulate
|
|
tools/droiddeckctl glasses mode 0x34
|
|
tools/droiddeckctl glasses raw 0x0007
|
|
```
|
|
|
|
Glasses status reports `deviceMode` from the MCU separately from `sbs`, which
|
|
requires a physical stereo display mode. `switching3d` marks the bounded
|
|
confirmation window. A successful mode-command reply alone does not confirm 3D.
|
|
|
|
A sleeping device suspends the session and hides the app's windows from `ui dump`; `wake` first.
|
|
|
|
`input` needs a session that has drawn its first frame. Pad input goes through the session screen's
|
|
virtual pad, the same one the on-screen controls drive, so games that ignore touch still respond.
|
|
`launch`, `quit` and `cdp` need a Steam session; `focus` and `wait focused` need gamescope (every
|
|
mode except the desktop).
|
|
|
|
## State (schema 2)
|
|
|
|
`state` returns `schema`, `build`, `appVersion`, `runtime {installed, version}`,
|
|
`agent {guest, guestFocus, commands}` and `session`:
|
|
|
|
- `id`, `phase`, `running`, `mode`, `program`, `steamUi`, `steamUrl`, `steamVr`, `suspended`, `pip`,
|
|
`firstFrame`, `output` [w, h], `refreshHz`, `lastTransitionAt`, `guestPid`, `installing`
|
|
- `logDir`, `eventsFile`, `artifactsAvailable`, `artifactsComplete`, `failure {code, message, status}`
|
|
- `focus`: gamescope's focus as the guest agent last published it, or null:
|
|
`focusedApp`, `focusedWindow`, `focusableApps`, `baselayerAppIds`, `seq`, `t`.
|
|
|
|
Phases: `IDLE`, `PREPARING`, `INSTALLING_RUNTIME`, `STARTING_COMPOSITOR`, `STARTING_GUEST`,
|
|
`STARTING_STEAM`, `READY`, `SUSPENDED`, `STOPPING`, `FAILED`. Schema 2 only added `agent` and
|
|
`session.focus`; every schema 1 field is unchanged.
|
|
|
|
A game the player can see is `focusedApp == appId` **and** `appId in focusableApps`.
|
|
`GAMESCOPE_FOCUSABLE_WINDOWS` is what the Steam client orders the base layer from: a game missing
|
|
there stays behind the client's loading screen however healthy its window looks
|
|
(docs/development/steam-focus-debugging.md). `focus` returns the full snapshot, including each
|
|
server's windows with map state, override-redirect, app id, `WM_STATE`, pid, class, opacity and
|
|
`_NET_WM_STATE`.
|
|
|
|
## Events
|
|
|
|
Each session writes `events.jsonl` beside its other artifacts: the lifecycle (`session.created`,
|
|
`compositor.started`, `guest.exited`, ...) plus `agent.ready` (the guest agent is up),
|
|
`focus.changed` (the focused app, focusable apps or base-layer order changed) and
|
|
`agent.launch_requested`.
|
|
|
|
## UI test tags
|
|
|
|
Compose test tags are exposed as resource ids (`ui/TestTags.kt`), so both `ui dump` and UI
|
|
Automator show them:
|
|
|
|
| Tag | Where |
|
|
|---|---|
|
|
| `rail-steam`, `rail-games`, `rail-desktop`, `rail-store`, `rail-components`, `rail-android-apps`, `rail-setup`, `rail-updates` | the front end's rail |
|
|
| `play-steam` | Play on the Steam and Games pages |
|
|
| `setting-<key>` | every toggle and choice row, by its key (`setting-logs`, `setting-agent-commands`, ...) |
|
|
| `tab-<n>` | a page's tab strip (Setup's Overview, Controller, Session, Launcher...), by position |
|
|
| `drawer-tab-controller`, `-second_screen`, `-display`, `-effects`, `-games`, `-session` | the session drawer's pages |
|
|
| `drawer-stop`, `drawer-stop-confirm`, `drawer-stop-cancel` | stopping a session from the drawer |
|
|
|
|
## Scenarios
|
|
|
|
`tools/droiddeck-scenario` runs a JSON scenario: `setup` once, `steps` `repeat` times, `teardown`
|
|
once. Each run starts with `before` (typically `start steam --wait --reuse`, so a session that died
|
|
in one run, to a crash or the low-memory killer, is started again for the next) and ends with
|
|
`after`, whether it passed or not, so a failed run cannot leave a game running into the next. A list step is a droiddeckctl command line; `{"assert": "dotted.path", "equals"|"in"|
|
|
"contains"|"not"|"exists": ...}` checks the last answer; `{"sleep"}`, `{"screenshot"}` and
|
|
`{"record"}` do what they say. A failing run keeps a screenshot, the focus snapshot and the
|
|
session's logs in its folder, and `report.json` sums up the runs.
|
|
|
|
```sh
|
|
tools/droiddeck-scenario tools/scenarios/steam-game-focus.json --var appId=489830 --repeat 10 --keep-going
|
|
```
|
|
|
|
`tools/scenarios/steam-game-focus.json` launches a game, checks it has focus the way a player
|
|
sees it, and quits it: the test that catches a game left behind Steam's loading screen, which only
|
|
shows up in some fraction of launches. A game with a launcher of its own needs the launcher pressed
|
|
through first, since the launcher window carries the game's app id too:
|
|
`tools/scenarios/skyrim-launcher-focus.json` taps Skyrim's Play before checking.
|