@houwert/conductor 0.13.0 → 0.14.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/dist/android/sdk.js +149 -0
- package/dist/commands/delete-device.js +17 -5
- package/dist/commands/device-pool.js +2 -1
- package/dist/commands/download-app.js +3 -2
- package/dist/commands/foreground-app.js +1 -0
- package/dist/commands/hprof.js +422 -0
- package/dist/commands/install-app.js +2 -9
- package/dist/commands/list-apps.js +2 -1
- package/dist/commands/list-devices.js +27 -3
- package/dist/commands/memory.js +716 -63
- package/dist/commands/run-parallel.js +2 -1
- package/dist/commands/start-device.js +12 -14
- package/dist/commands/stop-device.js +2 -1
- package/dist/daemon/server.js +2 -0
- package/dist/daemon/web-server.js +38 -0
- package/dist/drivers/android.js +6 -3
- package/dist/drivers/bootstrap.js +48 -9
- package/dist/drivers/log-sources/android.js +6 -6
- package/dist/drivers/log-sources/metro-discovery.js +8 -2
- package/dist/drivers/web.js +16 -2
- package/dist/index.js +26 -1
- package/dist/runner.js +9 -3
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ exports.HELP = ` run-parallel --flows-dir <path> Run flows in parallel acro
|
|
|
21
21
|
const child_process_1 = require("child_process");
|
|
22
22
|
const path_1 = __importDefault(require("path"));
|
|
23
23
|
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
24
25
|
const output_js_1 = require("../output.js");
|
|
25
26
|
async function runParallel(flowsDir, opts = {}) {
|
|
26
27
|
if (!flowsDir) {
|
|
@@ -107,7 +108,7 @@ async function runShard(deviceId, flowFile) {
|
|
|
107
108
|
async function discoverAllDevices() {
|
|
108
109
|
const devices = [];
|
|
109
110
|
try {
|
|
110
|
-
const out = await spawnCapture('adb', ['devices']);
|
|
111
|
+
const out = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices']);
|
|
111
112
|
for (const line of out.split('\n').slice(1)) {
|
|
112
113
|
const id = line.trim().split(/\s+/)[0];
|
|
113
114
|
if (id && !line.includes('offline') && id !== '')
|
|
@@ -15,6 +15,7 @@ exports.HELP = ` start-device
|
|
|
15
15
|
const fs_1 = __importDefault(require("fs"));
|
|
16
16
|
const child_process_1 = require("child_process");
|
|
17
17
|
const runner_js_1 = require("../runner.js");
|
|
18
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
18
19
|
const client_js_1 = require("../daemon/client.js");
|
|
19
20
|
const protocol_js_1 = require("../daemon/protocol.js");
|
|
20
21
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
@@ -390,7 +391,9 @@ async function startTvOS(osVersion, opts, name, deviceType) {
|
|
|
390
391
|
}
|
|
391
392
|
// ── Android ───────────────────────────────────────────────────────────────────
|
|
392
393
|
async function listAVDs() {
|
|
393
|
-
const result = await (0, runner_js_1.spawnCommand)('emulator', ['-list-avds']
|
|
394
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-list-avds'], {
|
|
395
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
396
|
+
});
|
|
394
397
|
if (!result.success)
|
|
395
398
|
throw new Error(`emulator -list-avds failed: ${result.stderr}`);
|
|
396
399
|
return result.stdout
|
|
@@ -402,7 +405,9 @@ async function waitForAndroidBoot(avdName) {
|
|
|
402
405
|
const deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;
|
|
403
406
|
const connectedBefore = new Set();
|
|
404
407
|
// Snapshot currently connected devices so we can identify the new one
|
|
405
|
-
const before = await (0, runner_js_1.spawnCommand)('adb', ['devices']
|
|
408
|
+
const before = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices'], {
|
|
409
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
410
|
+
});
|
|
406
411
|
for (const line of before.stdout.split('\n').slice(1)) {
|
|
407
412
|
const id = line.trim().split(/\s+/)[0];
|
|
408
413
|
if (id)
|
|
@@ -410,7 +415,9 @@ async function waitForAndroidBoot(avdName) {
|
|
|
410
415
|
}
|
|
411
416
|
while (Date.now() < deadline) {
|
|
412
417
|
await (0, utils_js_1.sleep)(POLL_MS);
|
|
413
|
-
const result = await (0, runner_js_1.spawnCommand)('adb', ['devices']
|
|
418
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices'], {
|
|
419
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
420
|
+
});
|
|
414
421
|
if (!result.success)
|
|
415
422
|
continue;
|
|
416
423
|
for (const line of result.stdout.split('\n').slice(1)) {
|
|
@@ -419,13 +426,7 @@ async function waitForAndroidBoot(avdName) {
|
|
|
419
426
|
const status = parts[1];
|
|
420
427
|
if (id && status === 'device' && !connectedBefore.has(id)) {
|
|
421
428
|
// Check boot completed
|
|
422
|
-
const boot = await (0, runner_js_1.spawnCommand)('adb', [
|
|
423
|
-
'-s',
|
|
424
|
-
id,
|
|
425
|
-
'shell',
|
|
426
|
-
'getprop',
|
|
427
|
-
'sys.boot_completed',
|
|
428
|
-
]);
|
|
429
|
+
const boot = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', id, 'shell', 'getprop', 'sys.boot_completed'], { env: (0, sdk_js_1.androidSpawnEnv)() });
|
|
429
430
|
if (boot.stdout.trim() === '1')
|
|
430
431
|
return id;
|
|
431
432
|
}
|
|
@@ -452,10 +453,7 @@ async function startAndroid(avdName, opts) {
|
|
|
452
453
|
return 1;
|
|
453
454
|
}
|
|
454
455
|
console.log(`Launching emulator: ${target}...`);
|
|
455
|
-
const proc = (0, child_process_1.spawn)('emulator', ['-avd', target, '-netdelay', 'none', '-netspeed', 'full'], {
|
|
456
|
-
detached: true,
|
|
457
|
-
stdio: 'ignore',
|
|
458
|
-
});
|
|
456
|
+
const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-avd', target, '-netdelay', 'none', '-netspeed', 'full'], { detached: true, stdio: 'ignore', env: (0, sdk_js_1.androidSpawnEnv)() });
|
|
459
457
|
proc.unref();
|
|
460
458
|
let deviceId;
|
|
461
459
|
try {
|
|
@@ -6,6 +6,7 @@ exports.HELP = ` stop-device [<name-or-id>]
|
|
|
6
6
|
--platform <ios|tvos|android|web> Scope to a single platform
|
|
7
7
|
--all Stop all booted simulators / running emulators / web sessions`;
|
|
8
8
|
const runner_js_1 = require("../runner.js");
|
|
9
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
9
10
|
const output_js_1 = require("../output.js");
|
|
10
11
|
const client_js_1 = require("../daemon/client.js");
|
|
11
12
|
const list_devices_js_1 = require("./list-devices.js");
|
|
@@ -18,7 +19,7 @@ async function shutdownSimulator(udid) {
|
|
|
18
19
|
}
|
|
19
20
|
// ── Android ──────────────────────────────────────────────────────────────────
|
|
20
21
|
async function killEmulator(serial) {
|
|
21
|
-
const result = await (0, runner_js_1.spawnCommand)('adb', ['-s', serial, 'emu', 'kill']);
|
|
22
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', serial, 'emu', 'kill']);
|
|
22
23
|
if (!result.success) {
|
|
23
24
|
throw new Error(`Failed to kill emulator ${serial}: ${result.stderr.trim()}`);
|
|
24
25
|
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const url_1 = __importDefault(require("url"));
|
|
|
19
19
|
const fs_1 = __importDefault(require("fs"));
|
|
20
20
|
const path_1 = __importDefault(require("path"));
|
|
21
21
|
const protocol_js_1 = require("./protocol.js");
|
|
22
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
22
23
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
23
24
|
const android_js_1 = require("../drivers/android.js");
|
|
24
25
|
const web_server_js_1 = require("./web-server.js");
|
|
@@ -129,6 +130,7 @@ async function ensureDriverRunning() {
|
|
|
129
130
|
}
|
|
130
131
|
// ── Daemon main ──────────────────────────────────────────────────────────────
|
|
131
132
|
async function main() {
|
|
133
|
+
(0, sdk_js_1.ensureAndroidEnv)();
|
|
132
134
|
// Ensure per-session daemon directory exists
|
|
133
135
|
fs_1.default.mkdirSync(path_1.default.dirname(PID_FILE), { recursive: true });
|
|
134
136
|
fs_1.default.writeFileSync(PID_FILE, String(process.pid));
|
|
@@ -868,6 +868,44 @@ async function handleRequest(req, res, dlog) {
|
|
|
868
868
|
}
|
|
869
869
|
return;
|
|
870
870
|
}
|
|
871
|
+
case '/heapSnapshot': {
|
|
872
|
+
// V8 heap snapshot via CDP. The snapshot streams as JSON chunks via
|
|
873
|
+
// `HeapProfiler.addHeapSnapshotChunk` events; we concatenate them and
|
|
874
|
+
// return the assembled JSON. Snapshots are large (10-100+ MB on real
|
|
875
|
+
// pages) — fine over localhost HTTP.
|
|
876
|
+
const p = await getPage(dlog);
|
|
877
|
+
const session = await p.context().newCDPSession(p);
|
|
878
|
+
const chunks = [];
|
|
879
|
+
const onChunk = (e) => {
|
|
880
|
+
chunks.push(e.chunk);
|
|
881
|
+
};
|
|
882
|
+
try {
|
|
883
|
+
await session.send('HeapProfiler.enable').catch(() => { });
|
|
884
|
+
// Optional: collect garbage before snapshotting so transient
|
|
885
|
+
// allocations don't muddy diff comparisons. Triggered via ?gc=1.
|
|
886
|
+
if (parsedUrl.query['gc']) {
|
|
887
|
+
await session.send('HeapProfiler.collectGarbage').catch(() => { });
|
|
888
|
+
}
|
|
889
|
+
session.on('HeapProfiler.addHeapSnapshotChunk', onChunk);
|
|
890
|
+
await session.send('HeapProfiler.takeHeapSnapshot', {
|
|
891
|
+
reportProgress: false,
|
|
892
|
+
captureNumericValue: false,
|
|
893
|
+
exposeInternals: false,
|
|
894
|
+
});
|
|
895
|
+
// Each chunk is already JSON text; concatenation yields the full snapshot JSON.
|
|
896
|
+
const body = chunks.join('');
|
|
897
|
+
res.writeHead(200, {
|
|
898
|
+
'Content-Type': 'application/json',
|
|
899
|
+
'Content-Length': Buffer.byteLength(body),
|
|
900
|
+
});
|
|
901
|
+
res.end(body);
|
|
902
|
+
}
|
|
903
|
+
finally {
|
|
904
|
+
session.off('HeapProfiler.addHeapSnapshotChunk', onChunk);
|
|
905
|
+
await session.detach().catch(() => { });
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
871
909
|
case '/consoleLogs': {
|
|
872
910
|
const since = parsedUrl.query['since'] ?? '';
|
|
873
911
|
const entries = since
|
package/dist/drivers/android.js
CHANGED
|
@@ -50,6 +50,7 @@ const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
|
50
50
|
const child_process_1 = require("child_process");
|
|
51
51
|
const fs_1 = __importDefault(require("fs"));
|
|
52
52
|
const path_1 = __importDefault(require("path"));
|
|
53
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
53
54
|
// __dirname is available in CommonJS — points to dist/drivers/
|
|
54
55
|
const PROTO_PATH = path_1.default.join(__dirname, '../../proto/conductor_android.proto');
|
|
55
56
|
let _packageDef = null;
|
|
@@ -148,7 +149,9 @@ class AndroidDriver {
|
|
|
148
149
|
// ── ADB-shell operations (not in gRPC proto) ─────────────────────────────
|
|
149
150
|
adb(args) {
|
|
150
151
|
return new Promise((resolve, reject) => {
|
|
151
|
-
const proc = (0, child_process_1.spawn)('adb', ['-s', this.deviceId, ...args], {
|
|
152
|
+
const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, ...args], {
|
|
153
|
+
stdio: 'ignore',
|
|
154
|
+
});
|
|
152
155
|
proc.on('close', (code) => {
|
|
153
156
|
if (code === 0)
|
|
154
157
|
resolve();
|
|
@@ -182,7 +185,7 @@ class AndroidDriver {
|
|
|
182
185
|
}
|
|
183
186
|
adbOutput(args) {
|
|
184
187
|
return new Promise((resolve, reject) => {
|
|
185
|
-
const proc = (0, child_process_1.spawn)('adb', ['-s', this.deviceId, ...args], {
|
|
188
|
+
const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, ...args], {
|
|
186
189
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
187
190
|
});
|
|
188
191
|
let stdout = '';
|
|
@@ -326,7 +329,7 @@ class AndroidDriver {
|
|
|
326
329
|
if (this._recordingProcess)
|
|
327
330
|
await this.stopRecording();
|
|
328
331
|
this._recordingOutputPath = outputPath;
|
|
329
|
-
this._recordingProcess = (0, child_process_1.spawn)('adb', ['-s', this.deviceId, 'shell', 'screenrecord', '/sdcard/conductor_recording.mp4'], { stdio: 'ignore' });
|
|
332
|
+
this._recordingProcess = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, 'shell', 'screenrecord', '/sdcard/conductor_recording.mp4'], { stdio: 'ignore' });
|
|
330
333
|
}
|
|
331
334
|
async stopRecording() {
|
|
332
335
|
if (this._recordingProcess) {
|
|
@@ -42,6 +42,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
42
42
|
const path_1 = __importDefault(require("path"));
|
|
43
43
|
const verbose_js_1 = require("../verbose.js");
|
|
44
44
|
const utils_js_1 = require("../utils.js");
|
|
45
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
45
46
|
/** Cache: deviceId → platform */
|
|
46
47
|
const _platformCache = new Map();
|
|
47
48
|
async function detectPlatform(deviceId) {
|
|
@@ -353,8 +354,24 @@ async function installDriver(deviceId) {
|
|
|
353
354
|
throw new Error(`Conductor driver APKs not found at ${path_1.default.join(driversDir, 'android')}.\n` +
|
|
354
355
|
`Run 'make package-cli' from the repo root to build and bundle the drivers.`);
|
|
355
356
|
}
|
|
356
|
-
await spawnAndWait('adb', [
|
|
357
|
-
|
|
357
|
+
await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
358
|
+
'-s',
|
|
359
|
+
deviceId,
|
|
360
|
+
'install',
|
|
361
|
+
'-r',
|
|
362
|
+
'-t',
|
|
363
|
+
'-g',
|
|
364
|
+
appApk,
|
|
365
|
+
]);
|
|
366
|
+
await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
367
|
+
'-s',
|
|
368
|
+
deviceId,
|
|
369
|
+
'install',
|
|
370
|
+
'-r',
|
|
371
|
+
'-t',
|
|
372
|
+
'-g',
|
|
373
|
+
serverApk,
|
|
374
|
+
]);
|
|
358
375
|
(0, verbose_js_1.log)(`installDriver: done`);
|
|
359
376
|
}
|
|
360
377
|
// ── iOS bootstrap ─────────────────────────────────────────────────────────────
|
|
@@ -598,9 +615,15 @@ async function startAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
|
|
|
598
615
|
}
|
|
599
616
|
(0, verbose_js_1.log)(`Starting Android driver for device ${deviceId} on port ${port}`);
|
|
600
617
|
// Step 1: ADB port forward
|
|
601
|
-
await spawnAndWait('adb', [
|
|
618
|
+
await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
619
|
+
'-s',
|
|
620
|
+
deviceId,
|
|
621
|
+
'forward',
|
|
622
|
+
`tcp:${port}`,
|
|
623
|
+
`tcp:${port}`,
|
|
624
|
+
]);
|
|
602
625
|
// Step 2: Get device API level to decide instrumentation flags
|
|
603
|
-
const apiResult = await spawnCapture('adb', [
|
|
626
|
+
const apiResult = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
604
627
|
'-s',
|
|
605
628
|
deviceId,
|
|
606
629
|
'shell',
|
|
@@ -628,7 +651,7 @@ async function startAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
|
|
|
628
651
|
String(port),
|
|
629
652
|
CONDUCTOR_TEST_RUNNER,
|
|
630
653
|
];
|
|
631
|
-
const proc = (0, child_process_1.spawn)('adb', instrArgs, {
|
|
654
|
+
const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), instrArgs, {
|
|
632
655
|
detached: true,
|
|
633
656
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
634
657
|
});
|
|
@@ -647,7 +670,7 @@ async function startAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
|
|
|
647
670
|
`Try running: conductor install --device ${deviceId}`);
|
|
648
671
|
}
|
|
649
672
|
async function stopAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
|
|
650
|
-
await spawnAndWait('adb', [
|
|
673
|
+
await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
651
674
|
'-s',
|
|
652
675
|
deviceId,
|
|
653
676
|
'shell',
|
|
@@ -655,7 +678,13 @@ async function stopAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
|
|
|
655
678
|
'force-stop',
|
|
656
679
|
'dev.houwert.conductor',
|
|
657
680
|
]).catch(() => { });
|
|
658
|
-
await spawnAndWait('adb', [
|
|
681
|
+
await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
682
|
+
'-s',
|
|
683
|
+
deviceId,
|
|
684
|
+
'forward',
|
|
685
|
+
'--remove',
|
|
686
|
+
`tcp:${port}`,
|
|
687
|
+
]).catch(() => { });
|
|
659
688
|
}
|
|
660
689
|
// ── Web bootstrap ────────────────────────────────────────────────────────────
|
|
661
690
|
/**
|
|
@@ -783,8 +812,18 @@ async function uninstallDriver(deviceId, platform) {
|
|
|
783
812
|
await spawnAndWait('xcrun', ['simctl', 'uninstall', deviceId, TVOS_RUNNER_BUNDLE_ID]).catch(() => { });
|
|
784
813
|
}
|
|
785
814
|
else {
|
|
786
|
-
await spawnAndWait(
|
|
787
|
-
|
|
815
|
+
await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
816
|
+
'-s',
|
|
817
|
+
deviceId,
|
|
818
|
+
'uninstall',
|
|
819
|
+
'dev.houwert.conductor',
|
|
820
|
+
]).catch(() => { });
|
|
821
|
+
await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
822
|
+
'-s',
|
|
823
|
+
deviceId,
|
|
824
|
+
'uninstall',
|
|
825
|
+
'dev.houwert.conductor.test',
|
|
826
|
+
]).catch(() => { });
|
|
788
827
|
}
|
|
789
828
|
}
|
|
790
829
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -5,6 +5,7 @@ exports.AndroidLogSource = void 0;
|
|
|
5
5
|
* Android log source — streams logs from an Android device/emulator via `adb logcat`.
|
|
6
6
|
*/
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const sdk_js_1 = require("../../android/sdk.js");
|
|
8
9
|
function mapPriority(priority) {
|
|
9
10
|
// Android log priorities: 2=V, 3=D, 4=I, 5=W, 6=E, 7=F
|
|
10
11
|
const p = typeof priority === 'string' ? priority.toUpperCase() : String(priority);
|
|
@@ -44,10 +45,7 @@ class AndroidLogSource {
|
|
|
44
45
|
let pid;
|
|
45
46
|
if (this.appId) {
|
|
46
47
|
try {
|
|
47
|
-
pid = (0, child_process_1.
|
|
48
|
-
encoding: 'utf-8',
|
|
49
|
-
timeout: 5000,
|
|
50
|
-
}).trim();
|
|
48
|
+
pid = (0, child_process_1.execFileSync)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, 'shell', 'pidof', this.appId], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
51
49
|
}
|
|
52
50
|
catch {
|
|
53
51
|
// App may not be running yet — proceed without PID filter
|
|
@@ -55,7 +53,9 @@ class AndroidLogSource {
|
|
|
55
53
|
}
|
|
56
54
|
// Clear existing logcat buffer so we start fresh
|
|
57
55
|
try {
|
|
58
|
-
(0, child_process_1.
|
|
56
|
+
(0, child_process_1.execFileSync)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, 'logcat', '-c'], {
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
});
|
|
59
59
|
}
|
|
60
60
|
catch {
|
|
61
61
|
// Ignore clear failures
|
|
@@ -71,7 +71,7 @@ class AndroidLogSource {
|
|
|
71
71
|
if (pid) {
|
|
72
72
|
args.push('--pid', pid);
|
|
73
73
|
}
|
|
74
|
-
this.proc = (0, child_process_1.spawn)('adb', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
74
|
+
this.proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
75
75
|
let stderrChunks = '';
|
|
76
76
|
this.proc.stderr.on('data', (chunk) => {
|
|
77
77
|
stderrChunks += chunk.toString('utf-8');
|
|
@@ -24,6 +24,7 @@ exports.targetsForDevice = targetsForDevice;
|
|
|
24
24
|
*/
|
|
25
25
|
const child_process_1 = require("child_process");
|
|
26
26
|
const metro_js_1 = require("./metro.js");
|
|
27
|
+
const sdk_js_1 = require("../../android/sdk.js");
|
|
27
28
|
/** Metro dev-server port ranges we consider. */
|
|
28
29
|
const METRO_PORT_RANGES = [
|
|
29
30
|
[8080, 8099], // Metro default range
|
|
@@ -58,7 +59,12 @@ async function discoverMetroPortForDevice(platform, deviceId) {
|
|
|
58
59
|
}
|
|
59
60
|
async function discoverMetroPortAndroid(deviceId) {
|
|
60
61
|
try {
|
|
61
|
-
const output = await spawnCapture('adb', [
|
|
62
|
+
const output = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
63
|
+
'-s',
|
|
64
|
+
deviceId,
|
|
65
|
+
'reverse',
|
|
66
|
+
'--list',
|
|
67
|
+
]);
|
|
62
68
|
// Lines look like: host-13 tcp:8082 tcp:8082
|
|
63
69
|
for (const line of output.split('\n')) {
|
|
64
70
|
const match = line.match(/tcp:(\d+)\s+tcp:(\d+)/);
|
|
@@ -172,7 +178,7 @@ async function getDeviceDisplayName(platform, deviceId) {
|
|
|
172
178
|
}
|
|
173
179
|
if (platform === 'android') {
|
|
174
180
|
try {
|
|
175
|
-
const output = await spawnCapture('adb', [
|
|
181
|
+
const output = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
176
182
|
'-s',
|
|
177
183
|
deviceId,
|
|
178
184
|
'shell',
|
package/dist/drivers/web.js
CHANGED
|
@@ -18,7 +18,7 @@ class WebDriver {
|
|
|
18
18
|
this.host = host;
|
|
19
19
|
this.deviceId = deviceId;
|
|
20
20
|
}
|
|
21
|
-
request(method, path, body) {
|
|
21
|
+
request(method, path, body, timeoutMs = 30000) {
|
|
22
22
|
return new Promise((resolve, reject) => {
|
|
23
23
|
const bodyBuf = body !== undefined ? Buffer.from(JSON.stringify(body), 'utf-8') : undefined;
|
|
24
24
|
const options = {
|
|
@@ -38,7 +38,7 @@ class WebDriver {
|
|
|
38
38
|
res.on('end', () => resolve({ status: res.statusCode ?? 0, data: Buffer.concat(chunks) }));
|
|
39
39
|
res.on('error', reject);
|
|
40
40
|
});
|
|
41
|
-
req.setTimeout(
|
|
41
|
+
req.setTimeout(timeoutMs, () => {
|
|
42
42
|
req.destroy(new Error(`Web driver request timed out: ${method} ${path}`));
|
|
43
43
|
});
|
|
44
44
|
req.on('error', reject);
|
|
@@ -139,6 +139,20 @@ class WebDriver {
|
|
|
139
139
|
async memory() {
|
|
140
140
|
return this.get('memory');
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Take a V8 heap snapshot via CDP. Returns the raw JSON text (large; tens
|
|
144
|
+
* of MB on a real page). Caller can write it to a `.heapsnapshot` file for
|
|
145
|
+
* Chrome DevTools and/or parse it for class statistics.
|
|
146
|
+
*/
|
|
147
|
+
async heapSnapshot(opts = {}) {
|
|
148
|
+
// 5-minute timeout — large pages can take 30-60s to snapshot + transfer.
|
|
149
|
+
const qs = opts.gc ? '?gc=1' : '';
|
|
150
|
+
const { status, data } = await this.request('GET', `/heapSnapshot${qs}`, undefined, 5 * 60000);
|
|
151
|
+
if (status < 200 || status >= 300) {
|
|
152
|
+
throw new Error(`Web driver heapSnapshot failed (HTTP ${status}): ${data.toString('utf-8').slice(0, 200)}`);
|
|
153
|
+
}
|
|
154
|
+
return data.toString('utf-8');
|
|
155
|
+
}
|
|
142
156
|
async eraseAllText(count = 50) {
|
|
143
157
|
await this.post('eraseText', { count });
|
|
144
158
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const minimist_1 = __importDefault(require("minimist"));
|
|
8
8
|
const verbose_js_1 = require("./verbose.js");
|
|
9
|
+
const sdk_js_1 = require("./android/sdk.js");
|
|
9
10
|
const list_devices_js_1 = require("./commands/list-devices.js");
|
|
10
11
|
const launch_app_js_1 = require("./commands/launch-app.js");
|
|
11
12
|
const stop_app_js_1 = require("./commands/stop-app.js");
|
|
@@ -110,6 +111,7 @@ ${Object.values(COMMAND_HELP).join('\n')}
|
|
|
110
111
|
|
|
111
112
|
${OPTIONS_HELP}`;
|
|
112
113
|
async function main() {
|
|
114
|
+
(0, sdk_js_1.ensureAndroidEnv)();
|
|
113
115
|
(0, update_check_js_1.checkForUpdates)();
|
|
114
116
|
const argv = (0, minimist_1.default)(process.argv.slice(2), {
|
|
115
117
|
boolean: [
|
|
@@ -131,6 +133,11 @@ async function main() {
|
|
|
131
133
|
'optional',
|
|
132
134
|
'benchmark',
|
|
133
135
|
'dump',
|
|
136
|
+
'objects',
|
|
137
|
+
'heap',
|
|
138
|
+
'leaks',
|
|
139
|
+
'snapshots',
|
|
140
|
+
'growth-only',
|
|
134
141
|
],
|
|
135
142
|
string: [
|
|
136
143
|
'device',
|
|
@@ -160,6 +167,11 @@ async function main() {
|
|
|
160
167
|
'to',
|
|
161
168
|
'source',
|
|
162
169
|
'level',
|
|
170
|
+
'save',
|
|
171
|
+
'diff',
|
|
172
|
+
'vs',
|
|
173
|
+
'top',
|
|
174
|
+
'filter',
|
|
163
175
|
],
|
|
164
176
|
alias: { h: 'help', v: 'verbose', V: 'version' },
|
|
165
177
|
});
|
|
@@ -475,7 +487,20 @@ async function main() {
|
|
|
475
487
|
break;
|
|
476
488
|
case 'memory': {
|
|
477
489
|
const appId = rest[0];
|
|
478
|
-
|
|
490
|
+
const all = argv['all'];
|
|
491
|
+
exitCode = await (0, memory_js_1.memory)(appId, opts, sessionName, {
|
|
492
|
+
objects: argv['objects'] || argv['heap'] || all,
|
|
493
|
+
leaks: argv['leaks'] || all,
|
|
494
|
+
top: argv['top'] !== undefined ? Number(argv['top']) : undefined,
|
|
495
|
+
save: argv['save'],
|
|
496
|
+
diff: argv['diff'],
|
|
497
|
+
diffOther: argv['vs'],
|
|
498
|
+
listSnapshots: argv['snapshots'],
|
|
499
|
+
// minimist treats --no-gc as gc=false; default-on lives in memory.ts.
|
|
500
|
+
gc: argv['gc'],
|
|
501
|
+
filter: argv['filter'],
|
|
502
|
+
growthOnly: argv['growth-only'],
|
|
503
|
+
});
|
|
479
504
|
break;
|
|
480
505
|
}
|
|
481
506
|
case 'run-flow': {
|
package/dist/runner.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.runDirect = runDirect;
|
|
|
6
6
|
exports.spawnCommand = spawnCommand;
|
|
7
7
|
exports.runInlineFlow = runInlineFlow;
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
|
+
const sdk_js_1 = require("./android/sdk.js");
|
|
9
10
|
const session_js_1 = require("./session.js");
|
|
10
11
|
const flow_runner_js_1 = require("./drivers/flow-runner.js");
|
|
11
12
|
const verbose_js_1 = require("./verbose.js");
|
|
@@ -24,7 +25,9 @@ async function detectFirstDevice() {
|
|
|
24
25
|
if (_cachedDeviceId !== undefined)
|
|
25
26
|
return _cachedDeviceId ?? undefined;
|
|
26
27
|
// Android: adb devices
|
|
27
|
-
const adb = await spawnCommand('adb', ['devices', '-l']
|
|
28
|
+
const adb = await spawnCommand((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices', '-l'], {
|
|
29
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
30
|
+
}).catch(() => null);
|
|
28
31
|
if (adb) {
|
|
29
32
|
for (const line of adb.stdout.split('\n').slice(1)) {
|
|
30
33
|
const id = line.trim().split(/\s+/)[0];
|
|
@@ -217,9 +220,12 @@ async function runDirect(fn, sessionName = 'default') {
|
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
222
|
// ── Spawn helpers ─────────────────────────────────────────────────────────────
|
|
220
|
-
async function spawnCommand(cmd, args) {
|
|
223
|
+
async function spawnCommand(cmd, args, options) {
|
|
221
224
|
return new Promise((resolve) => {
|
|
222
|
-
const proc = (0, child_process_1.spawn)(cmd, args, {
|
|
225
|
+
const proc = (0, child_process_1.spawn)(cmd, args, {
|
|
226
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
227
|
+
env: options?.env,
|
|
228
|
+
});
|
|
223
229
|
let stdout = '';
|
|
224
230
|
let stderr = '';
|
|
225
231
|
proc.stdout.on('data', (chunk) => {
|