@houwert/conductor 0.13.1 → 0.15.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/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 +729 -57
- package/dist/commands/run-parallel.js +2 -1
- package/dist/commands/start-device.js +185 -23
- 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 +9 -4
- 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 +28 -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 !== '')
|
|
@@ -4,17 +4,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HELP = void 0;
|
|
7
|
+
exports.pickAndroidArch = pickAndroidArch;
|
|
8
|
+
exports.parseInstalledSystemImages = parseInstalledSystemImages;
|
|
9
|
+
exports.pickSystemImage = pickSystemImage;
|
|
10
|
+
exports.buildAvdmanagerCreateArgs = buildAvdmanagerCreateArgs;
|
|
7
11
|
exports.startDevice = startDevice;
|
|
8
12
|
exports.HELP = ` start-device
|
|
9
13
|
--platform <ios|android|tvos|web> Boot a simulator/emulator, or start the web driver (Playwright)
|
|
10
14
|
--os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
|
|
11
|
-
--avd <name> Android AVD name (default: first available)
|
|
15
|
+
--avd <name> Android AVD name (default: first available; created if missing + --device-type)
|
|
12
16
|
--name <name> Set a custom name on the device after creation (iOS/tvOS/web)
|
|
13
|
-
--device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K")
|
|
17
|
+
--device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K") or
|
|
18
|
+
Android device profile (e.g. "pixel_7"); creates if needed
|
|
19
|
+
--system-image <id> Android only: override auto-picked system image
|
|
20
|
+
(e.g. "system-images;android-34;google_apis;arm64-v8a")
|
|
14
21
|
--browser <chromium|firefox|webkit> Web only: which Playwright browser to launch (default: chromium)`;
|
|
15
22
|
const fs_1 = __importDefault(require("fs"));
|
|
16
23
|
const child_process_1 = require("child_process");
|
|
17
24
|
const runner_js_1 = require("../runner.js");
|
|
25
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
18
26
|
const client_js_1 = require("../daemon/client.js");
|
|
19
27
|
const protocol_js_1 = require("../daemon/protocol.js");
|
|
20
28
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
@@ -389,8 +397,13 @@ async function startTvOS(osVersion, opts, name, deviceType) {
|
|
|
389
397
|
return 0;
|
|
390
398
|
}
|
|
391
399
|
// ── Android ───────────────────────────────────────────────────────────────────
|
|
400
|
+
// TODO: Replace bare command names with the resolver from `android-sdk-path-resolver-68477d`
|
|
401
|
+
// once it lands on main. For now we rely on `emulator`/`adb`/`avdmanager`/`sdkmanager`
|
|
402
|
+
// being on PATH.
|
|
392
403
|
async function listAVDs() {
|
|
393
|
-
const result = await (0, runner_js_1.spawnCommand)('emulator', ['-list-avds']
|
|
404
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-list-avds'], {
|
|
405
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
406
|
+
});
|
|
394
407
|
if (!result.success)
|
|
395
408
|
throw new Error(`emulator -list-avds failed: ${result.stderr}`);
|
|
396
409
|
return result.stdout
|
|
@@ -398,11 +411,146 @@ async function listAVDs() {
|
|
|
398
411
|
.map((l) => l.trim())
|
|
399
412
|
.filter(Boolean);
|
|
400
413
|
}
|
|
414
|
+
/** Pick the Android system-image arch tag based on the host CPU. */
|
|
415
|
+
function pickAndroidArch(arch = process.arch) {
|
|
416
|
+
return arch === 'arm64' ? 'arm64-v8a' : 'x86_64';
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Parse `sdkmanager --list_installed` (or --list) output, returning installed
|
|
420
|
+
* system-image package paths like "system-images;android-34;google_apis;arm64-v8a".
|
|
421
|
+
*/
|
|
422
|
+
function parseInstalledSystemImages(stdout) {
|
|
423
|
+
const images = [];
|
|
424
|
+
for (const rawLine of stdout.split('\n')) {
|
|
425
|
+
const line = rawLine.trim();
|
|
426
|
+
if (!line.startsWith('system-images;'))
|
|
427
|
+
continue;
|
|
428
|
+
// Format is "<path> | <version> | <description> | <location>" with leading whitespace
|
|
429
|
+
const path = line.split(/[|\s]+/)[0];
|
|
430
|
+
if (path && !images.includes(path))
|
|
431
|
+
images.push(path);
|
|
432
|
+
}
|
|
433
|
+
return images;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Pick the best installed system image matching the requested API level and arch.
|
|
437
|
+
* Prefers `google_apis` over `default` (no Play store dependency on Apple Silicon
|
|
438
|
+
* where Play images often lack arm64). Returns undefined if no match.
|
|
439
|
+
*/
|
|
440
|
+
function pickSystemImage(installed, apiLevel, arch) {
|
|
441
|
+
const filtered = installed.filter((img) => {
|
|
442
|
+
const parts = img.split(';');
|
|
443
|
+
// parts: ["system-images", "android-<api>", "<variant>", "<arch>"]
|
|
444
|
+
if (parts.length < 4)
|
|
445
|
+
return false;
|
|
446
|
+
if (parts[3] !== arch)
|
|
447
|
+
return false;
|
|
448
|
+
if (apiLevel && parts[1] !== `android-${apiLevel}`)
|
|
449
|
+
return false;
|
|
450
|
+
return true;
|
|
451
|
+
});
|
|
452
|
+
if (filtered.length === 0)
|
|
453
|
+
return undefined;
|
|
454
|
+
// Variant preference: google_apis > google_apis_playstore > default > others
|
|
455
|
+
const rank = (img) => {
|
|
456
|
+
const variant = img.split(';')[2];
|
|
457
|
+
if (variant === 'google_apis')
|
|
458
|
+
return 0;
|
|
459
|
+
if (variant === 'google_apis_playstore')
|
|
460
|
+
return 1;
|
|
461
|
+
if (variant === 'default')
|
|
462
|
+
return 2;
|
|
463
|
+
return 3;
|
|
464
|
+
};
|
|
465
|
+
filtered.sort((a, b) => {
|
|
466
|
+
const r = rank(a) - rank(b);
|
|
467
|
+
if (r !== 0)
|
|
468
|
+
return r;
|
|
469
|
+
// Then prefer higher API level
|
|
470
|
+
const ai = parseInt(a.split(';')[1].replace('android-', ''), 10) || 0;
|
|
471
|
+
const bi = parseInt(b.split(';')[1].replace('android-', ''), 10) || 0;
|
|
472
|
+
return bi - ai;
|
|
473
|
+
});
|
|
474
|
+
return filtered[0];
|
|
475
|
+
}
|
|
476
|
+
/** Build the avdmanager argv for AVD creation — pure, exported for tests. */
|
|
477
|
+
function buildAvdmanagerCreateArgs(avdName, systemImage, deviceProfile) {
|
|
478
|
+
return ['create', 'avd', '-n', avdName, '-k', systemImage, '-d', deviceProfile];
|
|
479
|
+
}
|
|
480
|
+
async function listInstalledSystemImages() {
|
|
481
|
+
const result = await (0, runner_js_1.spawnCommand)('sdkmanager', ['--list_installed']);
|
|
482
|
+
if (!result.success) {
|
|
483
|
+
throw new Error(`sdkmanager --list_installed failed: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
484
|
+
}
|
|
485
|
+
return parseInstalledSystemImages(result.stdout);
|
|
486
|
+
}
|
|
487
|
+
/** Run avdmanager create avd, piping "no\n" so it skips the custom hardware prompt. */
|
|
488
|
+
async function spawnAvdmanagerCreate(args) {
|
|
489
|
+
await new Promise((resolve, reject) => {
|
|
490
|
+
const proc = (0, child_process_1.spawn)('avdmanager', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
491
|
+
let stderr = '';
|
|
492
|
+
proc.stdout.on('data', () => {
|
|
493
|
+
/* discard */
|
|
494
|
+
});
|
|
495
|
+
proc.stderr.on('data', (chunk) => {
|
|
496
|
+
stderr += chunk.toString();
|
|
497
|
+
});
|
|
498
|
+
proc.on('error', (err) => reject(err));
|
|
499
|
+
proc.on('close', (code) => {
|
|
500
|
+
if (code === 0)
|
|
501
|
+
resolve();
|
|
502
|
+
else
|
|
503
|
+
reject(new Error(`avdmanager exited ${code}: ${stderr.trim()}`));
|
|
504
|
+
});
|
|
505
|
+
proc.stdin.end('no\n');
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function createAndroidAVD(avdName, deviceProfile, apiLevel, systemImageOverride) {
|
|
509
|
+
const arch = pickAndroidArch();
|
|
510
|
+
let systemImage;
|
|
511
|
+
if (systemImageOverride) {
|
|
512
|
+
systemImage = systemImageOverride;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
let installed;
|
|
516
|
+
try {
|
|
517
|
+
installed = await listInstalledSystemImages();
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
521
|
+
throw new Error(`Could not list installed Android system images (${msg}). ` +
|
|
522
|
+
`Ensure the Android SDK command-line tools are installed and on PATH.`);
|
|
523
|
+
}
|
|
524
|
+
const picked = pickSystemImage(installed, apiLevel, arch);
|
|
525
|
+
if (!picked) {
|
|
526
|
+
const apiHint = apiLevel ?? '34';
|
|
527
|
+
throw new Error(`No installed Android system image matches arch=${arch}` +
|
|
528
|
+
(apiLevel ? `, api=${apiLevel}` : '') +
|
|
529
|
+
`.\nInstall one with:\n sdkmanager "system-images;android-${apiHint};google_apis;${arch}"`);
|
|
530
|
+
}
|
|
531
|
+
systemImage = picked;
|
|
532
|
+
}
|
|
533
|
+
const args = buildAvdmanagerCreateArgs(avdName, systemImage, deviceProfile);
|
|
534
|
+
try {
|
|
535
|
+
await spawnAvdmanagerCreate(args);
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
539
|
+
throw new Error(`Failed to create AVD "${avdName}": ${msg}`);
|
|
540
|
+
}
|
|
541
|
+
// Validate the AVD now appears in emulator -list-avds
|
|
542
|
+
const avds = await listAVDs();
|
|
543
|
+
if (!avds.includes(avdName)) {
|
|
544
|
+
throw new Error(`AVD "${avdName}" was not registered after creation (not in emulator -list-avds).`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
401
547
|
async function waitForAndroidBoot(avdName) {
|
|
402
548
|
const deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;
|
|
403
549
|
const connectedBefore = new Set();
|
|
404
550
|
// Snapshot currently connected devices so we can identify the new one
|
|
405
|
-
const before = await (0, runner_js_1.spawnCommand)('adb', ['devices']
|
|
551
|
+
const before = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices'], {
|
|
552
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
553
|
+
});
|
|
406
554
|
for (const line of before.stdout.split('\n').slice(1)) {
|
|
407
555
|
const id = line.trim().split(/\s+/)[0];
|
|
408
556
|
if (id)
|
|
@@ -410,7 +558,9 @@ async function waitForAndroidBoot(avdName) {
|
|
|
410
558
|
}
|
|
411
559
|
while (Date.now() < deadline) {
|
|
412
560
|
await (0, utils_js_1.sleep)(POLL_MS);
|
|
413
|
-
const result = await (0, runner_js_1.spawnCommand)('adb', ['devices']
|
|
561
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices'], {
|
|
562
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
563
|
+
});
|
|
414
564
|
if (!result.success)
|
|
415
565
|
continue;
|
|
416
566
|
for (const line of result.stdout.split('\n').slice(1)) {
|
|
@@ -419,13 +569,7 @@ async function waitForAndroidBoot(avdName) {
|
|
|
419
569
|
const status = parts[1];
|
|
420
570
|
if (id && status === 'device' && !connectedBefore.has(id)) {
|
|
421
571
|
// 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
|
-
]);
|
|
572
|
+
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
573
|
if (boot.stdout.trim() === '1')
|
|
430
574
|
return id;
|
|
431
575
|
}
|
|
@@ -433,7 +577,7 @@ async function waitForAndroidBoot(avdName) {
|
|
|
433
577
|
}
|
|
434
578
|
throw new Error(`Android emulator (${avdName}) did not appear within ${ANDROID_BOOT_TIMEOUT_MS / 1000}s`);
|
|
435
579
|
}
|
|
436
|
-
async function startAndroid(avdName, opts) {
|
|
580
|
+
async function startAndroid(avdName, opts, deviceType, osVersion, systemImage) {
|
|
437
581
|
let avds;
|
|
438
582
|
try {
|
|
439
583
|
avds = await listAVDs();
|
|
@@ -442,20 +586,38 @@ async function startAndroid(avdName, opts) {
|
|
|
442
586
|
(0, output_js_1.printError)(`Failed to list AVDs: ${e instanceof Error ? e.message : String(e)}`, opts);
|
|
443
587
|
return 1;
|
|
444
588
|
}
|
|
445
|
-
|
|
446
|
-
|
|
589
|
+
let target = avdName ?? avds[0];
|
|
590
|
+
// If the requested AVD doesn't exist (or none exist) and a device profile was
|
|
591
|
+
// provided, create the AVD before booting.
|
|
592
|
+
const needsCreate = (avds.length === 0 || (avdName !== undefined && !avds.includes(avdName))) &&
|
|
593
|
+
deviceType !== undefined;
|
|
594
|
+
if (needsCreate) {
|
|
595
|
+
if (!avdName) {
|
|
596
|
+
(0, output_js_1.printError)('--avd <name> is required when creating an AVD with --device-type.', opts);
|
|
597
|
+
return 1;
|
|
598
|
+
}
|
|
599
|
+
console.log(`No AVD "${avdName}" found. Creating one with device profile "${deviceType}"...`);
|
|
600
|
+
try {
|
|
601
|
+
await createAndroidAVD(avdName, deviceType, osVersion, systemImage);
|
|
602
|
+
}
|
|
603
|
+
catch (e) {
|
|
604
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
605
|
+
return 1;
|
|
606
|
+
}
|
|
607
|
+
target = avdName;
|
|
608
|
+
}
|
|
609
|
+
else if (avds.length === 0) {
|
|
610
|
+
(0, output_js_1.printError)('No Android AVDs found. Pass --device-type <profile> --avd <name> to create one, ' +
|
|
611
|
+
'or create one in Android Studio → Device Manager.', opts);
|
|
447
612
|
return 1;
|
|
448
613
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
614
|
+
else if (!avds.includes(target)) {
|
|
615
|
+
(0, output_js_1.printError)(`AVD "${target}" not found. Available: ${avds.join(', ')}. ` +
|
|
616
|
+
`Pass --device-type to create it.`, opts);
|
|
452
617
|
return 1;
|
|
453
618
|
}
|
|
454
619
|
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
|
-
});
|
|
620
|
+
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
621
|
proc.unref();
|
|
460
622
|
let deviceId;
|
|
461
623
|
try {
|
|
@@ -526,7 +688,7 @@ async function startDevice(platform, opts, flags) {
|
|
|
526
688
|
case 'tvos':
|
|
527
689
|
return startTvOS(flags.osVersion, opts, flags.name, flags.deviceType);
|
|
528
690
|
case 'android':
|
|
529
|
-
return startAndroid(flags.avd, opts);
|
|
691
|
+
return startAndroid(flags.avd, opts, flags.deviceType, flags.osVersion, flags.systemImage);
|
|
530
692
|
case 'web':
|
|
531
693
|
return startWebDriver(opts, flags.browser, flags.name);
|
|
532
694
|
default:
|
|
@@ -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 = '';
|
|
@@ -200,7 +203,9 @@ class AndroidDriver {
|
|
|
200
203
|
}
|
|
201
204
|
async getForegroundApp() {
|
|
202
205
|
const output = await this.adbOutput(['shell', 'dumpsys', 'activity', 'activities']);
|
|
203
|
-
|
|
206
|
+
// API 28- prints `mResumedActivity:`; API 29+ uses `ResumedActivity:` /
|
|
207
|
+
// `topResumedActivity=`. Accept all three so this works across versions.
|
|
208
|
+
const match = output.match(/(?:m|top)?ResumedActivity[=:].*?([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+)\//);
|
|
204
209
|
if (!match)
|
|
205
210
|
throw new Error('Could not determine foreground app');
|
|
206
211
|
return match[1];
|
|
@@ -326,7 +331,7 @@ class AndroidDriver {
|
|
|
326
331
|
if (this._recordingProcess)
|
|
327
332
|
await this.stopRecording();
|
|
328
333
|
this._recordingOutputPath = outputPath;
|
|
329
|
-
this._recordingProcess = (0, child_process_1.spawn)('adb', ['-s', this.deviceId, 'shell', 'screenrecord', '/sdcard/conductor_recording.mp4'], { stdio: 'ignore' });
|
|
334
|
+
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
335
|
}
|
|
331
336
|
async stopRecording() {
|
|
332
337
|
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
|
}
|