dsh-apk-vm 0.0.0-stage → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +345 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +177 -0
- package/DEMO.md +85 -0
- package/DESIGN.md +335 -0
- package/LICENSE +204 -0
- package/NOTICE +74 -0
- package/README.md +920 -2
- package/SECURITY.md +108 -0
- package/client.js +3780 -0
- package/cordis.patch.yml +56 -0
- package/docs/PRIVACY.md +36 -0
- package/docs/README.md +16 -0
- package/docs/RELEASING.md +104 -0
- package/docs/SUBMISSION.md +75 -0
- package/docs/screenshot-panel.png +0 -0
- package/docs/screenshot-sidebar.png +0 -0
- package/icon.svg +8 -0
- package/index.js +2532 -0
- package/lib/android.js +2502 -0
- package/lib/instances.js +559 -0
- package/lib/scrcpy.js +2370 -0
- package/lib/setup.js +1624 -0
- package/lib/vendor/scrcpy-server-v5.0.1.jar +0 -0
- package/lib/video-stream.js +329 -0
- package/lib/ws.js +198 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +79 -4
- package/scripts/privacy-scan.mjs +94 -0
package/README.md
CHANGED
|
@@ -1,3 +1,921 @@
|
|
|
1
|
-
#
|
|
1
|
+
# APK Virtual Machine — a DeepSeek Harness plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/yuxiaole-bili/dsh-apk-vm/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/yuxiaole-bili/dsh-apk-vm)
|
|
6
|
+
|
|
7
|
+
**Run a real Android APK inside a local, rooted, open-source Android VM and preview it in the DeepSeek Harness sidebar — streamed as H.264/WebCodecs video at up to 60 fps.**
|
|
8
|
+
|
|
9
|
+
This repository contains the static DSH plugin `dsh-apk-vm`. It adds a first-class sidebar
|
|
10
|
+
entry ("APK Virtual Machine" / "APK 虚拟机") whose main panel boots an AOSP Android Emulator,
|
|
11
|
+
installs an APK into the guest, streams the guest display into the panel, and forwards
|
|
12
|
+
tap / swipe / key / text input back into the VM. The guest is a `userdebug` image prepared for
|
|
13
|
+
root (`adb root` on every boot), and the plugin exposes that rooted device to DSH through a
|
|
14
|
+
same-origin, loopback-only debug HTTP API and an `apk_vm` Agent tool.
|
|
15
|
+
|
|
16
|
+
> **中文简介**:在 DeepSeek Harness 的侧边栏里预览 Android APK。插件会启动本地开源
|
|
17
|
+
> AOSP Android 虚拟机(Android 14 / API 34,`system-images;android-34;default;x86_64`),
|
|
18
|
+
> 安装 APK,并通过 scrcpy H.264 + WebCodecs 把画面串流到侧边栏(最高 60 fps),同时把
|
|
19
|
+
> 点击、滑动、按键、文本输入送回虚拟机。支持拖拽 APK 上传、环境自检卡片、root 调试
|
|
20
|
+
> API 与 `apk_vm` Agent 工具;多个 Agent 可各自使用独立的隔离虚拟机实例(多开)。
|
|
21
|
+
> 参考机上冷启动约 29.5 s、快照快速启动约 8.4–10.5 s。
|
|
22
|
+
> 当前 **Windows x64 为端到端已验证平台**;SDK/模拟器探测已覆盖 Windows / macOS / Linux。
|
|
23
|
+
|
|
24
|
+
**Status:** `0.3.13` · Apache-2.0 · no npm runtime dependencies · no build step.
|
|
25
|
+
|
|
26
|
+
This is an open-source DSH plugin. The package is prepared for npm as **`dsh-apk-vm`**; the
|
|
27
|
+
install command below uses that package name. If npm does not have the package yet, use the
|
|
28
|
+
source fallback below. Repository:
|
|
29
|
+
[github.com/yuxiaole-bili/dsh-apk-vm](https://github.com/yuxiaole-bili/dsh-apk-vm).
|
|
30
|
+
Maintainer: [yuxiaole-bili](https://github.com/yuxiaole-bili/).
|
|
31
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md), [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md),
|
|
32
|
+
[SECURITY.md](SECURITY.md), [docs/PRIVACY.md](docs/PRIVACY.md) and [docs/RELEASING.md](docs/RELEASING.md) before opening issues or
|
|
33
|
+
releasing.
|
|
34
|
+
|
|
35
|
+
## One-line value
|
|
36
|
+
|
|
37
|
+
Turn DeepSeek Harness into a local Android APK workbench: boot a rooted AOSP VM, preview it at
|
|
38
|
+
up to 60 fps in the sidebar, and let Agents install, tap, inspect and debug APKs without leaving
|
|
39
|
+
the session.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Install the published npm package (the command used by the DSH plugin center and the community
|
|
44
|
+
market). If the package is not yet visible in your registry, use the source fallback below.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin --profile web add dsh-apk-vm
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For the Electron-managed Desktop profile, use **Plugins → add/install local bundle** with this
|
|
51
|
+
directory, or install the package through the app's plugin manager.
|
|
52
|
+
|
|
53
|
+
Source fallback without npm:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
dsh plugin --profile web add github:yuxiaole-bili/dsh-apk-vm
|
|
57
|
+
# local clone / non-Electron profile:
|
|
58
|
+
dsh plugin --profile desktop add <path-to-this-clone>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Capability category
|
|
62
|
+
|
|
63
|
+
**开发与运维 / Developer Tools** — Android VM lifecycle, APK install/preview, rooted debugging
|
|
64
|
+
and Agent-facing UI automation for development workflows.
|
|
65
|
+
|
|
66
|
+
## Risks, permissions and compatibility
|
|
67
|
+
|
|
68
|
+
**Requirements**
|
|
69
|
+
|
|
70
|
+
- Android SDK with `adb`, emulator, `sdkmanager`/`avdmanager`, a matching system image and an AVD
|
|
71
|
+
(the Environment card checks all of this; SDK bootstrap is opt-in).
|
|
72
|
+
- Hardware virtualization: AEHD/WHPX on Windows, KVM on Linux, or HVF on macOS.
|
|
73
|
+
- Node.js ≥ 20 only for the repository's headless checks; the plugin itself has no npm
|
|
74
|
+
dependencies or build step.
|
|
75
|
+
- Each running VM costs roughly 2.5 GB RAM; `maxInstances` defaults to 3 extra VMs.
|
|
76
|
+
|
|
77
|
+
**Permissions / risk**
|
|
78
|
+
|
|
79
|
+
- The guest is intentionally `userdebug` and rooted (`adb root` on boot); do not put secrets or
|
|
80
|
+
personal data in the AVD.
|
|
81
|
+
- `apk_vm` is registered for every Agent in the profile and exposes guest root shell, app
|
|
82
|
+
install/uninstall, UIAutomator and input injection. Grant it the same trust as a terminal on a
|
|
83
|
+
rooted Android device.
|
|
84
|
+
- `/apk-vm/*` HTTP routes and WebSocket upgrades are same-origin and **loopback-only by
|
|
85
|
+
default**; `allowRemote: true` is an explicit opt-in for use behind a trusted authenticated
|
|
86
|
+
proxy.
|
|
87
|
+
- No telemetry. Network access only occurs for explicit SDK/system-image downloads through the
|
|
88
|
+
user's `sdkmanager`, or when the user configures a remote adb device.
|
|
89
|
+
|
|
90
|
+
**Compatibility**
|
|
91
|
+
|
|
92
|
+
- Tested with DSH Desktop `0.2.1-rc.3` (static bundle/plugin APIs).
|
|
93
|
+
- Windows x64 is the only platform verified end-to-end (boot, H.264, rooted debug, Agent tool).
|
|
94
|
+
macOS/Linux have headless setup/CI coverage but not a live emulator run in this repository.
|
|
95
|
+
- Optional integration with
|
|
96
|
+
[`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar); the plugin falls back
|
|
97
|
+
cleanly when that service is absent.
|
|
98
|
+
- AOSP Android 14 / API 34, x86_64 system image; physical/remote adb devices can be attached
|
|
99
|
+
instead of the AVD.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Demo / visible proof
|
|
104
|
+
|
|
105
|
+
Real screenshots from the reference host:
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
109
|
+

|
|
110
|
+
|
|
111
|
+
| Asset | Status |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `docs/screenshot-panel.png` | ✅ main panel: Environment, guest H.264, controls |
|
|
114
|
+
| `docs/screenshot-sidebar.png` | ✅ right-sidebar layout next to a session |
|
|
115
|
+
| `docs/demo.gif` | optional — record a 20–30 s demo on a real host |
|
|
116
|
+
|
|
117
|
+
The [Performance](#performance) and [Verification](#verification) sections list exactly what was
|
|
118
|
+
measured, on which host, and how. Real `apk_vm` output from the reference run:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
find_ui {"text":"Phone"} -> Phone @ 102,1121
|
|
122
|
+
assert_text {"text":"Phone"} -> found 1 matching UI node(s)
|
|
123
|
+
tap_text {"text":"Phone"} -> tapped Phone at 102,1121
|
|
124
|
+
key {"text":"HOME"} -> returned to launcher
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Features
|
|
130
|
+
|
|
131
|
+
- **Sidebar entry + main panel.** Registers a localized sidebar item (`sidebar.panellist`,
|
|
132
|
+
id `apk-vm`, order 30) with a phone glyph ([icon.svg](icon.svg)), plus a full center-column
|
|
133
|
+
panel (`main`, key `apk-vm`). Both are live-verified in the reference profile.
|
|
134
|
+
- **Environment self-check card.** A one-glance readiness report for the host — SDK, `adb`,
|
|
135
|
+
emulator, command-line tools (`sdkmanager`/`avdmanager`), the configured system image, the
|
|
136
|
+
AVD, and CPU acceleration — with the missing pieces and the available repair actions
|
|
137
|
+
(`GET/POST /apk-vm/setup`). The card can create the AVD when the system image is already
|
|
138
|
+
installed, and — when `allowSetupDownloads: true` — install missing SDK components, or
|
|
139
|
+
**bootstrap a completely empty host** (download Google command-line tools, extract, accept
|
|
140
|
+
licenses, install platform-tools/emulator/build-tools/platform).
|
|
141
|
+
- **Multi-agent isolation / multi-instance.** Several DSH Agents can drive separate VMs at the
|
|
142
|
+
same time: the first Agent gets the default VM, later Agents are assigned isolated instances
|
|
143
|
+
with their own AVD, emulator port, snapshot, debug state and **H.264/WebCodecs stream**
|
|
144
|
+
(`maxInstances`, default 3 extra; extra VMs use a 720×1280 preview display).
|
|
145
|
+
Extra instances expose `owner` / `lastUsedAt` / `idleMs`, and are **stopped automatically**
|
|
146
|
+
after `instanceIdleMs` without panel frames, stream clients or Agent commands (AVD kept for
|
|
147
|
+
quick reuse). The panel selector and **VM instances** card show owner/idle state and offer
|
|
148
|
+
Stop/Destroy. The `apk_vm` tool accepts
|
|
149
|
+
`instance` and exposes `list_instances` / `create_instance` / `destroy_instance`.
|
|
150
|
+
- **Multi-AVD switching.** The Environment card lists the AVDs visible to the emulator and can
|
|
151
|
+
switch (`GET /apk-vm/avds`, `POST /apk-vm/avd`); the current AVD is stopped, switched and
|
|
152
|
+
restarted automatically.
|
|
153
|
+
- **APK upload by button or drag-and-drop.** The panel has an **Upload APK** button and a
|
|
154
|
+
drop overlay on the screen area; the file is posted to `/apk-vm/upload`
|
|
155
|
+
(raw APK bytes, ≤ 500 MiB, `.apk`/ZIP magic checked), installed into the running VM and
|
|
156
|
+
launched. Uploaded files live in the OS temp dir and are cleaned up when the plugin is
|
|
157
|
+
disposed. Installing by absolute local path is still available (`/apk-vm/install`).
|
|
158
|
+
- **Installed-app management.** The panel's **Installed apps** card lists third-party packages
|
|
159
|
+
for the active instance and offers force-stop, clear-data and uninstall; it uses the same
|
|
160
|
+
rooted `debug/packages` + `debug/action` API as the Agent tool.
|
|
161
|
+
- **Optional [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar) tab.**
|
|
162
|
+
When that community plugin is installed, this plugin registers a real tab type
|
|
163
|
+
(`apk-vm:preview`) through its service — no fork, no DOM hacks, and it degrades cleanly when
|
|
164
|
+
the service is absent (see [compatibility](#compatibility-with-dsh-better-sidebar)).
|
|
165
|
+
- **VM lifecycle from the UI.** Start / stop / restart the AVD or save a snapshot; the
|
|
166
|
+
Environment card lists all adb devices (emulator, USB/physical, network) and can attach one at
|
|
167
|
+
runtime. Detach restores the managed AVD identity. Live state, serial, boot mode/time, and a
|
|
168
|
+
bounded diagnostic log are shown.
|
|
169
|
+
- **APK discovery + one-click install.** Recursively finds `*.apk` under the configured roots,
|
|
170
|
+
reads package name / label / launcher activity with `aapt2 dump badging`, then runs
|
|
171
|
+
`adb install -r` and launches the app with `am start`.
|
|
172
|
+
- **Static APK preview.** `GET /apk-vm/apk-info?path=…` reports version, min/target SDK,
|
|
173
|
+
permissions, native ABIs, features and launcher activity without starting the VM;
|
|
174
|
+
`GET /apk-vm/apk-icon?path=…` extracts the raster launcher icon directly from the APK ZIP.
|
|
175
|
+
The APK list shows icons when a raster icon is present.
|
|
176
|
+
- **Live H.264 stream, up to 60 fps.** Default transport is scrcpy 5.0.1 H.264 over the
|
|
177
|
+
same-origin WebSocket `/apk-vm/ws`, decoded with `WebCodecs VideoDecoder` onto a `<canvas>`
|
|
178
|
+
(configured for 60 fps, 540×960, 6 Mbps). Late clients get a `RESET_VIDEO` keyframe request.
|
|
179
|
+
If `WebCodecs` / `WebSocket` is unavailable, the panel falls back to `adb exec-out screencap -p`
|
|
180
|
+
PNG polling (target 1–10 fps).
|
|
181
|
+
- **Full input forwarding.** Click → tap, drag → swipe, wheel → swipe, **two-finger pinch/zoom**
|
|
182
|
+
via the scrcpy control socket, plus Home / Back / Recents / Power and a text field; after
|
|
183
|
+
clicking the screen, common hardware keys (arrows/Enter/Escape/Tab/Backspace/Delete) and
|
|
184
|
+
printable characters are forwarded as Android key/text input. Clipboard text can be written to
|
|
185
|
+
the device (scrcpy `SET_CLIPBOARD`) or read back (`GET_CLIPBOARD`). Coordinates are converted
|
|
186
|
+
from the rendered phone frame to the device resolution.
|
|
187
|
+
- **Rooted debug surface + extra device tools.** Auto `adb root` after every boot, an on-device
|
|
188
|
+
toolkit at `/data/local/tmp/dsh/debug.sh`, a same-origin debug HTTP API, and an `apk_vm`
|
|
189
|
+
Agent tool every DSH agent can call: root shell, logcat, props, packages, install, push/pull,
|
|
190
|
+
reboot, force-stop / clear-data / uninstall, screenshot, **rotate**, **UIAutomator UI tree**,
|
|
191
|
+
**5-second screen recording (pulled to the host temp dir)** and **performance snapshot**.
|
|
192
|
+
- **Fast start.** Hypervisor detection (AEHD / WHPX / KVM / HAXM / HVF), host GPU rendering,
|
|
193
|
+
quick-boot snapshots, prewarm and auto-start — measured **cold 29.5 s**, **quick boot
|
|
194
|
+
8.4–10.5 s** on the reference host ([details](#fast-start-four-layers)).
|
|
195
|
+
- **Cross-platform SDK discovery.** `lib/setup.js` and `lib/android.js` discover Android SDK,
|
|
196
|
+
`adb`, the emulator and AVDs across Windows, macOS and Linux layouts (including `.exe`
|
|
197
|
+
suffixes and per-OS fallback locations). Windows x64 is the only platform verified end-to-end
|
|
198
|
+
so far.
|
|
199
|
+
- **Same-origin only.** Every `/apk-vm/*` HTTP route and the `/apk-vm/ws` upgrade rejects
|
|
200
|
+
cross-origin requests; request bodies and command output are bounded, and host processes are
|
|
201
|
+
spawned with argv arrays (never a shell string).
|
|
202
|
+
- **Themed + localized.** All chrome uses `--dsw-alias-*` theme tokens; English and 简体中文
|
|
203
|
+
are bundled in [locale/](locale).
|
|
204
|
+
- **Dependency-free backend.** The Host half uses Node built-ins only: no npm dependencies,
|
|
205
|
+
no build step.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Quickstart
|
|
210
|
+
|
|
211
|
+
### 1. Prerequisites
|
|
212
|
+
|
|
213
|
+
| Requirement | Notes |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| **DeepSeek Harness** | A profile that can install local plugin bundles (the desktop Web UI or any agent with the `plugin_manager` tool). |
|
|
216
|
+
| **Android SDK** | `platform-tools` (adb), `emulator`, `build-tools` (for `aapt2`), command-line tools (`sdkmanager`/`avdmanager`) and an AOSP system image. A JDK (17 works) is needed by the SDK command-line tools. |
|
|
217
|
+
| **AOSP system image** | `system-images;android-34;default;x86_64` (Android 14, API 34) — the image used for all measurements below. Configurable with `systemImage`. |
|
|
218
|
+
| **Hardware virtualization** | Required for the x86_64 image: AEHD or WHPX on Windows, KVM on Linux, HVF on macOS. See [acceleration matrix](#cpu-acceleration-matrix). |
|
|
219
|
+
| **Disk / RAM** | A few GB for the SDK + system image and the AVD; an emulator with a modern desktop GPU is recommended. |
|
|
220
|
+
| **Node.js 24+** *(optional)* | Only for running this repository's checks ([Verification](#verification)). The plugin itself has no npm dependencies. |
|
|
221
|
+
|
|
222
|
+
The plugin's **Environment** card performs this check for you (see
|
|
223
|
+
[First run](#5-first-run)); the commands below are the manual equivalent.
|
|
224
|
+
|
|
225
|
+
#### CPU acceleration matrix
|
|
226
|
+
|
|
227
|
+
| Host OS | Backend | How to enable |
|
|
228
|
+
| --- | --- | --- |
|
|
229
|
+
| Windows | **AEHD** (Android Emulator Hypervisor Driver) | Install via SDK Manager package `extras;google;Android_Emulator_Hypervisor_Driver`, or the standalone installer; requires administrator rights and a reboot. Recommended when Hyper-V/WHPX is **not** active. |
|
|
230
|
+
| Windows | **WHPX** | Enable the *Windows Hypervisor Platform* optional feature (and Hyper-V if desired), reboot. Alternative when Hyper-V is already in use. |
|
|
231
|
+
| Linux | **KVM** | Load `kvm_intel`/`kvm_amd`, ensure `/dev/kvm` exists and your user can access it (e.g. `kvm` group), then reboot/login again. |
|
|
232
|
+
| macOS | **HVF** (Hypervisor.framework) | Built into macOS; the emulator uses it automatically on Intel and Apple Silicon hosts. |
|
|
233
|
+
|
|
234
|
+
Check the host before starting the plugin:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
"$ANDROID_HOME/emulator/emulator" -accel-check
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The plugin also runs the SDK's `emulator-check accel` itself and reports the result in
|
|
241
|
+
`/apk-vm/state` (`vm.accel`), `/apk-vm/setup` (`accel`) and the panel log. If no hypervisor is
|
|
242
|
+
usable, it **fails fast** with an explicit error instead of hanging for 20 minutes (the x86_64
|
|
243
|
+
image cannot boot under pure TCG).
|
|
244
|
+
|
|
245
|
+
### 2. Install the plugin
|
|
246
|
+
|
|
247
|
+
Copy or clone this repository anywhere local:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
git clone https://github.com/yuxiaole-bili/dsh-apk-vm.git
|
|
251
|
+
cd dsh-apk-vm
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Then pick one:
|
|
255
|
+
|
|
256
|
+
- **CLI npm install (community market / non-`desktop` profile)**:
|
|
257
|
+
```bash
|
|
258
|
+
dsh plugin --profile web add dsh-apk-vm
|
|
259
|
+
```
|
|
260
|
+
Source fallback: `dsh plugin --profile web add github:yuxiaole-bili/dsh-apk-vm`.
|
|
261
|
+
- **DSH Web UI** — open **Plugins**, choose to add/install a plugin bundle from a local
|
|
262
|
+
directory, and select this folder. The bundle patch is [cordis.patch.yml](cordis.patch.yml);
|
|
263
|
+
the `dsh.client` declaration in [package.json](package.json) loads `client.js` into the Web UI.
|
|
264
|
+
- **Agent tool** — ask a DSH agent to run `plugin_manager` with action `install_bundle` and the
|
|
265
|
+
target set to this folder path.
|
|
266
|
+
- **CLI local install (non-Electron profiles only)** —
|
|
267
|
+
`dsh plugin --profile <profile> add <path-to-this-folder>`. The Electron-managed `desktop`
|
|
268
|
+
profile must be managed from the app UI instead.
|
|
269
|
+
|
|
270
|
+
After installing or upgrading the plugin, **reload the DSH Web page** so the new client bundle
|
|
271
|
+
is loaded.
|
|
272
|
+
|
|
273
|
+
### 3. Provide an Android SDK, system image and AVD
|
|
274
|
+
|
|
275
|
+
The plugin auto-discovers the SDK (`sdkRoot`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, then common
|
|
276
|
+
per-OS locations) and adb/emulator. Fastest path: open the panel, check the **Environment**
|
|
277
|
+
card, and use **Create AVD** once the system image is installed. If the image is missing and
|
|
278
|
+
`allowSetupDownloads: true` is set, the card can also run `sdkmanager --install` for it;
|
|
279
|
+
otherwise install it yourself:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
sdkmanager --install "platform-tools" "emulator" "build-tools;34.0.0" \
|
|
283
|
+
"system-images;android-34;default;x86_64"
|
|
284
|
+
avdmanager create avd -n dsh_apk -k "system-images;android-34;default;x86_64"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The plugin can also attach to an already-running adb device instead of spawning the AVD; set
|
|
288
|
+
`serial` (for example `emulator-5558` or `<device-ip>:5555`) and leave `emulatorPath`/`avdName`
|
|
289
|
+
unset.
|
|
290
|
+
|
|
291
|
+
### 4. Configure
|
|
292
|
+
|
|
293
|
+
The bundle ships with portable defaults (SDK auto-detection, `avdName: dsh_apk`). Review the
|
|
294
|
+
config for your machine — at minimum `roots` if you want extra APK scan directories. The full
|
|
295
|
+
list of keys is in the [configuration reference](#configuration).
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
config:
|
|
299
|
+
# sdkRoot: 'C:/Android/sdk' # optional; otherwise auto-detected
|
|
300
|
+
# emulatorPath: 'C:/Android/sdk/emulator/emulator.exe' # optional; auto-detected
|
|
301
|
+
# serial: 'emulator-5558' # ...or attach an existing adb serial instead of spawning
|
|
302
|
+
systemImage: 'system-images;android-34;default;x86_64'
|
|
303
|
+
avdName: dsh_apk
|
|
304
|
+
port: 5558
|
|
305
|
+
roots: ['C:/projects'] # extra APK discovery roots (workspace is automatic)
|
|
306
|
+
gpuMode: host
|
|
307
|
+
accel: auto
|
|
308
|
+
requireAccel: true
|
|
309
|
+
quickBoot: true
|
|
310
|
+
snapshotName: default_boot
|
|
311
|
+
saveSnapshotAfterBoot: true
|
|
312
|
+
prewarm: false # opt-in warm boot on plugin activation
|
|
313
|
+
prewarmDelayMs: 3000
|
|
314
|
+
autoStart: true
|
|
315
|
+
multiInstance: true # isolated VMs for concurrent agents
|
|
316
|
+
maxInstances: 3 # extra VMs (default VM is always available)
|
|
317
|
+
instanceIdleMs: 900000 # stop idle extra VMs after 15 min (AVD kept)
|
|
318
|
+
instanceReapIntervalMs: 60000
|
|
319
|
+
allowRemote: false # reject non-loopback VM API clients
|
|
320
|
+
allowSetupDownloads: false # gate the optional system-image download action
|
|
321
|
+
scrcpy: true
|
|
322
|
+
scrcpyMaxSize: 960
|
|
323
|
+
scrcpyMaxFps: 60
|
|
324
|
+
scrcpyBitrate: 6000000
|
|
325
|
+
frameFps: 2 # only the PNG fallback target (1-10)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
If `emulatorPath` / `adbPath` are omitted, the plugin auto-discovers the Android SDK from
|
|
329
|
+
`sdkRoot`, then `ANDROID_HOME` / `ANDROID_SDK_ROOT`, then common per-user/system install
|
|
330
|
+
locations, and finally falls back to `PATH`. Spawning an AVD still needs `avdName`; use `serial`
|
|
331
|
+
to attach an existing device instead.
|
|
332
|
+
|
|
333
|
+
### 5. First run
|
|
334
|
+
|
|
335
|
+
1. Open the DSH sidebar and click **APK Virtual Machine**. Check the **Environment** card:
|
|
336
|
+
green **Ready** means the SDK, image, AVD and acceleration were found; **Setup needed** lists
|
|
337
|
+
what is missing and offers **Create AVD** (and optionally **Download image** when
|
|
338
|
+
`allowSetupDownloads: true`).
|
|
339
|
+
2. Press **Start VM** (or let `prewarm`/`autoStart` boot it). The **first start is a cold boot**
|
|
340
|
+
(measured 29.5 s on the reference host); the plugin then saves the quick-boot snapshot
|
|
341
|
+
`default_boot`, and later starts restore it (measured 8.4–10.5 s).
|
|
342
|
+
3. Install an APK: click **Upload APK**, drag an `.apk` file onto the panel, or pick one from the
|
|
343
|
+
discovered list / install by absolute path. The app is installed and launched automatically.
|
|
344
|
+
Use the phone frame for input and the **Rooted debug** card for root shell, logcat, props,
|
|
345
|
+
UI tree, rotation, recording, perf and screenshot.
|
|
346
|
+
4. Or delegate to an agent, for example: *"Use the `apk_vm` tool to install
|
|
347
|
+
`C:/work/app.apk` and take a screenshot."*
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Configuration
|
|
352
|
+
|
|
353
|
+
All keys are optional; defaults are shown in the **Default** column.
|
|
354
|
+
|
|
355
|
+
| Key | Default | Purpose |
|
|
356
|
+
| --- | --- | --- |
|
|
357
|
+
| `emulatorPath` | auto-detected | Path to the Android emulator executable. Auto-discovered from the SDK / `PATH`. |
|
|
358
|
+
| `avdName` | — | AVD name to spawn. Required to spawn an AVD. |
|
|
359
|
+
| `serial` | — | Attach this adb serial instead of spawning an emulator. |
|
|
360
|
+
| `port` | `5558` | Emulator console port used when spawning. |
|
|
361
|
+
| `sdkRoot` | `ANDROID_HOME` / `ANDROID_SDK_ROOT` | Android SDK root for `adb`, `aapt2`, `emulator-check`, `sdkmanager`/`avdmanager`. |
|
|
362
|
+
| `adbPath` | `<sdk>/platform-tools/adb` | Explicit adb executable. |
|
|
363
|
+
| `systemImage` | `system-images;android-34;default;x86_64` | System image required by the Environment card and used for AVD creation. |
|
|
364
|
+
| `spawnEmulator` | `true` | Set `false` to only attach (never spawn). |
|
|
365
|
+
| `emulatorArgs` | `[]` | Extra argv appended to the emulator command line. |
|
|
366
|
+
| `roots` | `[]` | Extra APK discovery roots. The DSH working directory, `process.cwd()` and the home directory are added automatically. |
|
|
367
|
+
| `bootTimeoutMs` | `1200000` | Max wait for the guest to report `sys.boot_completed=1`. |
|
|
368
|
+
| `gpuMode` | `host` | `host` uses the real GPU; `swiftshader_indirect` is the software fallback for headless hosts. |
|
|
369
|
+
| `accel` | `auto` | `auto` detects AEHD/WHPX/KVM/HAXM/HVF; `on`/`off` force the decision. |
|
|
370
|
+
| `requireAccel` | `true` | Fail fast with an explicit error when no hypervisor is found, instead of booting forever under TCG. |
|
|
371
|
+
| `quickBoot` | `true` | Restore the saved AVD snapshot when present; `false` adds `-no-snapshot`. |
|
|
372
|
+
| `snapshotName` | `default_boot` | Snapshot name to load/save. |
|
|
373
|
+
| `saveSnapshotAfterBoot` | `true` | Save a snapshot after the first successful cold boot. |
|
|
374
|
+
| `prewarm` | `false` | Boot the VM in the background shortly after plugin activation. |
|
|
375
|
+
| `prewarmDelayMs` | `3000` | Delay before the prewarm start. |
|
|
376
|
+
| `autoStart` | `true` | Start the VM on the first `/apk-vm/state` or `/apk-vm/frame` request. |
|
|
377
|
+
| `multiInstance` | `true` | Allow isolated extra VMs for concurrent Agents. |
|
|
378
|
+
| `maxInstances` | `3` | Maximum number of extra instances (in addition to the default VM). Each running instance costs guest RAM/CPU. |
|
|
379
|
+
| `instanceIdleMs` | `900000` | Stop an extra instance after this long without panel frames, a video client, or an Agent command (AVD is kept). |
|
|
380
|
+
| `instanceReapIntervalMs` | `60000` | How often the idle reaper checks extra instances. |
|
|
381
|
+
| `allowRemote` | `false` | Allow non-loopback clients to reach `/apk-vm/*` and `/apk-vm/ws` (only behind your own authenticated proxy). |
|
|
382
|
+
| `allowSetupDownloads` | `false` | Allow the Environment card's `install-image` action to run `sdkmanager --install` (large download). |
|
|
383
|
+
| `frameFps` | `2` | PNG fallback target frame rate, 1–10 (the H.264 path is not affected). |
|
|
384
|
+
| `scrcpy` | `true` | Enable the H.264/WebCodecs transport. |
|
|
385
|
+
| `scrcpyMaxSize` | `960` | Encoder max size; 960 yields 540×960 video. |
|
|
386
|
+
| `scrcpyMaxFps` | `60` | Encoder frame-rate cap, 1–60. |
|
|
387
|
+
| `scrcpyBitrate` | `6000000` | Video bitrate in bits/s. |
|
|
388
|
+
| `scrcpyPort` | `27183` | Local adb-forward port for the scrcpy server tunnel. |
|
|
389
|
+
| `scrcpyJarPath` | vendored JAR | Override the scrcpy server JAR path. |
|
|
390
|
+
|
|
391
|
+
The Environment report (`GET /apk-vm/setup`) returns `ok`, `platform`, `sdkRoot`, `adbPath`,
|
|
392
|
+
`emulatorPath`, `avdmanagerPath`, `sdkmanagerPath`, `emulatorVersion`, `systemImage` +
|
|
393
|
+
`systemImagePresent`, `avdName` + `avdPresent` + `avdPath`, `accel { available, backend,
|
|
394
|
+
detail }`, `hypervisorNotes`, `missing[]` and `actions[]` with per-action availability.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Architecture
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
┌────────────────────────────── DSH Web UI ────────────────────────────────┐
|
|
402
|
+
│ sidebar.panellist [apk-vm, order 30] ──click──▶ main[apk-vm] panel │
|
|
403
|
+
│ Client half: client.js │
|
|
404
|
+
│ Environment card · upload/drop · APK list · phone frame (WebCodecs │
|
|
405
|
+
│ canvas or PNG fallback) · Rooted debug card │
|
|
406
|
+
└──────────────────────────────────┬───────────────────────────────────────┘
|
|
407
|
+
│ same-origin HTTP + WebSocket
|
|
408
|
+
┌──────────────────────────────────▼───────────────────────────────────────┐
|
|
409
|
+
│ DSH Host: index.js (/apk-vm/* routes, ws relay, debug API, agent tool) │
|
|
410
|
+
│ ├─ lib/android.js AndroidVm: AVD lifecycle, adb, aapt2, input, debug │
|
|
411
|
+
│ ├─ lib/setup.js Environment detection, createAvd, installSystemImage │
|
|
412
|
+
│ ├─ lib/scrcpy.js ScrcpySession: push server, adb forward, H.264 demux │
|
|
413
|
+
│ │ (Annex-B → AVCC), control messages (RESET_VIDEO, …) │
|
|
414
|
+
│ └─ lib/ws.js dependency-free RFC 6455 WebSocket server │
|
|
415
|
+
└──────────────────────────────────┬───────────────────────────────────────┘
|
|
416
|
+
│ adb
|
|
417
|
+
┌──────────────▼──────────────┐
|
|
418
|
+
│ AOSP Android Emulator │
|
|
419
|
+
│ Android 14 / API 34, x86_64 │
|
|
420
|
+
│ userdebug, adbd as root │
|
|
421
|
+
└──────────────────────────────┘
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
| Layer | Implementation | Role |
|
|
425
|
+
| --- | --- | --- |
|
|
426
|
+
| VM | AOSP Android Emulator (`emulator` + `system-images;android-34;default;x86_64`) | Boots the open-source Android system image; driven through adb. |
|
|
427
|
+
| Setup | [lib/setup.js](lib/setup.js) | Dependency-free SDK / adb / emulator / AVD / acceleration detection plus one-click `create-avd` and optional `install-image`. |
|
|
428
|
+
| Streaming | scrcpy 5.0.1 server (`lib/vendor/scrcpy-server-v5.0.1.jar`, pushed to `/data/local/tmp/scrcpy-server.jar`) + [lib/scrcpy.js](lib/scrcpy.js) | H.264 screen capture and the scrcpy control protocol, without the desktop scrcpy client. |
|
|
429
|
+
| Device bridge | `adb` from Android SDK platform-tools | install / launch / screencap / input / logcat / file push-pull. |
|
|
430
|
+
| Plugin runtime | DSH plugin APIs (`slots`, `locale`, `webServer`, `tools`, `dsh.client`) | Sidebar entry, main panel, same-origin HTTP/WebSocket control plane, agent tool. |
|
|
431
|
+
|
|
432
|
+
No npm dependencies are used: the Host half imports only Node built-ins, `client.js` is a
|
|
433
|
+
pre-built browser bundle, and the plugin has no build step.
|
|
434
|
+
|
|
435
|
+
### Fast start: four layers
|
|
436
|
+
|
|
437
|
+
| Layer | What it does |
|
|
438
|
+
| --- | --- |
|
|
439
|
+
| **Hardware CPU acceleration** | Runs `emulator-check accel`; when AEHD / WHPX / KVM / HAXM / HVF is usable the emulator gets hardware virtualization. Pure TCG is never silently attempted for the x86_64 image (`requireAccel: true` fails fast with a clear error). |
|
|
440
|
+
| **Host GPU** | `-gpu host` renders the VM with the real GPU instead of SwiftShader; switch `gpuMode` on headless/no-GPU hosts. |
|
|
441
|
+
| **Quick-boot snapshot** | The first successful cold boot saves `adb emu avd snapshot save default_boot`; later starts pass `-snapshot default_boot`. A leftover `ram.img` without `snapshot.pb` is not treated as a valid snapshot. |
|
|
442
|
+
| **Warm start** | `prewarm: true` boots the VM ~3 s after plugin activation; `autoStart: true` boots it on the first `/apk-vm/state`/`/apk-vm/frame` request. |
|
|
443
|
+
|
|
444
|
+
`/apk-vm/state` exposes `vm.bootKind` (`cold` / `snapshot` / attach), `vm.bootMs`,
|
|
445
|
+
`vm.accel`, `vm.gpuMode` and `vm.snapshotAvailable`; the panel shows `cold Ns` / `quick Ns`.
|
|
446
|
+
|
|
447
|
+
### H.264 streaming and input
|
|
448
|
+
|
|
449
|
+
The default transport (when `scrcpy: true`) is:
|
|
450
|
+
|
|
451
|
+
```
|
|
452
|
+
scrcpy-server (guest H.264) → adb forward → ScrcpyVideoDemuxer → Annex-B→AVCC
|
|
453
|
+
→ /apk-vm/ws WebSocket → browser VideoDecoder → <canvas>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
- WebSocket text frame `init`: `{ codec:"h264", codecString, description (avcC, base64),
|
|
457
|
+
width, height, maxFps, bitrate }`.
|
|
458
|
+
- Binary frame: `[0]=1][1=keyframe flag][2..9]=pts (int64 LE)][10..]=AVCC NAL units]`.
|
|
459
|
+
- A client that connects to an already-running session triggers a scrcpy control
|
|
460
|
+
`RESET_VIDEO` (type 17) so it receives a fresh keyframe; clients drop delta frames until the
|
|
461
|
+
first keyframe so the canvas never decodes garbage.
|
|
462
|
+
- The session is shared by all connected clients; when the last client disconnects the scrcpy
|
|
463
|
+
encoder is stopped after a grace period (the VM keeps running). Stopping/restarting the VM or
|
|
464
|
+
unloading the plugin stops the stream.
|
|
465
|
+
- Input is independent of the streaming path: `/apk-vm/input` remains the only input channel
|
|
466
|
+
(the panel does not bypass it), so input keeps working with the PNG fallback.
|
|
467
|
+
|
|
468
|
+
The protocol notes collected while implementing this against scrcpy 5.0.1 are in
|
|
469
|
+
[spike/scrcpy/PROTOCOL.md](spike/scrcpy/PROTOCOL.md).
|
|
470
|
+
|
|
471
|
+
### HTTP control plane (same origin, local only)
|
|
472
|
+
|
|
473
|
+
Every route rejects cross-origin requests; the WebSocket upgrade is checked the same way.
|
|
474
|
+
JSON bodies are bounded to 64 KiB; the raw APK upload is bounded to 500 MiB.
|
|
475
|
+
|
|
476
|
+
| Method | Path | Purpose |
|
|
477
|
+
| --- | --- | --- |
|
|
478
|
+
| GET | `/apk-vm/state` | VM state, serial, boot time/kind, accel/GPU, root/debug status, display size, current APK, stream counters, log tail. |
|
|
479
|
+
| GET | `/apk-vm/setup?force=1` | Environment report (SDK/adb/emulator/tools/image/AVD/acceleration, missing items, available actions). Cached ~15 s. |
|
|
480
|
+
| POST | `/apk-vm/setup` | `{"action":"refresh"\|"create-avd"\|"install-image"\|"install-tools"\|"bootstrap-sdk"}`. `install-tools`/`bootstrap-sdk` accept optional `packages`; all download actions require `allowSetupDownloads: true` (otherwise `403`) and are never automatic. |
|
|
481
|
+
| GET | `/apk-vm/apks` | Discover `*.apk` files under the configured roots. |
|
|
482
|
+
| GET | `/apk-vm/apk-info?path=…` | Static APK metadata (package/version/targetSdk/permissions/features/native code/launch activity/icon path) via `aapt2`. |
|
|
483
|
+
| GET/HEAD | `/apk-vm/apk-icon?path=…` | Extract the raster launcher icon from the APK ZIP (PNG/WebP/JPEG). |
|
|
484
|
+
| GET | `/apk-vm/devices` | List adb devices (`serial`, `state`, `kind`, `model`, `product`, transport) plus current/managed-device metadata. |
|
|
485
|
+
| POST | `/apk-vm/device` | `{"serial":"<adb-serial>"}` attach an online physical/network device; `{"serial":""}` detach and restore the managed AVD. |
|
|
486
|
+
| POST | `/apk-vm/vm` | `{"action":"start"\|"stop"\|"restart"\|"snapshot"}`. |
|
|
487
|
+
| POST | `/apk-vm/install` | `{"path":"C:/path/app.apk"}` → install + launch. |
|
|
488
|
+
| POST | `/apk-vm/upload` | Raw APK bytes (≤ 500 MiB) with the file name in the `x-apk-name` header → validate ZIP/APK magic, write to the OS temp dir, install + launch. |
|
|
489
|
+
| GET | `/apk-vm/frame?t=…` | Latest PNG frame (`409` while the VM is not ready). |
|
|
490
|
+
| POST | `/apk-vm/input` | `tap` / `swipe` / `key` / `text` payloads. |
|
|
491
|
+
| POST | `/apk-vm/touch` | Multi-touch scrcpy control (`{"action":"down\|move\|up\|cancel","pointerId":0,"x":…,"y":…}` in video-frame coordinates) for pinch/zoom. |
|
|
492
|
+
| GET/POST | `/apk-vm/clipboard` | Read the device clipboard (`GET`, scrcpy `GET_CLIPBOARD`) or write text (`POST {"text":…}`). |
|
|
493
|
+
| POST | `/apk-vm/stream` | `{"fps":1–10}` — set the PNG fallback target frame rate. |
|
|
494
|
+
| GET (upgrade) | `/apk-vm/ws` | WebSocket H.264 stream (see above). |
|
|
495
|
+
| GET | `/apk-vm/instances` | List default + extra VM instances, including `owner`, `lastUsedAt`, `idleMs`, `idleTimeoutMs`, `videoClients`. |
|
|
496
|
+
| POST | `/apk-vm/instances` | `{"name":"agent-a","start":true}` — create an isolated AVD/VM; every extra instance has its own H.264 stream. |
|
|
497
|
+
| DELETE | `/apk-vm/instances/:id` | Stop/dispose an extra instance (the default cannot be destroyed). |
|
|
498
|
+
| GET/POST | `/apk-vm/instances/:id/...` | Per-instance mirror of `state`, `frame`, `vm`, `input`, `install`, `upload`, `debug/*`. |
|
|
499
|
+
|
|
500
|
+
Uploaded APKs are written under `<os temp>/dsh-apk-vm-uploads` and that directory is removed
|
|
501
|
+
when the plugin is disposed (best-effort stale-file cleanup also runs before each upload).
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Rooted debug surface
|
|
506
|
+
|
|
507
|
+
The VM image is the AOSP `android-34;default;x86_64` **userdebug** build. After every boot the
|
|
508
|
+
Host half prepares a rooted debug environment and captures it in the quick-boot snapshot:
|
|
509
|
+
|
|
510
|
+
- **adb root by default.** `_prepareDebugSystem()` runs `adb root`, waits for adbd, and verifies
|
|
511
|
+
`uid=0(root)`. The image has `ro.debuggable=1` and `ro.adb.secure=0`, so there is no adb
|
|
512
|
+
authorization prompt. State reports `root: true` plus identity (`debugBuild`, `adbSecure`).
|
|
513
|
+
- **Debug-friendly settings.** adb/development settings enabled, package verifier and
|
|
514
|
+
adb-install verification disabled, `persist.sys.usb.config=adb`, root-access property set.
|
|
515
|
+
- **On-device toolkit** at `/data/local/tmp/dsh/debug.sh` (+ `README.txt`, mode 755):
|
|
516
|
+
`info | logs [lines] | prop <name> [value] | pkgs | install <apk> | help`.
|
|
517
|
+
- **Snapshot persistence.** Root state, settings and the toolkit are captured by
|
|
518
|
+
`adb emu avd snapshot save default_boot`; a quick boot restores them and `adb root` is
|
|
519
|
+
re-applied idempotently.
|
|
520
|
+
- The emulator accepts only `-prop qemu.*` arguments, so root/debug state is applied right after
|
|
521
|
+
boot (not faked via a `-prop ro.*` flag) and then snapshotted.
|
|
522
|
+
|
|
523
|
+
### Debug HTTP API (same origin)
|
|
524
|
+
|
|
525
|
+
| Method | Path | Operation |
|
|
526
|
+
| --- | --- | --- |
|
|
527
|
+
| GET | `/apk-vm/debug/info` | serial, identity (Android/API/ABI/debug), root, toolkit, display, uptime. |
|
|
528
|
+
| POST | `/apk-vm/debug/shell` | `{"command":"id; getprop …"}` → rooted shell (bounded stdout/stderr/code). |
|
|
529
|
+
| GET | `/apk-vm/debug/logcat?lines=200&buffer=main&filter=…` | bounded logcat dump; `buffer=crash` for the crash buffer. |
|
|
530
|
+
| GET/POST | `/apk-vm/debug/prop` | read (`?name=`) / write (`{"name","value"}`) system properties. |
|
|
531
|
+
| GET | `/apk-vm/debug/packages` | third-party (or `?all=1` all) packages with APK paths. |
|
|
532
|
+
| POST | `/apk-vm/debug/root` | ensure adbd is root. |
|
|
533
|
+
| POST | `/apk-vm/debug/push` | `{"localPath","remotePath"}` (remote under `/data/local/tmp`, `/sdcard`, `/storage/emulated/0`). |
|
|
534
|
+
| POST | `/apk-vm/debug/pull` | `{"remotePath","localPath"}` (local confined to APK roots + temp). |
|
|
535
|
+
| POST | `/apk-vm/debug/reboot` | `{"mode":"system"\|"recovery"\|"bootloader"}`. |
|
|
536
|
+
| POST | `/apk-vm/debug/action` | `force-stop` / `clear-data` / `uninstall` by package name. |
|
|
537
|
+
| POST | `/apk-vm/debug/rotate` | `{"orientation":"portrait"\|"landscape"\|"left"\|"right"\|"upside-down"}` or rotation `0..3`; locks the guest rotation. |
|
|
538
|
+
| GET | `/apk-vm/debug/ui` | UIAutomator hierarchy XML (bounded to 500 000 chars) plus node count and guest remote path. |
|
|
539
|
+
| POST | `/apk-vm/debug/record` | `{"seconds":1–60}` (default 5) → `screenrecord` MP4 pulled to the host temp dir; returns `localPath` and byte size; the panel auto-downloads it. |
|
|
540
|
+
| GET | `/apk-vm/artifacts?name=…` | Download a screenshot/recording previously written by the plugin into the OS temp dir (basename whitelist, no arbitrary paths). |
|
|
541
|
+
| GET | `/apk-vm/debug/perf` | load average, `/proc/meminfo` totals/free/available and the first lines of `dumpsys cpuinfo`. |
|
|
542
|
+
| GET/HEAD | `/apk-vm/debug/screenshot` | PNG download (`Content-Disposition: attachment`). |
|
|
543
|
+
|
|
544
|
+
### Agent tool `apk_vm`
|
|
545
|
+
|
|
546
|
+
Registered globally through `ctx.tools`, so every DSH agent can operate the rooted VM directly:
|
|
547
|
+
`info`, `shell`, `logcat`, `getprop`, `setprop`, `packages`, `install`, `push`, `pull`,
|
|
548
|
+
`reboot`, `root`, `force_stop`, `clear_data`, `uninstall`, `rotate`, `ui_dump`, `record`, `perf`,
|
|
549
|
+
`screenshot`, high-level input `tap` / `swipe` / `long_press` / `key` / `text` (device coordinates),
|
|
550
|
+
UI automation `find_ui` / `assert_text` / `tap_text` (UIAutomator text/resource-id/content-desc/class),
|
|
551
|
+
clipboard `clipboard_get` / `clipboard_set`,
|
|
552
|
+
static `apk_info` (metadata without booting a VM),
|
|
553
|
+
device management (`devices`, `attach_device`, `detach_device`),
|
|
554
|
+
plus `start` / `stop` and instance management (`list_instances`, `create_instance`,
|
|
555
|
+
`destroy_instance`). It calls the same Host methods as the HTTP routes and the panel (no
|
|
556
|
+
duplicated logic).
|
|
557
|
+
|
|
558
|
+
For concurrent Agents, pass `instance` to target a specific VM; when omitted, the tool resolves
|
|
559
|
+
the calling Agent's assigned instance (first Agent → default VM, later Agents → isolated
|
|
560
|
+
instances created on demand until `maxInstances`). Each extra instance has its own H.264
|
|
561
|
+
WebSocket (`/apk-vm/instances/<id>/ws`) and falls back to PNG if WebCodecs is unavailable.
|
|
562
|
+
|
|
563
|
+
UI automation is live-verified against a multi-instance VM: `find_ui {"text":"Phone"}` returned
|
|
564
|
+
`Phone @ 102,1121`, `assert_text {"text":"Phone"}` found one node, `tap_text {"text":"Phone"}`
|
|
565
|
+
tapped that center and opened the app, and `key HOME` returned to the launcher.
|
|
566
|
+
|
|
567
|
+
### Panel UI
|
|
568
|
+
|
|
569
|
+
The APK VM panel has an **Environment** card (readiness, missing items, Create AVD / Download
|
|
570
|
+
image / Re-check) and a **Rooted debug** card: root/shell badge, Android/API/ABI line, rooted
|
|
571
|
+
command input (equivalent to `adb shell`), Logcat / **Live logcat** / **Crash log** / Props / Root /
|
|
572
|
+
**UI tree** / **Perf** /
|
|
573
|
+
**Record 5s** / **Rotate** / **Screenshot** buttons, and a bounded output console.
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Compatibility with dsh-better-sidebar
|
|
578
|
+
|
|
579
|
+
[dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) is the community sidebar
|
|
580
|
+
workbench (right-sidebar tabs, bottom workbench, file viewers). When it is active, this plugin
|
|
581
|
+
registers a real tab type through its documented service — no forks, no DOM hacks, no hard
|
|
582
|
+
dependency:
|
|
583
|
+
|
|
584
|
+
- The client half defers with `ctx.inject(['betterSidebar'], …)`, so the plugin keeps working
|
|
585
|
+
unchanged on a plain DSH and simply gains the tab when the service appears.
|
|
586
|
+
- `ctx.betterSidebar.registerTab({ id: 'apk-vm:preview', title, description, icon, order: 60,
|
|
587
|
+
single: true, component })` — the tab appears in the native right Sidebar's **+** menu and in
|
|
588
|
+
the bottom workbench, rendered with the same VM console adapted to a narrow pane (stacked APK
|
|
589
|
+
list, compact header, `visible`-gated frame polling).
|
|
590
|
+
- The main panel gains an **"Open in side bar"** action that calls
|
|
591
|
+
`ctx.betterSidebar.openTab({ type: 'apk-vm:preview', target: 'right' })` for the active
|
|
592
|
+
session (with a clear message when no session is open).
|
|
593
|
+
- Everything degrades cleanly: the service is optional, and a missing/broken service never
|
|
594
|
+
affects the native `main` panel or the left sidebar entry.
|
|
595
|
+
|
|
596
|
+
Live-verified in the reference profile: the `sidebar.right.pane.tab` slot lists the occupant
|
|
597
|
+
`dsh-better-sidebar:apk-vm:preview` (`active: true`). The sandbox test
|
|
598
|
+
[spike/ui-better-sidebar-smoke.cjs](spike/ui-better-sidebar-smoke.cjs) separately asserts
|
|
599
|
+
descriptor fields, localization, disposal, the "already present" path, and resilience when the
|
|
600
|
+
service disappears.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Performance
|
|
605
|
+
|
|
606
|
+
Measured on the **reference host**: Windows x64, AEHD 2.2 hardware acceleration, `-gpu host`
|
|
607
|
+
(NVIDIA RTX 5070 Ti), AOSP `system-images;android-34;default;x86_64` (Android 14 / API 34).
|
|
608
|
+
These are observations from a single machine, not universal guarantees.
|
|
609
|
+
|
|
610
|
+
| Metric | Result |
|
|
611
|
+
| --- | --- |
|
|
612
|
+
| Cold boot (first boot; also after re-saving the snapshot for the current AVD config) | **29.5 s** |
|
|
613
|
+
| Quick boot (snapshot restore) | **8.4 s** measured (`Boot completed in 8371 ms`, `bootKind: snapshot`); **8.4–10.5 s** across runs — inside the 30 s target |
|
|
614
|
+
| Raw H.264 stream over `/apk-vm/ws` (60 Hz animation test app) | **~59.3 fps** (425–474 packets in 8 s, zero protocol errors) |
|
|
615
|
+
| Headless browser software WebCodecs decode | **318 frames / 6.32 s = 50.3 fps** (~57 fps excluding ~0.7 s startup) |
|
|
616
|
+
| Late second client (keyframe recovery) | **234 packets / 4 s (~58.5 fps)** after `RESET_VIDEO` |
|
|
617
|
+
| PNG `screencap` fallback | **~1.1 fps** at 720×1280 (transport limit; not a regression) |
|
|
618
|
+
| Rooted guest | `adb shell id` = `uid=0(root)`; `ro.debuggable=1`; `ro.adb.secure=0`; toolkit installed (755) |
|
|
619
|
+
|
|
620
|
+
FPS is **content-driven**: scrcpy only emits frames when the screen changes, so a static home
|
|
621
|
+
screen legitimately shows a low FPS. The 60 Hz animation test sustains ~59 fps over the wire.
|
|
622
|
+
The `Target FPS` selector applies only to the PNG fallback; the H.264 path is capped by
|
|
623
|
+
`scrcpyMaxFps`.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Verification
|
|
628
|
+
|
|
629
|
+
### Automated checks (CI)
|
|
630
|
+
|
|
631
|
+
[.github/workflows/ci.yml](.github/workflows/ci.yml) runs on `ubuntu-latest` with Node.js 24 on
|
|
632
|
+
every push and pull request (full headless suite), plus a **cross-platform headless matrix**
|
|
633
|
+
(`ubuntu-latest`, `windows-latest`, `macos-latest`) that runs syntax checks, setup detection,
|
|
634
|
+
instance lifecycle and the UI bundle smoke test:
|
|
635
|
+
|
|
636
|
+
- `node --check` on `index.js`, `client.js` and every `lib/*.js` module;
|
|
637
|
+
- [spike/ui-smoke.cjs](spike/ui-smoke.cjs) — 136 sandbox assertions against the real client
|
|
638
|
+
bundle (module wrapper, registration, i18n, rendering states, POST handlers);
|
|
639
|
+
- [spike/ui-better-sidebar-smoke.cjs](spike/ui-better-sidebar-smoke.cjs) — descriptor /
|
|
640
|
+
localization / disposal / fallback assertions for the optional integration;
|
|
641
|
+
- [spike/setup-smoke.mjs](spike/setup-smoke.mjs) — 30 hermetic environment-detection /
|
|
642
|
+
create-avd / install-image / setup-report checks (mocked OS layouts and `exec`, no SDK needed);
|
|
643
|
+
- [spike/android/mock-device.mjs](spike/android/mock-device.mjs) — device critical path against
|
|
644
|
+
a mock adb (24 checks), including real `aapt2` APK metadata parsing;
|
|
645
|
+
- [spike/android/launch-plan.mjs](spike/android/launch-plan.mjs) — fast-boot argv plans
|
|
646
|
+
(snapshot restore, fail-fast without a hypervisor, explicit TCG, `-no-snapshot`).
|
|
647
|
+
|
|
648
|
+
**No Android emulator or AVD is required in CI.** Emulator-dependent integration tests are
|
|
649
|
+
**Windows-local only** and are not suitable for hosted CI runners (nested virtualization is not
|
|
650
|
+
available on GitHub-hosted runners).
|
|
651
|
+
|
|
652
|
+
### Reference-host verification
|
|
653
|
+
|
|
654
|
+
| Check | Result |
|
|
655
|
+
| --- | --- |
|
|
656
|
+
| Bundle install via DSH `plugin_manager install_bundle` | `application: applied`, no warnings |
|
|
657
|
+
| Sidebar registration (`sidebar.panellist`, id `apk-vm`, order 30) | active in the live slot tree |
|
|
658
|
+
| Main panel registration (`main`, key `apk-vm`) | active in the live slot tree |
|
|
659
|
+
| `GET /apk-vm/state` / `GET /apk-vm/apks` | `200 {"ok":true,…}`; discovered the workspace `sample.apk` and user APKs |
|
|
660
|
+
| `GET /apk-vm/frame` while stopped | `409` with JSON error (as specified) |
|
|
661
|
+
| Client bundle | `node --check` OK; 136/136 sandbox checks pass |
|
|
662
|
+
| dsh-better-sidebar tab | live `sidebar.right.pane.tab` occupant `dsh-better-sidebar:apk-vm:preview`; smoke test passes |
|
|
663
|
+
| Host/backend modules | `node --check` OK; `lib/android.js` smoke tests 11/11; mock-device 24/24 |
|
|
664
|
+
| Environment detection | `spike/setup-smoke.mjs` 30/30 (hermetic, runs on CI) |
|
|
665
|
+
| scrcpy relay | 160 assertions incl. a fake-device end-to-end protocol test; real emulator measured ~59 fps |
|
|
666
|
+
| H.264 / WebCodecs | raw stream 59.3 fps; headless software decode 50.3 fps; late-client keyframe recovery works |
|
|
667
|
+
| Fast boot | cold 29.5 s; quick boot 8.4–10.5 s (`bootKind: snapshot`) |
|
|
668
|
+
| Rooted guest / debug API | `root:true`, `debugPrepared:true`; `/debug/info`, `/debug/shell`, `/debug/logcat`, `/debug/prop`, `/debug/packages`, snapshot save all returned `ok` |
|
|
669
|
+
| Agent tool `apk_vm` | registered through `ctx.tools`; DSH reported the updated tool schema |
|
|
670
|
+
|
|
671
|
+
The APK upload, environment card, and the added device actions (rotate / UI tree / record / perf /
|
|
672
|
+
screenshot download) are implemented in this release and covered by the syntax and headless
|
|
673
|
+
suites where applicable; the measured table above only lists values that were actually observed.
|
|
674
|
+
No screenshots are published yet (see [Demo](#demo)).
|
|
675
|
+
|
|
676
|
+
Run the same headless suites locally (Windows or any OS) from the repository root:
|
|
677
|
+
|
|
678
|
+
```bash
|
|
679
|
+
node --check index.js
|
|
680
|
+
node --check client.js
|
|
681
|
+
for f in lib/*.js; do node --check "$f"; done
|
|
682
|
+
node spike/ui-smoke.cjs
|
|
683
|
+
node spike/ui-better-sidebar-smoke.cjs
|
|
684
|
+
node spike/setup-smoke.mjs
|
|
685
|
+
node spike/android/mock-device.mjs
|
|
686
|
+
node spike/android/launch-plan.mjs
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
`spike/android/mock-device.mjs` needs a real `aapt2`; install Android SDK build-tools or point
|
|
690
|
+
`ANDROID_HOME` / `ANDROID_SDK_ROOT` at an SDK that has them.
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## Troubleshooting
|
|
695
|
+
|
|
696
|
+
### The Environment card says "Setup needed"
|
|
697
|
+
|
|
698
|
+
The report lists each missing item: SDK path, `adb`, emulator, `sdkmanager`/`avdmanager`, the
|
|
699
|
+
configured system image, the AVD, or CPU acceleration.
|
|
700
|
+
|
|
701
|
+
- Missing SDK/emulator: install Android SDK packages and/or set `sdkRoot` (or `ANDROID_HOME` /
|
|
702
|
+
`ANDROID_SDK_ROOT`).
|
|
703
|
+
- Missing system image: install it with `sdkmanager`, or set `allowSetupDownloads: true` and use
|
|
704
|
+
**Download image** in the card (large download; never automatic).
|
|
705
|
+
- Missing AVD: use **Create AVD** in the card (requires the system image and `avdmanager`) or run
|
|
706
|
+
`avdmanager create avd -n dsh_apk -k "system-images;android-34;default;x86_64"`.
|
|
707
|
+
- Missing acceleration: see the next section.
|
|
708
|
+
|
|
709
|
+
### "No usable CPU hypervisor (AEHD/WHPX) was found"
|
|
710
|
+
|
|
711
|
+
The plugin fails fast (`requireAccel: true` by default) because an x86_64 system image cannot
|
|
712
|
+
boot under pure software TCG. Check with `"$ANDROID_HOME/emulator/emulator" -accel-check`, then:
|
|
713
|
+
|
|
714
|
+
- **Windows (AEHD):** install the `extras;google;Android_Emulator_Hypervisor_Driver` package or
|
|
715
|
+
the standalone AEHD installer, then reboot. Conflicts with Hyper-V/WHPX, WSL2, memory
|
|
716
|
+
integrity/VBS, or vendor emulator drivers (MuMu, BlueStacks, LDPlayer, Nox) can prevent AEHD
|
|
717
|
+
from loading or cause an installer error such as **1450** — stop/disable the competing
|
|
718
|
+
driver, reboot, and retry. Do not run two hypervisor stacks at the same time.
|
|
719
|
+
- **Windows (WHPX):** enable the *Windows Hypervisor Platform* feature and reboot. If the
|
|
720
|
+
Windows component store is damaged, enabling WHPX can fail (for example DISM error **14098**);
|
|
721
|
+
repair Windows or use AEHD instead.
|
|
722
|
+
- **Linux:** enable KVM in the firmware, load the KVM module, and make sure your user can open
|
|
723
|
+
`/dev/kvm`.
|
|
724
|
+
- **macOS:** HVF is built in; make sure the machine is not running inside another VM.
|
|
725
|
+
|
|
726
|
+
Setting `requireAccel: false` / `accel: off` is only useful for diagnostics — the x86_64 guest
|
|
727
|
+
is not expected to finish booting that way.
|
|
728
|
+
|
|
729
|
+
### AEHD installed but the emulator is slow or refuses to start
|
|
730
|
+
|
|
731
|
+
- Confirm the driver is running (`sc query aehd` on Windows) and that virtualization is enabled
|
|
732
|
+
in the BIOS/UEFI.
|
|
733
|
+
- A stale vendor driver (for example MuMu's `MuMuVMMDrv`) can hold AMD-V/VT-x. Stop/disable it
|
|
734
|
+
and reboot. (Restore it later if you still use that emulator.)
|
|
735
|
+
- A reboot is required after changing hypervisor-related Windows features or drivers.
|
|
736
|
+
|
|
737
|
+
### Emulator / scrcpy port conflicts
|
|
738
|
+
|
|
739
|
+
| Port | Used for | Fix |
|
|
740
|
+
| --- | --- | --- |
|
|
741
|
+
| `5558` (`port`) | Emulator console port | Change `port`, or close the stale emulator/adb process. |
|
|
742
|
+
| `27183` (`scrcpyPort`) | Local adb forward for the scrcpy tunnel | Change `scrcpyPort`. |
|
|
743
|
+
| `5037` | adb server | `adb kill-server` if a stale or foreign adb server owns it. |
|
|
744
|
+
|
|
745
|
+
### Quick boot fails: "different AVD configuration", black screen, or no snapshot
|
|
746
|
+
|
|
747
|
+
- The snapshot is only valid for the AVD configuration that created it. After changing the AVD
|
|
748
|
+
(API level, ABI, resolution, GPU mode, `-writable-system`, …) delete the old snapshot and let
|
|
749
|
+
the plugin create a new one: remove
|
|
750
|
+
`~/.android/avd/<avdName>.avd/snapshots/<snapshotName>` (on Windows,
|
|
751
|
+
`%USERPROFILE%\.android\avd\<avdName>.avd\snapshots\<snapshotName>`), then start cold again.
|
|
752
|
+
- A snapshot directory with `ram.img` but no `snapshot.pb` is not a valid snapshot; the plugin
|
|
753
|
+
ignores it. Delete it or set `quickBoot: false` once to force a clean cold boot.
|
|
754
|
+
- If a stale offline adb emulator is attached, `adb kill-server` (or kill the leftover
|
|
755
|
+
`emulator`/`qemu-system-*` process) and retry.
|
|
756
|
+
|
|
757
|
+
### APK upload fails
|
|
758
|
+
|
|
759
|
+
- The upload endpoint accepts raw `.apk`/ZIP bytes up to **500 MiB**; larger files return `413`
|
|
760
|
+
and non-ZIP content returns `400`. Use **Install from path** for unusual cases.
|
|
761
|
+
- Only one install (path install or upload) can run at a time — a concurrent request returns
|
|
762
|
+
`409`; wait for the current install to finish.
|
|
763
|
+
- A file that installs on a phone can still be rejected by the emulator (ABI/split APK); the
|
|
764
|
+
error is shown in the panel and in `/apk-vm/state`.
|
|
765
|
+
|
|
766
|
+
### The sidebar panel does not update after install/upgrade
|
|
767
|
+
|
|
768
|
+
- Reload the DSH Web page so the new `client.js` bundle is loaded; a long-lived tab may keep the
|
|
769
|
+
old bundle cached.
|
|
770
|
+
- If API requests return `401`/`403` after a DSH restart, sign in / refresh the session — the
|
|
771
|
+
DSH Web GUI requires its auth token, and the plugin routes live behind the same server.
|
|
772
|
+
- Check the plugin log tail in the panel (`/apk-vm/state` → `vm.logTail`) and the DSH host log.
|
|
773
|
+
|
|
774
|
+
### Low FPS / a blank canvas
|
|
775
|
+
|
|
776
|
+
- A static screen legitimately shows low FPS — scrcpy only encodes frames when the display
|
|
777
|
+
changes. Measure with an animation, not the home screen.
|
|
778
|
+
- A blank canvas that never receives a keyframe: reload the page; the panel requests
|
|
779
|
+
`RESET_VIDEO` on connect. If the browser has no `WebCodecs`/`WebSocket`, the panel falls back
|
|
780
|
+
to PNG polling (~1 fps) automatically.
|
|
781
|
+
- The `Target FPS` selector changes only the PNG fallback; use `scrcpyMaxFps` for H.264.
|
|
782
|
+
|
|
783
|
+
### APK is not listed / `aapt2` errors
|
|
784
|
+
|
|
785
|
+
- Add the project directory to `roots`; the DSH working directory, `process.cwd()` and the home
|
|
786
|
+
directory are searched automatically.
|
|
787
|
+
- Install Android SDK `build-tools` (provides `aapt2`) and make sure `ANDROID_HOME` /
|
|
788
|
+
`ANDROID_SDK_ROOT` points at the SDK, or set the SDK root via config.
|
|
789
|
+
- A malformed or non-APK file is rejected by `aapt2 dump badging`; the panel surfaces the error.
|
|
790
|
+
|
|
791
|
+
### macOS / Linux notes
|
|
792
|
+
|
|
793
|
+
SDK/adb/emulator/AVD discovery covers the standard per-OS layouts (including Homebrew and
|
|
794
|
+
`/opt` style installs), but end-to-end emulator + streaming behavior has only been verified on
|
|
795
|
+
Windows x64 so far. If you hit a platform issue, include `/apk-vm/setup` output in the bug
|
|
796
|
+
report — it lists exactly which paths were detected.
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## Security
|
|
801
|
+
|
|
802
|
+
This plugin intentionally exposes powerful local control over an Android emulator. Read this
|
|
803
|
+
before pointing the DSH Web UI at an untrusted network.
|
|
804
|
+
|
|
805
|
+
- **The plugin manages processes on your machine.** It spawns the Android emulator, `adb`,
|
|
806
|
+
`sdkmanager` and `avdmanager` with `child_process.spawn(exe, argv)` — never through a shell —
|
|
807
|
+
so user-supplied strings are not shell-interpreted. Paths are validated (APK paths must exist
|
|
808
|
+
and end in `.apk`; push/pull paths are restricted to `/data/local/tmp`, `/sdcard`,
|
|
809
|
+
`/storage/emulated/0` and allowed local roots).
|
|
810
|
+
- **The guest is deliberately rooted and debuggable.** `adb root`, `ro.debuggable=1` and
|
|
811
|
+
`ro.adb.secure=0` mean anything you install inside the VM is not sandboxed from root. Do not
|
|
812
|
+
put real secrets or personal data in the AVD.
|
|
813
|
+
- **The debug HTTP API is a local root shell for the guest.** `/apk-vm/debug/shell` runs
|
|
814
|
+
arbitrary commands *inside the emulator* (not on the host), and `/apk-vm/debug/action` can
|
|
815
|
+
uninstall packages. Requests are same-origin only and bounded, but any page/extension that can
|
|
816
|
+
make same-origin requests to your DSH Web server can exercise them.
|
|
817
|
+
- **Upload and environment setup are privileged local actions.** Upload accepts only APK/ZIP
|
|
818
|
+
bytes up to 500 MiB and installs them into the guest; `create-avd` writes an AVD; and
|
|
819
|
+
`install-image` runs `sdkmanager --install` only when the user opted in with
|
|
820
|
+
`allowSetupDownloads: true` (default `false`). All of these are reachable only from the same
|
|
821
|
+
origin.
|
|
822
|
+
- **The `apk_vm` Agent tool is available to every agent in the DSH profile.** Grant agent access
|
|
823
|
+
to this plugin with the same care as giving an agent a terminal on an Android device.
|
|
824
|
+
- **Loopback-only by default.** In addition to the same-origin check, every `/apk-vm/*` HTTP
|
|
825
|
+
route and the WebSocket upgrade reject non-loopback clients unless `allowRemote: true` is
|
|
826
|
+
explicitly set (for use behind your own authenticated reverse proxy). This means exposing the
|
|
827
|
+
DSH Web UI on the LAN does not automatically expose the root debug API.
|
|
828
|
+
- **Keep the DSH Web server local.** If you expose the DSH Web UI beyond localhost (reverse
|
|
829
|
+
proxy, LAN bind, tunnel), you also expose the plugin routes. Add authentication at the proxy
|
|
830
|
+
layer or keep the port local; do not disable the same-origin/loopback checks.
|
|
831
|
+
- **No telemetry.** The plugin makes no network requests of its own to third-party services;
|
|
832
|
+
it only talks to the local adb/emulator and the same-origin DSH Web server (the opt-in
|
|
833
|
+
`install-image` action runs the user's own `sdkmanager`, which does contact Google's SDK
|
|
834
|
+
repository).
|
|
835
|
+
|
|
836
|
+
To report a vulnerability, follow [SECURITY.md](SECURITY.md) — please use private disclosure,
|
|
837
|
+
not a public issue.
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## Roadmap
|
|
842
|
+
|
|
843
|
+
The features below are **not shipped yet** — they are tracked for future releases. Everything
|
|
844
|
+
described above is implemented in this release.
|
|
845
|
+
|
|
846
|
+
- **Physical-device switching** — first-class selection/attachment of adb-attached physical
|
|
847
|
+
devices from the panel (config `serial` already supports attaching one; multi-AVD switching is
|
|
848
|
+
shipped via `/apk-vm/avds` + `/apk-vm/avd` and the Environment card).
|
|
849
|
+
- **Audio streaming** — forward guest audio alongside the video stream.
|
|
850
|
+
- **Cloud / remote VM execution** — run the guest on a remote host and stream it to the panel.
|
|
851
|
+
- **Static APK preview** *(optional)* — inspect an APK's manifest, resources and launcher UI
|
|
852
|
+
without booting a full VM.
|
|
853
|
+
|
|
854
|
+
QEMU + Android-x86 for hosts without hardware virtualization and `-writable-system` image
|
|
855
|
+
customization remain research notes in [DESIGN.md](DESIGN.md) §8.
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## Known limitations
|
|
860
|
+
|
|
861
|
+
- **Windows x64 is the only end-to-end verified platform.** SDK/adb/emulator/AVD discovery is
|
|
862
|
+
implemented for Windows, macOS and Linux, but emulator + H.264 behavior on macOS/Linux (and
|
|
863
|
+
`arm64-v8a` images on Apple Silicon) is unverified.
|
|
864
|
+
- **Hardware virtualization is mandatory** for the x86_64 AOSP image; pure TCG fails by design.
|
|
865
|
+
- **Quick boot is snapshot-based.** Changing AVD configuration invalidates the snapshot; recreate
|
|
866
|
+
it as described in [Troubleshooting](#quick-boot-fails-different-avd-configuration-black-screen-or-no-snapshot).
|
|
867
|
+
- **H.264 FPS is content-driven.** Static screens show low FPS; the PNG fallback is ~1 fps by
|
|
868
|
+
transport limitation.
|
|
869
|
+
- **The rooted debug API is local-only by design.** Do not expose the DSH Web port to untrusted
|
|
870
|
+
networks.
|
|
871
|
+
- **No audio and no physical-device switching yet** (multi-AVD switching is available; see
|
|
872
|
+
[Roadmap](#roadmap)).
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
## Contributing
|
|
877
|
+
|
|
878
|
+
Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) for the development
|
|
879
|
+
workflow, repository layout, the checks to run before a PR, and the rules for the UI half
|
|
880
|
+
(theme tokens, localization, no new npm dependencies). All participants are expected to follow
|
|
881
|
+
[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Security issues should follow [SECURITY.md](SECURITY.md).
|
|
882
|
+
By contributing you agree that your contributions are licensed under the repository's Apache-2.0
|
|
883
|
+
terms. Release mechanics are in [docs/RELEASING.md](docs/RELEASING.md).
|
|
884
|
+
|
|
885
|
+
Quick reference for a change:
|
|
886
|
+
|
|
887
|
+
```bash
|
|
888
|
+
node --check index.js && node --check client.js
|
|
889
|
+
for f in lib/*.js; do node --check "$f"; done
|
|
890
|
+
node spike/ui-smoke.cjs
|
|
891
|
+
node spike/ui-better-sidebar-smoke.cjs
|
|
892
|
+
node spike/setup-smoke.mjs
|
|
893
|
+
node spike/android/mock-device.mjs
|
|
894
|
+
node spike/android/launch-plan.mjs
|
|
895
|
+
node spike/instances-smoke.mjs
|
|
896
|
+
node scripts/privacy-scan.mjs
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Use [.github/ISSUE_TEMPLATE/bug_report.yml](.github/ISSUE_TEMPLATE/bug_report.yml) for bugs and
|
|
900
|
+
[.github/ISSUE_TEMPLATE/feature_request.yml](.github/ISSUE_TEMPLATE/feature_request.yml) for
|
|
901
|
+
features. Emulator integration testing is Windows-local only (see
|
|
902
|
+
[CONTRIBUTING.md](CONTRIBUTING.md#testing)); hosted CI runs the headless suites only.
|
|
903
|
+
|
|
904
|
+
---
|
|
905
|
+
|
|
906
|
+
## License
|
|
907
|
+
|
|
908
|
+
Licensed under the **Apache License, Version 2.0** — see [LICENSE](LICENSE). Third-party
|
|
909
|
+
components and attribution are listed in [NOTICE](NOTICE).
|
|
910
|
+
|
|
911
|
+
- **scrcpy** — © Genymobile and contributors, Apache-2.0. The server JAR
|
|
912
|
+
`lib/vendor/scrcpy-server-v5.0.1.jar` is redistributed unmodified under that license and is
|
|
913
|
+
pushed to the guest at runtime; the desktop scrcpy client is not used.
|
|
914
|
+
- **AOSP Android Emulator / system images / adb** — © The Android Open Source Project,
|
|
915
|
+
Apache-2.0. They are installed separately through the Android SDK and are not bundled in this
|
|
916
|
+
repository.
|
|
917
|
+
- **dsh-better-sidebar** — a separate community plugin (not bundled here); the optional
|
|
918
|
+
integration uses its published service only.
|
|
919
|
+
|
|
920
|
+
This project is not affiliated with or endorsed by Genymobile, the Android Open Source Project,
|
|
921
|
+
or the DeepSeek Harness project. "Android" is a trademark of Google LLC.
|