dsh-apk-vm 0.4.1 → 0.6.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 +44 -0
- package/README.md +56 -6
- package/client.js +547 -1
- package/index.js +318 -5
- package/lib/android.js +277 -2
- package/lib/instances.js +6 -0
- package/lib/storage.js +300 -0
- package/package.json +3 -2
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
|
|
|
@@ -244,6 +245,9 @@ export function apply(ctx, config = {}) {
|
|
|
244
245
|
quickBoot: cfg.quickBoot !== false,
|
|
245
246
|
snapshotName: cfg.snapshotName || undefined,
|
|
246
247
|
saveSnapshotAfterBoot: cfg.saveSnapshotAfterBoot !== false,
|
|
248
|
+
dnsServers: Array.isArray(cfg.dnsServers) ? cfg.dnsServers.map(String) : undefined,
|
|
249
|
+
networkMode: cfg.networkMode || undefined,
|
|
250
|
+
tcpdumpPath: cfg.tcpdumpPath || undefined,
|
|
247
251
|
});
|
|
248
252
|
|
|
249
253
|
const instanceManager =
|
|
@@ -263,6 +267,9 @@ export function apply(ctx, config = {}) {
|
|
|
263
267
|
scrcpyBitrate: cfg.scrcpyBitrate || 6_000_000,
|
|
264
268
|
scrcpyMaxFps: cfg.scrcpyMaxFps || 60,
|
|
265
269
|
idleTimeoutMs: cfg.instanceIdleMs || 15 * 60 * 1000,
|
|
270
|
+
dnsServers: Array.isArray(cfg.dnsServers) ? cfg.dnsServers.map(String) : undefined,
|
|
271
|
+
networkMode: cfg.networkMode || undefined,
|
|
272
|
+
tcpdumpPath: cfg.tcpdumpPath || undefined,
|
|
266
273
|
log: (line) => vm._pushLog(line),
|
|
267
274
|
});
|
|
268
275
|
|
|
@@ -298,7 +305,7 @@ export function apply(ctx, config = {}) {
|
|
|
298
305
|
/* ---- H.264 video stream (scrcpy) ---------------------------------- *
|
|
299
306
|
* The PNG screencap path is capped near 1-2 fps by adb. For real frame
|
|
300
307
|
* 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
|
|
308
|
+
* its H.264 stream with lib/scrcpy.js, converts Annex-B 鈫?AVCC, and relays
|
|
302
309
|
* the packets over a no-dependency WebSocket (/apk-vm/ws) to the panel,
|
|
303
310
|
* which decodes them with WebCodecs VideoDecoder. Input still goes through
|
|
304
311
|
* the existing /apk-vm/input routes. */
|
|
@@ -868,6 +875,75 @@ export function apply(ctx, config = {}) {
|
|
|
868
875
|
});
|
|
869
876
|
});
|
|
870
877
|
|
|
878
|
+
function storageSdkRoot() {
|
|
879
|
+
if (vm.sdkRoot) return vm.sdkRoot;
|
|
880
|
+
if (vm.adbPath) return path.dirname(path.dirname(vm.adbPath));
|
|
881
|
+
return process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
register('/apk-vm/storage', async (req, res) => {
|
|
885
|
+
const known = () => {
|
|
886
|
+
const names = new Set();
|
|
887
|
+
if (vm.avdName) names.add(vm.avdName);
|
|
888
|
+
if (instanceManager) {
|
|
889
|
+
for (const item of instanceManager.list()) if (item.avdName) names.add(item.avdName);
|
|
890
|
+
}
|
|
891
|
+
return [...names];
|
|
892
|
+
};
|
|
893
|
+
const active = () => {
|
|
894
|
+
const names = new Set();
|
|
895
|
+
if (vm.avdName && ['ready', 'booting', 'starting'].includes(vm._state)) names.add(vm.avdName);
|
|
896
|
+
if (instanceManager) {
|
|
897
|
+
for (const item of instanceManager.list()) {
|
|
898
|
+
if (item.avdName && ['ready', 'booting', 'starting'].includes(item.state)) names.add(item.avdName);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return [...names];
|
|
902
|
+
};
|
|
903
|
+
if (req.method === 'GET') {
|
|
904
|
+
try {
|
|
905
|
+
sendJson(
|
|
906
|
+
res,
|
|
907
|
+
200,
|
|
908
|
+
await collectStorage({
|
|
909
|
+
sdkRoot: storageSdkRoot(),
|
|
910
|
+
avdRoot: undefined,
|
|
911
|
+
knownAvds: known(),
|
|
912
|
+
activeAvds: active(),
|
|
913
|
+
}),
|
|
914
|
+
);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
sendError(res, 500, error.message);
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (req.method !== 'POST') {
|
|
921
|
+
sendError(res, 405, 'GET or POST only');
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const body = await readJsonBody(req);
|
|
925
|
+
const action = String(body.action || '').toLowerCase();
|
|
926
|
+
try {
|
|
927
|
+
let result;
|
|
928
|
+
if (action === 'temp') {
|
|
929
|
+
result = await cleanTempFiles();
|
|
930
|
+
} else if (action === 'snapshots') {
|
|
931
|
+
if (body.confirm !== true) throw new Error('snapshot cleanup requires confirm:true');
|
|
932
|
+
const name = String(body.avdName || vm.avdName || 'dsh_apk');
|
|
933
|
+
if (active().includes(name)) throw new Error(`AVD ${name} is active; stop it before deleting snapshots`);
|
|
934
|
+
result = await cleanSnapshots({ avdName: name });
|
|
935
|
+
} else if (action === 'orphan-avds') {
|
|
936
|
+
if (body.confirm !== true) throw new Error('orphan AVD cleanup requires confirm:true');
|
|
937
|
+
result = await cleanOrphanAvds({ knownAvds: known(), activeAvds: active() });
|
|
938
|
+
} else {
|
|
939
|
+
throw new Error(`unsupported storage action: ${action || '(missing)'}`);
|
|
940
|
+
}
|
|
941
|
+
sendJson(res, 200, result);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
sendError(res, 400, error.message);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
871
947
|
register('/apk-vm/state', async (req, res) => {
|
|
872
948
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
873
949
|
sendError(res, 405, 'GET only');
|
|
@@ -1148,6 +1224,55 @@ export function apply(ctx, config = {}) {
|
|
|
1148
1224
|
});
|
|
1149
1225
|
return;
|
|
1150
1226
|
}
|
|
1227
|
+
if (rest === 'network') {
|
|
1228
|
+
if (!(await ready())) return;
|
|
1229
|
+
if (req.method === 'GET') {
|
|
1230
|
+
sendJson(res, 200, await instance.vm.networkInfo());
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (req.method !== 'POST') {
|
|
1234
|
+
sendError(res, 405, 'GET or POST only');
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const body = await readJsonBody(req);
|
|
1238
|
+
const action = String(body.action || '').toLowerCase();
|
|
1239
|
+
try {
|
|
1240
|
+
let result;
|
|
1241
|
+
if (action === 'mode') result = instance.vm.setNetworkMode(body.mode);
|
|
1242
|
+
else if (action === 'dns') result = await instance.vm.setDnsServers(body.servers);
|
|
1243
|
+
else if (action === 'hosts') result = await instance.vm.setHosts(body.entries);
|
|
1244
|
+
else if (action === 'test') result = await instance.vm.testNetwork(body);
|
|
1245
|
+
else if (action === 'pcap') result = instance.vm.setTcpdumpPath(body.path);
|
|
1246
|
+
else if (action === 'forward-add') result = await instance.vm.addPortForward(body.localPort, body.remotePort, { shared: body.shared === true });
|
|
1247
|
+
else if (action === 'forward-remove') result = await instance.vm.removePortForward(body.localPort);
|
|
1248
|
+
else throw new Error(`unsupported network action: ${action || '(missing)'}`);
|
|
1249
|
+
sendJson(res, 200, result);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
sendError(res, 400, error.message);
|
|
1252
|
+
}
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (rest === 'network/pcap') {
|
|
1256
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
1257
|
+
sendError(res, 405, 'GET or HEAD only');
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const tcpdumpPath = instance.vm.tcpdumpPath;
|
|
1261
|
+
if (!tcpdumpPath || !existsSync(tcpdumpPath)) {
|
|
1262
|
+
sendError(res, 404, 'pcap capture is not configured or has no file yet');
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const stat = await fsp.stat(tcpdumpPath);
|
|
1266
|
+
res.writeHead(200, {
|
|
1267
|
+
'Content-Type': 'application/vnd.tcpdump.pcap',
|
|
1268
|
+
'Content-Length': stat.size,
|
|
1269
|
+
'Cache-Control': 'no-store',
|
|
1270
|
+
'Content-Disposition': `attachment; filename="${path.basename(tcpdumpPath)}"`,
|
|
1271
|
+
});
|
|
1272
|
+
if (req.method === 'HEAD') res.end();
|
|
1273
|
+
else createReadStream(tcpdumpPath).pipe(res);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1151
1276
|
if (rest === 'touch' && req.method === 'POST') {
|
|
1152
1277
|
const body = await readJsonBody(req);
|
|
1153
1278
|
const action = String(body.action || '').toLowerCase();
|
|
@@ -1382,6 +1507,8 @@ export function apply(ctx, config = {}) {
|
|
|
1382
1507
|
'debug/pull',
|
|
1383
1508
|
'debug/reboot',
|
|
1384
1509
|
'flows',
|
|
1510
|
+
'network',
|
|
1511
|
+
'network/pcap',
|
|
1385
1512
|
];
|
|
1386
1513
|
const disposers = [];
|
|
1387
1514
|
for (const suffix of suffixes) {
|
|
@@ -2016,6 +2143,59 @@ export function apply(ctx, config = {}) {
|
|
|
2016
2143
|
}
|
|
2017
2144
|
});
|
|
2018
2145
|
|
|
2146
|
+
register('/apk-vm/network', async (req, res) => {
|
|
2147
|
+
if (req.method === 'GET') {
|
|
2148
|
+
if (!(await ensureDebugReady(res))) return;
|
|
2149
|
+
try {
|
|
2150
|
+
sendJson(res, 200, await vm.networkInfo());
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
sendError(res, 400, error.message);
|
|
2153
|
+
}
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (req.method !== 'POST') {
|
|
2157
|
+
sendError(res, 405, 'GET or POST only');
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
if (!(await ensureDebugReady(res))) return;
|
|
2161
|
+
const body = await readJsonBody(req);
|
|
2162
|
+
const action = String(body.action || '').toLowerCase();
|
|
2163
|
+
try {
|
|
2164
|
+
let result;
|
|
2165
|
+
if (action === 'mode') result = vm.setNetworkMode(body.mode);
|
|
2166
|
+
else if (action === 'dns') result = await vm.setDnsServers(body.servers);
|
|
2167
|
+
else if (action === 'hosts') result = await vm.setHosts(body.entries);
|
|
2168
|
+
else if (action === 'test') result = await vm.testNetwork(body);
|
|
2169
|
+
else if (action === 'pcap') result = vm.setTcpdumpPath(body.path);
|
|
2170
|
+
else if (action === 'forward-add') result = await vm.addPortForward(body.localPort, body.remotePort, { shared: body.shared === true });
|
|
2171
|
+
else if (action === 'forward-remove') result = await vm.removePortForward(body.localPort);
|
|
2172
|
+
else throw new Error(`unsupported network action: ${action || '(missing)'}`);
|
|
2173
|
+
sendJson(res, 200, result);
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
sendError(res, 400, error.message);
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
register('/apk-vm/network/pcap', async (req, res) => {
|
|
2180
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
2181
|
+
sendError(res, 405, 'GET or HEAD only');
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (!vm.tcpdumpPath || !existsSync(vm.tcpdumpPath)) {
|
|
2185
|
+
sendError(res, 404, 'pcap capture is not configured or has no file yet');
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
const stat = await fsp.stat(vm.tcpdumpPath);
|
|
2189
|
+
res.writeHead(200, {
|
|
2190
|
+
'Content-Type': 'application/vnd.tcpdump.pcap',
|
|
2191
|
+
'Content-Length': stat.size,
|
|
2192
|
+
'Cache-Control': 'no-store',
|
|
2193
|
+
'Content-Disposition': `attachment; filename="${path.basename(vm.tcpdumpPath)}"`,
|
|
2194
|
+
});
|
|
2195
|
+
if (req.method === 'HEAD') res.end();
|
|
2196
|
+
else createReadStream(vm.tcpdumpPath).pipe(res);
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2019
2199
|
register('/apk-vm/debug/record', async (req, res) => {
|
|
2020
2200
|
if (req.method !== 'POST') {
|
|
2021
2201
|
sendError(res, 405, 'POST only');
|
|
@@ -2223,6 +2403,14 @@ export function apply(ctx, config = {}) {
|
|
|
2223
2403
|
getProxy: () => vm.getProxy(),
|
|
2224
2404
|
setProxy: (options) => vm.setProxy(options),
|
|
2225
2405
|
installCaCertificate: (certPath) => vm.installCaCertificate(certPath),
|
|
2406
|
+
networkInfo: () => vm.networkInfo(),
|
|
2407
|
+
setNetworkMode: (mode) => vm.setNetworkMode(mode),
|
|
2408
|
+
setTcpdumpPath: (filePath) => vm.setTcpdumpPath(filePath),
|
|
2409
|
+
setDnsServers: (servers) => vm.setDnsServers(servers),
|
|
2410
|
+
setHosts: (entries) => vm.setHosts(entries),
|
|
2411
|
+
testNetwork: (options) => vm.testNetwork(options),
|
|
2412
|
+
addPortForward: (local, remote, options) => vm.addPortForward(local, remote, options),
|
|
2413
|
+
removePortForward: (local) => vm.removePortForward(local),
|
|
2226
2414
|
screenRecord: (options) => vm.screenRecord(options),
|
|
2227
2415
|
perfSnapshot: () => vm.perfSnapshot(),
|
|
2228
2416
|
screenshot: async () => {
|
|
@@ -2267,6 +2455,14 @@ export function apply(ctx, config = {}) {
|
|
|
2267
2455
|
getProxy: () => instance.vm.getProxy(),
|
|
2268
2456
|
setProxy: (options) => instance.vm.setProxy(options),
|
|
2269
2457
|
installCaCertificate: (certPath) => instance.vm.installCaCertificate(certPath),
|
|
2458
|
+
networkInfo: () => instance.vm.networkInfo(),
|
|
2459
|
+
setNetworkMode: (mode) => instance.vm.setNetworkMode(mode),
|
|
2460
|
+
setTcpdumpPath: (filePath) => instance.vm.setTcpdumpPath(filePath),
|
|
2461
|
+
setDnsServers: (servers) => instance.vm.setDnsServers(servers),
|
|
2462
|
+
setHosts: (entries) => instance.vm.setHosts(entries),
|
|
2463
|
+
testNetwork: (options) => instance.vm.testNetwork(options),
|
|
2464
|
+
addPortForward: (local, remote, options) => instance.vm.addPortForward(local, remote, options),
|
|
2465
|
+
removePortForward: (local) => instance.vm.removePortForward(local),
|
|
2270
2466
|
screenRecord: (options) => instance.screenRecord(options),
|
|
2271
2467
|
perfSnapshot: () => instance.perfSnapshot(),
|
|
2272
2468
|
screenshot: async () => {
|
|
@@ -2368,8 +2564,8 @@ export function apply(ctx, config = {}) {
|
|
|
2368
2564
|
'Operate the local rooted Android APK virtual machine (Android emulator) that the APK VM panel manages. ' +
|
|
2369
2565
|
'The VM is usually already running (warm start); adbd runs as root once ready. ' +
|
|
2370
2566
|
'Actions: info, shell, logcat, getprop, setprop, packages, install, push, pull, reboot, root, ' +
|
|
2371
|
-
'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. ' +
|
|
2567
|
+
'force_stop, clear_data, uninstall, rotate, ui_dump, inspect_ui, proxy, install_ca, network, record, perf, screenshot, tap, swipe, long_press, key, text, ' +
|
|
2568
|
+
'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
2569
|
'Multi-instance: pass `instance` to target an isolated VM; omit it to use the calling Agent\'s ' +
|
|
2374
2570
|
'assigned instance (first Agent gets the default VM, later Agents get isolated instances). ' +
|
|
2375
2571
|
'Use shell for low-level Android commands, logcat for logs, ' +
|
|
@@ -2382,7 +2578,7 @@ export function apply(ctx, config = {}) {
|
|
|
2382
2578
|
properties: {
|
|
2383
2579
|
action: {
|
|
2384
2580
|
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',
|
|
2581
|
+
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|network|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
2582
|
},
|
|
2387
2583
|
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
2584
|
instanceName: { type: 'string', description: 'create_instance: optional name for the new isolated instance.' },
|
|
@@ -2418,6 +2614,19 @@ export function apply(ctx, config = {}) {
|
|
|
2418
2614
|
port: { type: 'integer', description: 'proxy: HTTP proxy port (1-65535).' },
|
|
2419
2615
|
bypass: { type: 'string', description: 'proxy: optional no-proxy/exclusion list.' },
|
|
2420
2616
|
flowId: { type: 'string', description: 'flow_replay: saved flow id from flow_list.' },
|
|
2617
|
+
storageAction: { type: 'string', description: 'clean_storage: temp|snapshots|orphan-avds.' },
|
|
2618
|
+
avdName: { type: 'string', description: 'clean_storage snapshots: target AVD (defaults to the active AVD).' },
|
|
2619
|
+
confirm: { type: 'boolean', description: 'clean_storage: required true for snapshot or orphan AVD deletion.' },
|
|
2620
|
+
networkAction: { type: 'string', description: 'network: info|mode|dns|hosts|test|pcap|forward-add|forward-remove.' },
|
|
2621
|
+
networkMode: { type: 'string', description: 'network mode: isolated|shared.' },
|
|
2622
|
+
localPort: { type: 'integer', description: 'port forward: host loopback port.' },
|
|
2623
|
+
remotePort: { type: 'integer', description: 'port forward: guest service port.' },
|
|
2624
|
+
shared: { type: 'boolean', description: 'port forward: expose to other VMs through the emulator host alias (requires shared mode).' },
|
|
2625
|
+
servers: { type: 'array', items: { type: 'string' }, description: 'network DNS: up to 4 IPv4/IPv6 DNS servers.' },
|
|
2626
|
+
entries: { type: 'array', items: { type: 'string' }, description: 'network hosts: lines such as "1.2.3.4 api.example.com".' },
|
|
2627
|
+
testHost: { type: 'string', description: 'network test: host to ping/TCP-test (default: emulator host alias).' },
|
|
2628
|
+
testPort: { type: 'integer', description: 'network test: TCP port.' },
|
|
2629
|
+
pcapPath: { type: 'string', description: 'network pcap: host path ending in .pcap; capture starts on the next emulator start.' },
|
|
2421
2630
|
},
|
|
2422
2631
|
required: ['action'],
|
|
2423
2632
|
},
|
|
@@ -2544,6 +2753,109 @@ export function apply(ctx, config = {}) {
|
|
|
2544
2753
|
});
|
|
2545
2754
|
return { ok: true, summary: `replayed ${replayed.steps} step(s) from ${replayed.name}`, result: replayed };
|
|
2546
2755
|
}
|
|
2756
|
+
if (action === 'storage') {
|
|
2757
|
+
const report = await collectStorage({
|
|
2758
|
+
sdkRoot: storageSdkRoot(),
|
|
2759
|
+
knownAvds: [
|
|
2760
|
+
vm.avdName,
|
|
2761
|
+
...(instanceManager ? instanceManager.list().map((item) => item.avdName) : []),
|
|
2762
|
+
].filter(Boolean),
|
|
2763
|
+
activeAvds: [
|
|
2764
|
+
...(['ready', 'booting', 'starting'].includes(vm._state) ? [vm.avdName] : []),
|
|
2765
|
+
...(instanceManager
|
|
2766
|
+
? instanceManager
|
|
2767
|
+
.list()
|
|
2768
|
+
.filter((item) => ['ready', 'booting', 'starting'].includes(item.state))
|
|
2769
|
+
.map((item) => item.avdName)
|
|
2770
|
+
: []),
|
|
2771
|
+
].filter(Boolean),
|
|
2772
|
+
});
|
|
2773
|
+
return {
|
|
2774
|
+
ok: true,
|
|
2775
|
+
summary:
|
|
2776
|
+
`AVD ${formatBytes(report.totals.avdBytes)}, snapshots ` +
|
|
2777
|
+
`${formatBytes(report.totals.snapshotBytes)}, SDK ${formatBytes(report.totals.sdkBytes)}, ` +
|
|
2778
|
+
`temp ${formatBytes(report.totals.tempBytes)}, free ${formatBytes(report.totals.freeBytes)}`,
|
|
2779
|
+
result: report,
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
if (action === 'clean_storage') {
|
|
2783
|
+
if (!args.storageAction) throw new Error('action=clean_storage requires storageAction');
|
|
2784
|
+
const storageAction = String(args.storageAction).toLowerCase();
|
|
2785
|
+
const knownAvds = [
|
|
2786
|
+
vm.avdName,
|
|
2787
|
+
...(instanceManager ? instanceManager.list().map((item) => item.avdName) : []),
|
|
2788
|
+
].filter(Boolean);
|
|
2789
|
+
const activeAvds = [
|
|
2790
|
+
...(['ready', 'booting', 'starting'].includes(vm._state) ? [vm.avdName] : []),
|
|
2791
|
+
...(instanceManager
|
|
2792
|
+
? instanceManager
|
|
2793
|
+
.list()
|
|
2794
|
+
.filter((item) => ['ready', 'booting', 'starting'].includes(item.state))
|
|
2795
|
+
.map((item) => item.avdName)
|
|
2796
|
+
: []),
|
|
2797
|
+
].filter(Boolean);
|
|
2798
|
+
let result;
|
|
2799
|
+
if (storageAction === 'temp') result = await cleanTempFiles();
|
|
2800
|
+
else if (storageAction === 'snapshots') {
|
|
2801
|
+
if (args.confirm !== true) throw new Error('clean_storage snapshots requires confirm=true');
|
|
2802
|
+
const target = String(args.avdName || vm.avdName || 'dsh_apk');
|
|
2803
|
+
if (activeAvds.includes(target)) throw new Error(`AVD ${target} is active; stop it first`);
|
|
2804
|
+
result = await cleanSnapshots({ avdName: target });
|
|
2805
|
+
} else if (storageAction === 'orphan-avds') {
|
|
2806
|
+
if (args.confirm !== true) throw new Error('clean_storage orphan-avds requires confirm=true');
|
|
2807
|
+
result = await cleanOrphanAvds({ knownAvds, activeAvds });
|
|
2808
|
+
} else throw new Error(`unsupported storageAction: ${storageAction}`);
|
|
2809
|
+
return {
|
|
2810
|
+
ok: true,
|
|
2811
|
+
summary: `${storageAction}: freed ${formatBytes(result.freedBytes)}`,
|
|
2812
|
+
result,
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
if (action === 'network') {
|
|
2816
|
+
const networkAction = String(args.networkAction || 'info').toLowerCase();
|
|
2817
|
+
if (networkAction === 'mode') {
|
|
2818
|
+
const result = api.setNetworkMode(args.networkMode || args.mode);
|
|
2819
|
+
return { ok: true, summary: `network mode: ${result.mode}`, result };
|
|
2820
|
+
}
|
|
2821
|
+
if (networkAction === 'dns') {
|
|
2822
|
+
const servers = Array.isArray(args.servers) ? args.servers : String(args.servers || '').split(',');
|
|
2823
|
+
const result = await api.setDnsServers(servers);
|
|
2824
|
+
return { ok: true, summary: `DNS: ${(result.servers || []).join(', ') || '(default)'}`, result };
|
|
2825
|
+
}
|
|
2826
|
+
if (networkAction === 'pcap') {
|
|
2827
|
+
const result = api.setTcpdumpPath(args.pcapPath);
|
|
2828
|
+
return { ok: true, summary: result.enabled ? `pcap -> ${result.path}` : 'pcap disabled', result };
|
|
2829
|
+
}
|
|
2830
|
+
await ensureToolApiReady(api, action);
|
|
2831
|
+
if (networkAction === 'info') {
|
|
2832
|
+
const result = await api.networkInfo();
|
|
2833
|
+
return {
|
|
2834
|
+
ok: true,
|
|
2835
|
+
summary: `mode=${result.mode} proxy=${result.proxy || 'off'} forwards=${result.forwards.length} dns=${(result.dnsServers || []).join(',') || 'default'}`,
|
|
2836
|
+
result,
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
if (networkAction === 'hosts') {
|
|
2840
|
+
const result = await api.setHosts(Array.isArray(args.entries) ? args.entries : []);
|
|
2841
|
+
return { ok: true, summary: `hosts applied: ${result.entries}`, result };
|
|
2842
|
+
}
|
|
2843
|
+
if (networkAction === 'test') {
|
|
2844
|
+
const result = await api.testNetwork({ host: args.testHost, port: args.testPort });
|
|
2845
|
+
return { ok: result.ok, summary: result.checks.map((item) => `${item.name}:${item.ok ? 'ok' : 'fail'}`).join(', '), result };
|
|
2846
|
+
}
|
|
2847
|
+
if (networkAction === 'forward-add') {
|
|
2848
|
+
if (!args.localPort || !args.remotePort) throw new Error('forward-add requires localPort and remotePort');
|
|
2849
|
+
const result = await api.addPortForward(args.localPort, args.remotePort, { shared: args.shared === true });
|
|
2850
|
+
return { ok: true, summary: `forward tcp:${result.localPort} -> ${result.remotePort}`, result };
|
|
2851
|
+
}
|
|
2852
|
+
if (networkAction === 'forward-remove') {
|
|
2853
|
+
if (!args.localPort) throw new Error('forward-remove requires localPort');
|
|
2854
|
+
const result = await api.removePortForward(args.localPort);
|
|
2855
|
+
return { ok: true, summary: `removed forward tcp:${result.localPort}`, result };
|
|
2856
|
+
}
|
|
2857
|
+
throw new Error(`unsupported networkAction: ${networkAction}`);
|
|
2858
|
+
}
|
|
2547
2859
|
if (action === 'start') {
|
|
2548
2860
|
const snapshot = await api.start();
|
|
2549
2861
|
return { ok: true, summary: `started ${api.id}`, result: snapshot || {} };
|
|
@@ -2766,3 +3078,4 @@ export function apply(ctx, config = {}) {
|
|
|
2766
3078
|
}, 'dsh-apk-vm: apk_vm tool');
|
|
2767
3079
|
});
|
|
2768
3080
|
}
|
|
3081
|
+
|