dsh-apk-vm 0.4.1 → 0.5.0
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 +18 -0
- package/README.md +27 -1
- package/client.js +224 -0
- package/index.js +137 -4
- package/lib/storage.js +296 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
Storage management.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Storage panel: AVD / snapshot / SDK / plugin temp / free-disk footprint, with guarded
|
|
10
|
+
cleanup actions (two-click confirmation for destructive actions).
|
|
11
|
+
- `GET /apk-vm/storage` and `POST /apk-vm/storage` (`temp`, `snapshots`, `orphan-avds`).
|
|
12
|
+
- `apk_vm` actions `storage` and `clean_storage`.
|
|
13
|
+
- `lib/storage.js` with dependency-free size accounting and cleanup helpers; unit coverage for
|
|
14
|
+
temp cleanup, orphan AVD detection and snapshot-aware accounting.
|
|
15
|
+
|
|
16
|
+
### Notes
|
|
17
|
+
|
|
18
|
+
- Snapshot cleanup only targets a stopped plugin-managed AVD; the active VM is never deleted.
|
|
19
|
+
- Orphan cleanup only matches `dsh_apk_*` AVDs that are not registered to an instance.
|
|
20
|
+
|
|
3
21
|
## 0.4.1
|
|
4
22
|
|
|
5
23
|
Privacy cleanup for the published proof assets.
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ same-origin, loopback-only debug HTTP API and an `apk_vm` Agent tool.
|
|
|
24
24
|
> 当前 **Windows x64 与 Linux x86_64(headless)为端到端已验证平台**;macOS 仅做探测/CI
|
|
25
25
|
> 覆盖,详见 `docs/LINUX-E2E.md`。
|
|
26
26
|
|
|
27
|
-
**Status:** `0.
|
|
27
|
+
**Status:** `0.5.0` · Apache-2.0 · no npm runtime dependencies · no build step.
|
|
28
28
|
|
|
29
29
|
This is an open-source DSH plugin published on npm as **`dsh-apk-vm`**. Repository:
|
|
30
30
|
[github.com/yuxiaole-bili/dsh-apk-vm](https://github.com/yuxiaole-bili/dsh-apk-vm).
|
|
@@ -174,6 +174,10 @@ key {"text":"HOME"} -> returned to launcher
|
|
|
174
174
|
(`settings put global http_proxy`) and installs a local `.pem`/`.crt` CA into the rooted
|
|
175
175
|
Android user CA store with the correct OpenSSL subject-hash filename; the system CA store is
|
|
176
176
|
updated when the guest allows a remount.
|
|
177
|
+
- **Storage panel + guarded cleanup.** A Storage card reports the AVD, snapshot, SDK, temp-file
|
|
178
|
+
and free-disk footprint and offers explicit cleanups: plugin temp files, quick-boot
|
|
179
|
+
snapshots for a stopped AVD, and orphan `dsh_apk_*` AVDs. Destructive actions need a second
|
|
180
|
+
click and never touch the active VM.
|
|
177
181
|
- **Installed-app management.** The panel's **Installed apps** card lists third-party packages
|
|
178
182
|
for the active instance and offers force-stop, clear-data and uninstall; it uses the same
|
|
179
183
|
rooted `debug/packages` + `debug/action` API as the Agent tool.
|
|
@@ -237,6 +241,28 @@ key {"text":"HOME"} -> returned to launcher
|
|
|
237
241
|
- `POST /apk-vm/debug/ca` accepts a host `.pem`/`.crt` path and installs it under the guest's
|
|
238
242
|
user CA directory with the correct subject hash.
|
|
239
243
|
|
|
244
|
+
## Storage management
|
|
245
|
+
|
|
246
|
+
`GET /apk-vm/storage` reports the logical size of every AVD, its quick-boot snapshots, the
|
|
247
|
+
Android SDK, plugin temp files and the free space on the AVD volume. The panel's **Storage**
|
|
248
|
+
card uses the same endpoint.
|
|
249
|
+
|
|
250
|
+
`POST /apk-vm/storage` accepts:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{ "action": "temp" }
|
|
254
|
+
{ "action": "snapshots", "avdName": "dsh_apk", "confirm": true }
|
|
255
|
+
{ "action": "orphan-avds", "confirm": true }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- `temp` removes only screenshots, MP4 recordings, uploaded APKs and flow JSON created by this
|
|
259
|
+
plugin.
|
|
260
|
+
- `snapshots` deletes the quick-boot snapshots of a **stopped** plugin-managed AVD; the next
|
|
261
|
+
boot is a cold boot.
|
|
262
|
+
- `orphan-avds` removes only `dsh_apk_*` AVDs that are not registered to any instance and are
|
|
263
|
+
not active.
|
|
264
|
+
- `apk_vm` tool actions: `storage`, `clean_storage` (`storageAction`, `avdName`, `confirm`).
|
|
265
|
+
|
|
240
266
|
## Reproducible demo
|
|
241
267
|
|
|
242
268
|
A public, dependency-free demo lives in [examples](examples/README.md):
|
package/client.js
CHANGED
|
@@ -200,6 +200,21 @@ window.__ModuleLoader__.load({
|
|
|
200
200
|
'flow.saved': 'Saved {name}',
|
|
201
201
|
'flow.done': 'Replayed {name}',
|
|
202
202
|
'flow.count': '{n} step(s)',
|
|
203
|
+
'storage.title': 'Storage',
|
|
204
|
+
'storage.refresh': 'Refresh',
|
|
205
|
+
'storage.cleanTemp': 'Clean temp',
|
|
206
|
+
'storage.cleanSnapshots': 'Clean snapshots',
|
|
207
|
+
'storage.cleanOrphans': 'Clean orphan AVDs',
|
|
208
|
+
'storage.confirm': 'Click again to confirm',
|
|
209
|
+
'storage.freed': 'Freed {size}',
|
|
210
|
+
'storage.avds': '{size} AVDs',
|
|
211
|
+
'storage.snapshots': 'Snapshots',
|
|
212
|
+
'storage.sdk': 'SDK',
|
|
213
|
+
'storage.temp': 'Temp',
|
|
214
|
+
'storage.free': 'Free disk',
|
|
215
|
+
'storage.active': 'active',
|
|
216
|
+
'storage.orphan': 'orphan',
|
|
217
|
+
'storage.empty': 'No AVDs found',
|
|
203
218
|
'apk.bundle': 'Bundle',
|
|
204
219
|
'setup.title': 'Environment',
|
|
205
220
|
'setup.ok': 'Ready',
|
|
@@ -398,6 +413,21 @@ window.__ModuleLoader__.load({
|
|
|
398
413
|
'flow.saved': '已保存 {name}',
|
|
399
414
|
'flow.done': '已回放 {name}',
|
|
400
415
|
'flow.count': '{n} 步',
|
|
416
|
+
'storage.title': '存储',
|
|
417
|
+
'storage.refresh': '刷新',
|
|
418
|
+
'storage.cleanTemp': '清理临时文件',
|
|
419
|
+
'storage.cleanSnapshots': '清理快照',
|
|
420
|
+
'storage.cleanOrphans': '清理孤立 AVD',
|
|
421
|
+
'storage.confirm': '再点一次确认',
|
|
422
|
+
'storage.freed': '已释放 {size}',
|
|
423
|
+
'storage.avds': '{size} 个 AVD',
|
|
424
|
+
'storage.snapshots': '快照',
|
|
425
|
+
'storage.sdk': 'SDK',
|
|
426
|
+
'storage.temp': '临时文件',
|
|
427
|
+
'storage.free': '磁盘剩余',
|
|
428
|
+
'storage.active': '运行中',
|
|
429
|
+
'storage.orphan': '孤立',
|
|
430
|
+
'storage.empty': '没有找到 AVD',
|
|
401
431
|
'apk.bundle': '安装包',
|
|
402
432
|
'setup.title': '环境',
|
|
403
433
|
'setup.ok': '就绪',
|
|
@@ -1207,6 +1237,19 @@ window.__ModuleLoader__.load({
|
|
|
1207
1237
|
var flowBusy = flowBusyState[0];
|
|
1208
1238
|
var setFlowBusy = flowBusyState[1];
|
|
1209
1239
|
|
|
1240
|
+
var storageState = React.useState(null);
|
|
1241
|
+
var storage = storageState[0];
|
|
1242
|
+
var setStorage = storageState[1];
|
|
1243
|
+
var storageBusyState = React.useState('');
|
|
1244
|
+
var storageBusy = storageBusyState[0];
|
|
1245
|
+
var setStorageBusy = storageBusyState[1];
|
|
1246
|
+
var storageErrorState = React.useState('');
|
|
1247
|
+
var storageError = storageErrorState[0];
|
|
1248
|
+
var setStorageError = storageErrorState[1];
|
|
1249
|
+
var storageConfirmState = React.useState('');
|
|
1250
|
+
var storageConfirm = storageConfirmState[0];
|
|
1251
|
+
var setStorageConfirm = storageConfirmState[1];
|
|
1252
|
+
|
|
1210
1253
|
var proxyState = React.useState({ host: '', port: '' });
|
|
1211
1254
|
var proxy = proxyState[0];
|
|
1212
1255
|
var setProxy = proxyState[1];
|
|
@@ -1392,6 +1435,7 @@ window.__ModuleLoader__.load({
|
|
|
1392
1435
|
loadInstances();
|
|
1393
1436
|
loadDevices();
|
|
1394
1437
|
loadFlows();
|
|
1438
|
+
loadStorage();
|
|
1395
1439
|
var pollId = setInterval(function () {
|
|
1396
1440
|
if (stateBusyRef.current) return;
|
|
1397
1441
|
stateBusyRef.current = true;
|
|
@@ -2173,6 +2217,67 @@ window.__ModuleLoader__.load({
|
|
|
2173
2217
|
});
|
|
2174
2218
|
}
|
|
2175
2219
|
|
|
2220
|
+
function loadStorage() {
|
|
2221
|
+
setStorageBusy('load');
|
|
2222
|
+
setStorageError('');
|
|
2223
|
+
return requestJson('/apk-vm/storage', null, T).then(function (result) {
|
|
2224
|
+
if (!mountedRef.current) return result;
|
|
2225
|
+
setStorageBusy('');
|
|
2226
|
+
if (!result.ok) {
|
|
2227
|
+
setStorageError(result.error || T('err.action'));
|
|
2228
|
+
return result;
|
|
2229
|
+
}
|
|
2230
|
+
setStorage(isObject(result.data) ? result.data : null);
|
|
2231
|
+
return result;
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function storageSnapshotAvd() {
|
|
2236
|
+
var list = storage && Array.isArray(storage.avds) ? storage.avds : [];
|
|
2237
|
+
var current = snapshot && snapshot.vm && snapshot.vm.avd ? String(snapshot.vm.avd) : '';
|
|
2238
|
+
var match = list.find(function (item) {
|
|
2239
|
+
return item.name === current;
|
|
2240
|
+
});
|
|
2241
|
+
if (match) return match.name;
|
|
2242
|
+
var active = list.find(function (item) {
|
|
2243
|
+
return item.active && item.managed;
|
|
2244
|
+
});
|
|
2245
|
+
if (active) return active.name;
|
|
2246
|
+
var managed = list.find(function (item) {
|
|
2247
|
+
return item.managed;
|
|
2248
|
+
});
|
|
2249
|
+
return managed ? managed.name : 'dsh_apk';
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function cleanStorage(action) {
|
|
2253
|
+
if (storageBusy) return;
|
|
2254
|
+
var destructive = action === 'snapshots' || action === 'orphan-avds';
|
|
2255
|
+
if (destructive && storageConfirm !== action) {
|
|
2256
|
+
setStorageConfirm(action);
|
|
2257
|
+
pushToast('success', T('storage.confirm'));
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
setStorageConfirm('');
|
|
2261
|
+
setStorageBusy(action);
|
|
2262
|
+
var payload = { action: action };
|
|
2263
|
+
if (action === 'snapshots') {
|
|
2264
|
+
payload.avdName = storageSnapshotAvd();
|
|
2265
|
+
payload.confirm = true;
|
|
2266
|
+
}
|
|
2267
|
+
if (action === 'orphan-avds') payload.confirm = true;
|
|
2268
|
+
requestJson('/apk-vm/storage', { method: 'POST', body: JSON.stringify(payload) }, T).then(function (result) {
|
|
2269
|
+
if (!mountedRef.current) return;
|
|
2270
|
+
setStorageBusy('');
|
|
2271
|
+
if (!result.ok) {
|
|
2272
|
+
pushToast('error', T('err.action') + ': ' + result.error);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
var freed = result.data && Number(result.data.freedBytes) ? Number(result.data.freedBytes) : 0;
|
|
2276
|
+
pushToast('success', T('storage.freed', { size: formatBytes(freed) }));
|
|
2277
|
+
loadStorage();
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2176
2281
|
function refreshAll() {
|
|
2177
2282
|
loadState();
|
|
2178
2283
|
loadApks();
|
|
@@ -3814,6 +3919,124 @@ window.__ModuleLoader__.load({
|
|
|
3814
3919
|
)
|
|
3815
3920
|
);
|
|
3816
3921
|
|
|
3922
|
+
var storageCard = h(
|
|
3923
|
+
'details',
|
|
3924
|
+
{ className: 'apkvm-card apkvm-debug' },
|
|
3925
|
+
h(
|
|
3926
|
+
'summary',
|
|
3927
|
+
{ className: 'apkvm-debug-summary' },
|
|
3928
|
+
h('span', { className: 'apkvm-card-title' }, T('storage.title')),
|
|
3929
|
+
h(
|
|
3930
|
+
'span',
|
|
3931
|
+
{ className: 'apkvm-dim', style: { marginLeft: 'auto' } },
|
|
3932
|
+
storage ? formatBytes((storage.totals && storage.totals.avdBytes) || 0) : '…'
|
|
3933
|
+
),
|
|
3934
|
+
h('span', { className: 'apkvm-chevron', 'aria-hidden': 'true' }, '▸')
|
|
3935
|
+
),
|
|
3936
|
+
h(
|
|
3937
|
+
'div',
|
|
3938
|
+
{ className: 'apkvm-debug-body' },
|
|
3939
|
+
!storage
|
|
3940
|
+
? h('div', { className: 'apkvm-dim' }, storageError || T('storage.empty'))
|
|
3941
|
+
: h(
|
|
3942
|
+
'div',
|
|
3943
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '5px', minWidth: 0 } },
|
|
3944
|
+
h(
|
|
3945
|
+
'div',
|
|
3946
|
+
{ className: 'apkvm-dim', style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
|
|
3947
|
+
h('span', null, T('storage.avds', { size: (storage.avds || []).length }) + ': ' + formatBytes((storage.totals && storage.totals.avdBytes) || 0)),
|
|
3948
|
+
h('span', null, T('storage.snapshots') + ': ' + formatBytes((storage.totals && storage.totals.snapshotBytes) || 0)),
|
|
3949
|
+
h('span', null, T('storage.sdk') + ': ' + formatBytes((storage.totals && storage.totals.sdkBytes) || 0)),
|
|
3950
|
+
h('span', null, T('storage.temp') + ': ' + formatBytes((storage.totals && storage.totals.tempBytes) || 0)),
|
|
3951
|
+
storage.disk
|
|
3952
|
+
? h('span', null, T('storage.free') + ': ' + formatBytes(storage.disk.freeBytes))
|
|
3953
|
+
: null
|
|
3954
|
+
),
|
|
3955
|
+
(storage.avds || []).slice(0, 4).map(function (avd) {
|
|
3956
|
+
return h(
|
|
3957
|
+
'div',
|
|
3958
|
+
{
|
|
3959
|
+
key: 'storage-' + avd.name,
|
|
3960
|
+
className: 'apkvm-apk',
|
|
3961
|
+
style: { padding: '5px 7px', gap: '3px' },
|
|
3962
|
+
},
|
|
3963
|
+
h(
|
|
3964
|
+
'div',
|
|
3965
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 } },
|
|
3966
|
+
h('span', { className: 'apkvm-apk-name', title: avd.path }, avd.name),
|
|
3967
|
+
avd.active ? h('span', { className: 'apkvm-tag ok' }, T('storage.active')) : null,
|
|
3968
|
+
avd.orphan ? h('span', { className: 'apkvm-tag' }, T('storage.orphan')) : null
|
|
3969
|
+
),
|
|
3970
|
+
h(
|
|
3971
|
+
'div',
|
|
3972
|
+
{ className: 'apkvm-dim' },
|
|
3973
|
+
formatBytes(avd.totalBytes || 0) +
|
|
3974
|
+
' · ' +
|
|
3975
|
+
T('storage.snapshots') +
|
|
3976
|
+
' ' +
|
|
3977
|
+
formatBytes(avd.snapshotBytes || 0)
|
|
3978
|
+
)
|
|
3979
|
+
);
|
|
3980
|
+
}),
|
|
3981
|
+
h(
|
|
3982
|
+
'div',
|
|
3983
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '5px', flexWrap: 'wrap' } },
|
|
3984
|
+
actionBtn(storageBusy === 'load' ? '…' : T('storage.refresh'), {
|
|
3985
|
+
key: 'storage-refresh',
|
|
3986
|
+
small: true,
|
|
3987
|
+
disabled: Boolean(storageBusy),
|
|
3988
|
+
onClick: loadStorage,
|
|
3989
|
+
}),
|
|
3990
|
+
actionBtn(storageBusy === 'temp' ? '…' : T('storage.cleanTemp'), {
|
|
3991
|
+
key: 'storage-temp',
|
|
3992
|
+
small: true,
|
|
3993
|
+
disabled: Boolean(storageBusy),
|
|
3994
|
+
onClick: function () {
|
|
3995
|
+
cleanStorage('temp');
|
|
3996
|
+
},
|
|
3997
|
+
}),
|
|
3998
|
+
actionBtn(
|
|
3999
|
+
storageConfirm === 'snapshots'
|
|
4000
|
+
? T('storage.confirm')
|
|
4001
|
+
: storageBusy === 'snapshots'
|
|
4002
|
+
? '…'
|
|
4003
|
+
: T('storage.cleanSnapshots'),
|
|
4004
|
+
{
|
|
4005
|
+
key: 'storage-snapshots',
|
|
4006
|
+
small: true,
|
|
4007
|
+
disabled: Boolean(storageBusy),
|
|
4008
|
+
onClick: function () {
|
|
4009
|
+
cleanStorage('snapshots');
|
|
4010
|
+
},
|
|
4011
|
+
}
|
|
4012
|
+
),
|
|
4013
|
+
(storage.recommendations || []).some(function (item) {
|
|
4014
|
+
return item.code === 'orphan-avds';
|
|
4015
|
+
}) || (storage.avds || []).some(function (item) {
|
|
4016
|
+
return item.orphan;
|
|
4017
|
+
})
|
|
4018
|
+
? actionBtn(
|
|
4019
|
+
storageConfirm === 'orphan-avds'
|
|
4020
|
+
? T('storage.confirm')
|
|
4021
|
+
: storageBusy === 'orphan-avds'
|
|
4022
|
+
? '…'
|
|
4023
|
+
: T('storage.cleanOrphans'),
|
|
4024
|
+
{
|
|
4025
|
+
key: 'storage-orphans',
|
|
4026
|
+
small: true,
|
|
4027
|
+
disabled: Boolean(storageBusy),
|
|
4028
|
+
onClick: function () {
|
|
4029
|
+
cleanStorage('orphan-avds');
|
|
4030
|
+
},
|
|
4031
|
+
}
|
|
4032
|
+
)
|
|
4033
|
+
: null
|
|
4034
|
+
),
|
|
4035
|
+
storageError ? h('div', { className: 'apkvm-dim' }, storageError) : null
|
|
4036
|
+
)
|
|
4037
|
+
)
|
|
4038
|
+
);
|
|
4039
|
+
|
|
3817
4040
|
var primaryAction =
|
|
3818
4041
|
ready || busyState
|
|
3819
4042
|
? actionBtn(vmBusy ? T('action.stopping') : T('action.stop'), {
|
|
@@ -4149,6 +4372,7 @@ window.__ModuleLoader__.load({
|
|
|
4149
4372
|
debugCard,
|
|
4150
4373
|
flowCard,
|
|
4151
4374
|
networkCard,
|
|
4375
|
+
storageCard,
|
|
4152
4376
|
appsCard,
|
|
4153
4377
|
diagCard,
|
|
4154
4378
|
h('div', { className: 'apkvm-footnote', title: T('hint.boot') }, T('hint.short')),
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-apk-vm
|
|
2
|
+
* dsh-apk-vm 鈥?Host half.
|
|
3
3
|
*
|
|
4
4
|
* Mounts the APK Virtual Machine bridge behind the Web server:
|
|
5
5
|
* GET /apk-vm/state VM lifecycle snapshot (adb/emulator state, display size)
|
|
@@ -28,6 +28,7 @@ import { detectEnvironment, createAvd, installSystemImage, installSdkPackages, b
|
|
|
28
28
|
import { acceptWebSocket } from './lib/ws.js';
|
|
29
29
|
import { parseUiHierarchy, decorateUiNodes, buildInspectorPayload } from './lib/ui-inspector.js';
|
|
30
30
|
import { FlowRecorder } from './lib/flows.js';
|
|
31
|
+
import { collectStorage, cleanTempFiles, cleanSnapshots, cleanOrphanAvds, formatBytes } from './lib/storage.js';
|
|
31
32
|
|
|
32
33
|
export const name = 'dsh-apk-vm';
|
|
33
34
|
|
|
@@ -298,7 +299,7 @@ export function apply(ctx, config = {}) {
|
|
|
298
299
|
/* ---- H.264 video stream (scrcpy) ---------------------------------- *
|
|
299
300
|
* The PNG screencap path is capped near 1-2 fps by adb. For real frame
|
|
300
301
|
* rates the plugin runs the Apache-2.0 scrcpy server (vendored jar), parses
|
|
301
|
-
* its H.264 stream with lib/scrcpy.js, converts Annex-B
|
|
302
|
+
* its H.264 stream with lib/scrcpy.js, converts Annex-B 鈫?AVCC, and relays
|
|
302
303
|
* the packets over a no-dependency WebSocket (/apk-vm/ws) to the panel,
|
|
303
304
|
* which decodes them with WebCodecs VideoDecoder. Input still goes through
|
|
304
305
|
* the existing /apk-vm/input routes. */
|
|
@@ -868,6 +869,75 @@ export function apply(ctx, config = {}) {
|
|
|
868
869
|
});
|
|
869
870
|
});
|
|
870
871
|
|
|
872
|
+
function storageSdkRoot() {
|
|
873
|
+
if (vm.sdkRoot) return vm.sdkRoot;
|
|
874
|
+
if (vm.adbPath) return path.dirname(path.dirname(vm.adbPath));
|
|
875
|
+
return process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
register('/apk-vm/storage', async (req, res) => {
|
|
879
|
+
const known = () => {
|
|
880
|
+
const names = new Set();
|
|
881
|
+
if (vm.avdName) names.add(vm.avdName);
|
|
882
|
+
if (instanceManager) {
|
|
883
|
+
for (const item of instanceManager.list()) if (item.avdName) names.add(item.avdName);
|
|
884
|
+
}
|
|
885
|
+
return [...names];
|
|
886
|
+
};
|
|
887
|
+
const active = () => {
|
|
888
|
+
const names = new Set();
|
|
889
|
+
if (vm.avdName && ['ready', 'booting', 'starting'].includes(vm._state)) names.add(vm.avdName);
|
|
890
|
+
if (instanceManager) {
|
|
891
|
+
for (const item of instanceManager.list()) {
|
|
892
|
+
if (item.avdName && ['ready', 'booting', 'starting'].includes(item.state)) names.add(item.avdName);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return [...names];
|
|
896
|
+
};
|
|
897
|
+
if (req.method === 'GET') {
|
|
898
|
+
try {
|
|
899
|
+
sendJson(
|
|
900
|
+
res,
|
|
901
|
+
200,
|
|
902
|
+
await collectStorage({
|
|
903
|
+
sdkRoot: storageSdkRoot(),
|
|
904
|
+
avdRoot: undefined,
|
|
905
|
+
knownAvds: known(),
|
|
906
|
+
activeAvds: active(),
|
|
907
|
+
}),
|
|
908
|
+
);
|
|
909
|
+
} catch (error) {
|
|
910
|
+
sendError(res, 500, error.message);
|
|
911
|
+
}
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (req.method !== 'POST') {
|
|
915
|
+
sendError(res, 405, 'GET or POST only');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const body = await readJsonBody(req);
|
|
919
|
+
const action = String(body.action || '').toLowerCase();
|
|
920
|
+
try {
|
|
921
|
+
let result;
|
|
922
|
+
if (action === 'temp') {
|
|
923
|
+
result = await cleanTempFiles();
|
|
924
|
+
} else if (action === 'snapshots') {
|
|
925
|
+
if (body.confirm !== true) throw new Error('snapshot cleanup requires confirm:true');
|
|
926
|
+
const name = String(body.avdName || vm.avdName || 'dsh_apk');
|
|
927
|
+
if (active().includes(name)) throw new Error(`AVD ${name} is active; stop it before deleting snapshots`);
|
|
928
|
+
result = await cleanSnapshots({ avdName: name });
|
|
929
|
+
} else if (action === 'orphan-avds') {
|
|
930
|
+
if (body.confirm !== true) throw new Error('orphan AVD cleanup requires confirm:true');
|
|
931
|
+
result = await cleanOrphanAvds({ knownAvds: known(), activeAvds: active() });
|
|
932
|
+
} else {
|
|
933
|
+
throw new Error(`unsupported storage action: ${action || '(missing)'}`);
|
|
934
|
+
}
|
|
935
|
+
sendJson(res, 200, result);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
sendError(res, 400, error.message);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
871
941
|
register('/apk-vm/state', async (req, res) => {
|
|
872
942
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
873
943
|
sendError(res, 405, 'GET only');
|
|
@@ -2369,7 +2439,7 @@ export function apply(ctx, config = {}) {
|
|
|
2369
2439
|
'The VM is usually already running (warm start); adbd runs as root once ready. ' +
|
|
2370
2440
|
'Actions: info, shell, logcat, getprop, setprop, packages, install, push, pull, reboot, root, ' +
|
|
2371
2441
|
'force_stop, clear_data, uninstall, rotate, ui_dump, inspect_ui, proxy, install_ca, record, perf, screenshot, tap, swipe, long_press, key, text, ' +
|
|
2372
|
-
'apk_info, find_ui, assert_text, tap_text, clipboard_get, clipboard_set, devices, attach_device, detach_device, start, stop, list_instances, create_instance, destroy_instance, flow_start, flow_stop, flow_list, flow_replay. ' +
|
|
2442
|
+
'apk_info, find_ui, assert_text, tap_text, clipboard_get, clipboard_set, devices, attach_device, detach_device, start, stop, list_instances, create_instance, destroy_instance, flow_start, flow_stop, flow_list, flow_replay, storage, clean_storage. ' +
|
|
2373
2443
|
'Multi-instance: pass `instance` to target an isolated VM; omit it to use the calling Agent\'s ' +
|
|
2374
2444
|
'assigned instance (first Agent gets the default VM, later Agents get isolated instances). ' +
|
|
2375
2445
|
'Use shell for low-level Android commands, logcat for logs, ' +
|
|
@@ -2382,7 +2452,7 @@ export function apply(ctx, config = {}) {
|
|
|
2382
2452
|
properties: {
|
|
2383
2453
|
action: {
|
|
2384
2454
|
type: 'string',
|
|
2385
|
-
description: 'info|shell|logcat|getprop|setprop|packages|install|push|pull|reboot|root|force_stop|clear_data|uninstall|rotate|ui_dump|inspect_ui|proxy|install_ca|record|perf|screenshot|tap|swipe|long_press|key|text|apk_info|find_ui|assert_text|tap_text|clipboard_get|clipboard_set|devices|attach_device|detach_device|start|stop|list_instances|create_instance|destroy_instance|flow_start|flow_stop|flow_list|flow_replay',
|
|
2455
|
+
description: 'info|shell|logcat|getprop|setprop|packages|install|push|pull|reboot|root|force_stop|clear_data|uninstall|rotate|ui_dump|inspect_ui|proxy|install_ca|record|perf|screenshot|tap|swipe|long_press|key|text|apk_info|find_ui|assert_text|tap_text|clipboard_get|clipboard_set|devices|attach_device|detach_device|start|stop|list_instances|create_instance|destroy_instance|flow_start|flow_stop|flow_list|flow_replay|storage|clean_storage',
|
|
2386
2456
|
},
|
|
2387
2457
|
instance: { type: 'string', description: 'Target VM instance id (default or an extra instance from list_instances). Omit for the calling Agent\'s assigned instance.' },
|
|
2388
2458
|
instanceName: { type: 'string', description: 'create_instance: optional name for the new isolated instance.' },
|
|
@@ -2418,6 +2488,9 @@ export function apply(ctx, config = {}) {
|
|
|
2418
2488
|
port: { type: 'integer', description: 'proxy: HTTP proxy port (1-65535).' },
|
|
2419
2489
|
bypass: { type: 'string', description: 'proxy: optional no-proxy/exclusion list.' },
|
|
2420
2490
|
flowId: { type: 'string', description: 'flow_replay: saved flow id from flow_list.' },
|
|
2491
|
+
storageAction: { type: 'string', description: 'clean_storage: temp|snapshots|orphan-avds.' },
|
|
2492
|
+
avdName: { type: 'string', description: 'clean_storage snapshots: target AVD (defaults to the active AVD).' },
|
|
2493
|
+
confirm: { type: 'boolean', description: 'clean_storage: required true for snapshot or orphan AVD deletion.' },
|
|
2421
2494
|
},
|
|
2422
2495
|
required: ['action'],
|
|
2423
2496
|
},
|
|
@@ -2544,6 +2617,65 @@ export function apply(ctx, config = {}) {
|
|
|
2544
2617
|
});
|
|
2545
2618
|
return { ok: true, summary: `replayed ${replayed.steps} step(s) from ${replayed.name}`, result: replayed };
|
|
2546
2619
|
}
|
|
2620
|
+
if (action === 'storage') {
|
|
2621
|
+
const report = await collectStorage({
|
|
2622
|
+
sdkRoot: storageSdkRoot(),
|
|
2623
|
+
knownAvds: [
|
|
2624
|
+
vm.avdName,
|
|
2625
|
+
...(instanceManager ? instanceManager.list().map((item) => item.avdName) : []),
|
|
2626
|
+
].filter(Boolean),
|
|
2627
|
+
activeAvds: [
|
|
2628
|
+
...(['ready', 'booting', 'starting'].includes(vm._state) ? [vm.avdName] : []),
|
|
2629
|
+
...(instanceManager
|
|
2630
|
+
? instanceManager
|
|
2631
|
+
.list()
|
|
2632
|
+
.filter((item) => ['ready', 'booting', 'starting'].includes(item.state))
|
|
2633
|
+
.map((item) => item.avdName)
|
|
2634
|
+
: []),
|
|
2635
|
+
].filter(Boolean),
|
|
2636
|
+
});
|
|
2637
|
+
return {
|
|
2638
|
+
ok: true,
|
|
2639
|
+
summary:
|
|
2640
|
+
`AVD ${formatBytes(report.totals.avdBytes)}, snapshots ` +
|
|
2641
|
+
`${formatBytes(report.totals.snapshotBytes)}, SDK ${formatBytes(report.totals.sdkBytes)}, ` +
|
|
2642
|
+
`temp ${formatBytes(report.totals.tempBytes)}, free ${formatBytes(report.totals.freeBytes)}`,
|
|
2643
|
+
result: report,
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
if (action === 'clean_storage') {
|
|
2647
|
+
if (!args.storageAction) throw new Error('action=clean_storage requires storageAction');
|
|
2648
|
+
const storageAction = String(args.storageAction).toLowerCase();
|
|
2649
|
+
const knownAvds = [
|
|
2650
|
+
vm.avdName,
|
|
2651
|
+
...(instanceManager ? instanceManager.list().map((item) => item.avdName) : []),
|
|
2652
|
+
].filter(Boolean);
|
|
2653
|
+
const activeAvds = [
|
|
2654
|
+
...(['ready', 'booting', 'starting'].includes(vm._state) ? [vm.avdName] : []),
|
|
2655
|
+
...(instanceManager
|
|
2656
|
+
? instanceManager
|
|
2657
|
+
.list()
|
|
2658
|
+
.filter((item) => ['ready', 'booting', 'starting'].includes(item.state))
|
|
2659
|
+
.map((item) => item.avdName)
|
|
2660
|
+
: []),
|
|
2661
|
+
].filter(Boolean);
|
|
2662
|
+
let result;
|
|
2663
|
+
if (storageAction === 'temp') result = await cleanTempFiles();
|
|
2664
|
+
else if (storageAction === 'snapshots') {
|
|
2665
|
+
if (args.confirm !== true) throw new Error('clean_storage snapshots requires confirm=true');
|
|
2666
|
+
const target = String(args.avdName || vm.avdName || 'dsh_apk');
|
|
2667
|
+
if (activeAvds.includes(target)) throw new Error(`AVD ${target} is active; stop it first`);
|
|
2668
|
+
result = await cleanSnapshots({ avdName: target });
|
|
2669
|
+
} else if (storageAction === 'orphan-avds') {
|
|
2670
|
+
if (args.confirm !== true) throw new Error('clean_storage orphan-avds requires confirm=true');
|
|
2671
|
+
result = await cleanOrphanAvds({ knownAvds, activeAvds });
|
|
2672
|
+
} else throw new Error(`unsupported storageAction: ${storageAction}`);
|
|
2673
|
+
return {
|
|
2674
|
+
ok: true,
|
|
2675
|
+
summary: `${storageAction}: freed ${formatBytes(result.freedBytes)}`,
|
|
2676
|
+
result,
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2547
2679
|
if (action === 'start') {
|
|
2548
2680
|
const snapshot = await api.start();
|
|
2549
2681
|
return { ok: true, summary: `started ${api.id}`, result: snapshot || {} };
|
|
@@ -2766,3 +2898,4 @@ export function apply(ctx, config = {}) {
|
|
|
2766
2898
|
}, 'dsh-apk-vm: apk_vm tool');
|
|
2767
2899
|
});
|
|
2768
2900
|
}
|
|
2901
|
+
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage accounting and housekeeping for the Android VM.
|
|
3
|
+
*
|
|
4
|
+
* All reported sizes are logical bytes (sparse files are counted by length), so
|
|
5
|
+
* the panel distinguishes apparent AVD size from actual free space. Cleanup is
|
|
6
|
+
* explicit: this module never deletes an AVD or snapshot unless the caller asks
|
|
7
|
+
* for that exact action.
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fsp } from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
const MAX_FILES = 200000;
|
|
14
|
+
|
|
15
|
+
async function walkSize(root, options = {}) {
|
|
16
|
+
const result = { bytes: 0, files: 0, skipped: false };
|
|
17
|
+
const stack = [String(root)];
|
|
18
|
+
while (stack.length) {
|
|
19
|
+
const dir = stack.pop();
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
23
|
+
} catch {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (options.stopAt && result.bytes > options.stopAt) {
|
|
28
|
+
result.skipped = true;
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
if (result.files >= MAX_FILES) {
|
|
32
|
+
result.skipped = true;
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const full = path.join(dir, entry.name);
|
|
36
|
+
if (entry.isSymbolicLink()) continue;
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
stack.push(full);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!entry.isFile()) continue;
|
|
42
|
+
try {
|
|
43
|
+
const stat = await fsp.stat(full);
|
|
44
|
+
result.bytes += stat.size;
|
|
45
|
+
result.files += 1;
|
|
46
|
+
} catch {
|
|
47
|
+
/* vanished */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readAvdIni(avdRoot, name) {
|
|
55
|
+
try {
|
|
56
|
+
const text = await fsp.readFile(path.join(avdRoot, `${name}.ini`), 'utf8');
|
|
57
|
+
const match = /^path=(.+)$/m.exec(text);
|
|
58
|
+
return match ? match[1].trim() : path.join(avdRoot, `${name}.avd`);
|
|
59
|
+
} catch {
|
|
60
|
+
return path.join(avdRoot, `${name}.avd`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatBytes(bytes) {
|
|
65
|
+
const value = Number(bytes) || 0;
|
|
66
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
67
|
+
let index = 0;
|
|
68
|
+
let size = value;
|
|
69
|
+
while (size >= 1024 && index < units.length - 1) {
|
|
70
|
+
size /= 1024;
|
|
71
|
+
index += 1;
|
|
72
|
+
}
|
|
73
|
+
return `${size >= 10 || index === 0 ? Math.round(size) : size.toFixed(1)} ${units[index]}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fileStatOrNull(file) {
|
|
77
|
+
try {
|
|
78
|
+
return await fsp.stat(file);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Collect AVD sizes, SDK sizes, plugin temp files and free disk space. */
|
|
85
|
+
export async function collectStorage(options = {}) {
|
|
86
|
+
const home = options.home || os.homedir();
|
|
87
|
+
const avdRoot = path.resolve(options.avdRoot || path.join(home, '.android', 'avd'));
|
|
88
|
+
const tempRoot = path.resolve(options.tempRoot || os.tmpdir());
|
|
89
|
+
const sdkRoot = options.sdkRoot ? path.resolve(options.sdkRoot) : null;
|
|
90
|
+
const activeAvds = new Set((options.activeAvds || []).map(String));
|
|
91
|
+
const knownAvds = new Set((options.knownAvds || []).map(String));
|
|
92
|
+
|
|
93
|
+
const avds = [];
|
|
94
|
+
let avdEntries = [];
|
|
95
|
+
try {
|
|
96
|
+
avdEntries = await fsp.readdir(avdRoot, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
avdEntries = [];
|
|
99
|
+
}
|
|
100
|
+
for (const entry of avdEntries) {
|
|
101
|
+
if (!entry.isDirectory() || !entry.name.endsWith('.avd')) continue;
|
|
102
|
+
const name = entry.name.slice(0, -4);
|
|
103
|
+
const avdPath = await readAvdIni(avdRoot, name);
|
|
104
|
+
const total = await walkSize(avdPath);
|
|
105
|
+
const snapshotsDir = path.join(avdPath, 'snapshots');
|
|
106
|
+
const snapshots = await walkSize(snapshotsDir);
|
|
107
|
+
avds.push({
|
|
108
|
+
name,
|
|
109
|
+
path: avdPath,
|
|
110
|
+
totalBytes: total.bytes,
|
|
111
|
+
totalFiles: total.files,
|
|
112
|
+
snapshotBytes: snapshots.bytes,
|
|
113
|
+
userdataBytes: Math.max(0, total.bytes - snapshots.bytes),
|
|
114
|
+
active: activeAvds.has(name),
|
|
115
|
+
registered: knownAvds.has(name),
|
|
116
|
+
managed: name === 'dsh_apk' || name.startsWith('dsh_apk_'),
|
|
117
|
+
orphan: name.startsWith('dsh_apk_') && !knownAvds.has(name) && !activeAvds.has(name),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
avds.sort((a, b) => b.totalBytes - a.totalBytes);
|
|
121
|
+
|
|
122
|
+
const temp = {
|
|
123
|
+
uploadsBytes: (await walkSize(path.join(tempRoot, 'dsh-apk-vm-uploads'))).bytes,
|
|
124
|
+
flowsBytes: (await walkSize(path.join(tempRoot, 'dsh-apk-vm-flows'))).bytes,
|
|
125
|
+
screenshotsBytes: 0,
|
|
126
|
+
recordingsBytes: 0,
|
|
127
|
+
files: 0,
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
const entries = await fsp.readdir(tempRoot, { withFileTypes: true });
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (!entry.isFile()) continue;
|
|
133
|
+
if (!/^(apk-vm-shot-|dsh-apk-vm-record-)/.test(entry.name)) continue;
|
|
134
|
+
const stat = await fileStatOrNull(path.join(tempRoot, entry.name));
|
|
135
|
+
if (!stat) continue;
|
|
136
|
+
temp.files += 1;
|
|
137
|
+
if (entry.name.startsWith('apk-vm-shot-')) temp.screenshotsBytes += stat.size;
|
|
138
|
+
else temp.recordingsBytes += stat.size;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
/* ignore */
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let sdk = null;
|
|
145
|
+
if (sdkRoot) {
|
|
146
|
+
const total = await walkSize(sdkRoot);
|
|
147
|
+
const parts = {};
|
|
148
|
+
for (const part of ['system-images', 'emulator', 'build-tools', 'platform-tools', 'platforms']) {
|
|
149
|
+
parts[part] = (await walkSize(path.join(sdkRoot, part))).bytes;
|
|
150
|
+
}
|
|
151
|
+
sdk = { root: sdkRoot, totalBytes: total.bytes, totalFiles: total.files, parts };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let disk = null;
|
|
155
|
+
try {
|
|
156
|
+
const stats = await fsp.statfs(avdRoot || home);
|
|
157
|
+
disk = {
|
|
158
|
+
totalBytes: Number(stats.blocks) * Number(stats.bsize),
|
|
159
|
+
freeBytes: Number(stats.bavail) * Number(stats.bsize),
|
|
160
|
+
availableBytes: Number(stats.bavail) * Number(stats.bsize),
|
|
161
|
+
path: avdRoot || home,
|
|
162
|
+
};
|
|
163
|
+
} catch {
|
|
164
|
+
disk = null;
|
|
165
|
+
}
|
|
166
|
+
let sdkDisk = null;
|
|
167
|
+
if (sdkRoot) {
|
|
168
|
+
try {
|
|
169
|
+
const stats = await fsp.statfs(sdkRoot);
|
|
170
|
+
sdkDisk = {
|
|
171
|
+
totalBytes: Number(stats.blocks) * Number(stats.bsize),
|
|
172
|
+
freeBytes: Number(stats.bavail) * Number(stats.bsize),
|
|
173
|
+
path: sdkRoot,
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
sdkDisk = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const snapshotTotal = avds.reduce((sum, item) => sum + item.snapshotBytes, 0);
|
|
181
|
+
const tempTotal = temp.uploadsBytes + temp.flowsBytes + temp.screenshotsBytes + temp.recordingsBytes;
|
|
182
|
+
const recommendations = [];
|
|
183
|
+
if (snapshotTotal > 1024 * 1024 * 1024) {
|
|
184
|
+
recommendations.push({
|
|
185
|
+
code: 'snapshots',
|
|
186
|
+
bytes: snapshotTotal,
|
|
187
|
+
text: `Quick-boot snapshots use ${formatBytes(snapshotTotal)}; disable quickBoot/saveSnapshotAfterBoot or delete a snapshot to free it.`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (tempTotal > 0) {
|
|
191
|
+
recommendations.push({ code: 'temp', bytes: tempTotal, text: `Plugin temp files use ${formatBytes(tempTotal)}; safe to clean.` });
|
|
192
|
+
}
|
|
193
|
+
const orphans = avds.filter((item) => item.orphan);
|
|
194
|
+
if (orphans.length) {
|
|
195
|
+
recommendations.push({
|
|
196
|
+
code: 'orphan-avds',
|
|
197
|
+
bytes: orphans.reduce((sum, item) => sum + item.totalBytes, 0),
|
|
198
|
+
text: `${orphans.length} orphan dsh_apk_* AVD(s) are not registered to a running instance.`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
sampledAt: new Date().toISOString(),
|
|
205
|
+
home,
|
|
206
|
+
avdRoot,
|
|
207
|
+
tempRoot,
|
|
208
|
+
sdk,
|
|
209
|
+
sdkDisk,
|
|
210
|
+
avds,
|
|
211
|
+
temp,
|
|
212
|
+
disk,
|
|
213
|
+
totals: {
|
|
214
|
+
avdBytes: avds.reduce((sum, item) => sum + item.totalBytes, 0),
|
|
215
|
+
snapshotBytes: snapshotTotal,
|
|
216
|
+
sdkBytes: sdk ? sdk.totalBytes : 0,
|
|
217
|
+
tempBytes: tempTotal,
|
|
218
|
+
freeBytes: disk ? disk.freeBytes : 0,
|
|
219
|
+
},
|
|
220
|
+
recommendations,
|
|
221
|
+
formatter: 'bytes',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function removeTree(target) {
|
|
226
|
+
const existed = await fsp.stat(target).catch(() => null);
|
|
227
|
+
if (!existed) return 0;
|
|
228
|
+
const measured = await walkSize(target);
|
|
229
|
+
await fsp.rm(target, { recursive: true, force: true }).catch(() => {});
|
|
230
|
+
return measured.bytes;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Delete only the plugin's own temporary artifacts. */
|
|
234
|
+
export async function cleanTempFiles(options = {}) {
|
|
235
|
+
const tempRoot = path.resolve(options.tempRoot || os.tmpdir());
|
|
236
|
+
let freed = 0;
|
|
237
|
+
let files = 0;
|
|
238
|
+
for (const name of ['dsh-apk-vm-uploads', 'dsh-apk-vm-flows']) {
|
|
239
|
+
const dir = path.join(tempRoot, name);
|
|
240
|
+
const measured = await walkSize(dir);
|
|
241
|
+
await removeTree(dir);
|
|
242
|
+
freed += measured.bytes;
|
|
243
|
+
files += measured.files;
|
|
244
|
+
}
|
|
245
|
+
const entries = await fsp.readdir(tempRoot, { withFileTypes: true }).catch(() => []);
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
if (!entry.isFile() || !/^(apk-vm-shot-|dsh-apk-vm-record-)/.test(entry.name)) continue;
|
|
248
|
+
const full = path.join(tempRoot, entry.name);
|
|
249
|
+
const stat = await fileStatOrNull(full);
|
|
250
|
+
if (!stat) continue;
|
|
251
|
+
await fsp.unlink(full).catch(() => {});
|
|
252
|
+
freed += stat.size;
|
|
253
|
+
files += 1;
|
|
254
|
+
}
|
|
255
|
+
return { ok: true, action: 'temp', freedBytes: freed, files };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Delete snapshots for one AVD by name. Refuses active or non-managed AVDs unless forced. */
|
|
259
|
+
export async function cleanSnapshots(options = {}) {
|
|
260
|
+
const avdRoot = path.resolve(options.avdRoot || path.join(os.homedir(), '.android', 'avd'));
|
|
261
|
+
const name = String(options.avdName || '').trim();
|
|
262
|
+
if (!/^[A-Za-z0-9._-]{1,80}$/.test(name)) throw new Error('invalid AVD name');
|
|
263
|
+
if (name !== 'dsh_apk' && !name.startsWith('dsh_apk_')) {
|
|
264
|
+
throw new Error('only plugin-managed dsh_apk* AVDs can have snapshots cleaned');
|
|
265
|
+
}
|
|
266
|
+
const avdPath = await readAvdIni(avdRoot, name);
|
|
267
|
+
if (!avdPath.startsWith(avdRoot + path.sep)) throw new Error('AVD path escapes the AVD root');
|
|
268
|
+
const snapshotsDir = path.join(avdPath, 'snapshots');
|
|
269
|
+
const measured = await walkSize(snapshotsDir);
|
|
270
|
+
await removeTree(snapshotsDir);
|
|
271
|
+
return { ok: true, action: 'snapshots', avdName: name, freedBytes: measured.bytes, files: measured.files };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Delete only orphan dsh_apk_* AVDs that are not registered/active. */
|
|
275
|
+
export async function cleanOrphanAvds(options = {}) {
|
|
276
|
+
const avdRoot = path.resolve(options.avdRoot || path.join(os.homedir(), '.android', 'avd'));
|
|
277
|
+
const known = new Set((options.knownAvds || []).map(String));
|
|
278
|
+
const active = new Set((options.activeAvds || []).map(String));
|
|
279
|
+
const entries = await fsp.readdir(avdRoot, { withFileTypes: true }).catch(() => []);
|
|
280
|
+
const removed = [];
|
|
281
|
+
let freed = 0;
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
if (!entry.isDirectory() || !entry.name.endsWith('.avd')) continue;
|
|
284
|
+
const name = entry.name.slice(0, -4);
|
|
285
|
+
if (!name.startsWith('dsh_apk_') || known.has(name) || active.has(name)) continue;
|
|
286
|
+
const avdPath = path.join(avdRoot, entry.name);
|
|
287
|
+
const measured = await walkSize(avdPath);
|
|
288
|
+
await fsp.rm(avdPath, { recursive: true, force: true }).catch(() => {});
|
|
289
|
+
await fsp.unlink(path.join(avdRoot, `${name}.ini`)).catch(() => {});
|
|
290
|
+
freed += measured.bytes;
|
|
291
|
+
removed.push({ name, bytes: measured.bytes });
|
|
292
|
+
}
|
|
293
|
+
return { ok: true, action: 'orphan-avds', freedBytes: freed, removed };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { formatBytes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-apk-vm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local AOSP Android VM for DeepSeek Harness: preview rooted APKs at up to 60 fps, automate UIAutomator flows, and give each Agent an isolated VM.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"registry": "https://registry.npmjs.org/"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"check": "node --check index.js && node --check client.js && node --check lib/android.js && node --check lib/apk-bundle.js && node --check lib/flows.js && node --check lib/ui-inspector.js && node --check lib/x509.js && node --check lib/scrcpy.js && node --check lib/ws.js && node --check lib/setup.js && node --check lib/instances.js && node --check lib/video-stream.js",
|
|
28
|
+
"check": "node --check index.js && node --check client.js && node --check lib/android.js && node --check lib/apk-bundle.js && node --check lib/flows.js && node --check lib/storage.js && node --check lib/ui-inspector.js && node --check lib/x509.js && node --check lib/scrcpy.js && node --check lib/ws.js && node --check lib/setup.js && node --check lib/instances.js && node --check lib/video-stream.js",
|
|
29
29
|
"privacy": "node scripts/privacy-scan.mjs",
|
|
30
30
|
"test": "node spike/ui-smoke.cjs && node spike/ui-better-sidebar-smoke.cjs && node spike/setup-smoke.mjs && node spike/android/mock-device.mjs && node spike/android/launch-plan.mjs && node spike/instances-smoke.mjs && node spike/features.mjs"
|
|
31
31
|
},
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"lib/apk-bundle.js",
|
|
45
45
|
"lib/flows.js",
|
|
46
46
|
"lib/scrcpy.js",
|
|
47
|
+
"lib/storage.js",
|
|
47
48
|
"lib/ui-inspector.js",
|
|
48
49
|
"lib/ws.js",
|
|
49
50
|
"lib/setup.js",
|