neoagent 2.3.1-beta.84 → 2.3.1-beta.86
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/package.json +1 -1
- package/runtime/paths.js +6 -6
- package/server/guest-agent.android.package.json +13 -0
- package/server/guest-agent.browser.package.json +14 -0
- package/server/guest_agent.js +61 -51
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/routes/android.js +2 -11
- package/server/routes/browser.js +2 -2
- package/server/services/ai/capabilityHealth.js +6 -14
- package/server/services/android/android_bootstrap_worker.js +2 -2
- package/server/services/android/controller.js +529 -133
- package/server/services/browser/controller.js +187 -42
- package/server/services/runtime/backends/local-vm.js +62 -33
- package/server/services/runtime/guest_bootstrap.js +287 -113
- package/server/services/runtime/manager.js +53 -15
- package/server/services/runtime/qemu.js +477 -86
- package/server/services/runtime/settings.js +9 -14
- package/server/services/runtime/validation.js +11 -38
- package/server/utils/deployment.js +4 -4
- package/server/guest-agent.package.json +0 -16
|
@@ -17,16 +17,20 @@ const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
|
|
|
17
17
|
const UI_DUMPS_DIR = path.join(ARTIFACTS_DIR, 'ui-dumps');
|
|
18
18
|
const LOGS_DIR = path.join(ARTIFACTS_DIR, 'logs');
|
|
19
19
|
const TMP_DIR = path.join(ARTIFACTS_DIR, 'tmp');
|
|
20
|
+
const EMULATOR_HOME = path.join(ANDROID_ROOT, 'home');
|
|
21
|
+
const EMULATOR_ADVANCED_FEATURES_FILE = path.join(EMULATOR_HOME, 'advancedFeatures.ini');
|
|
20
22
|
const AVD_HOME = path.join(ANDROID_ROOT, 'avd');
|
|
23
|
+
const EMULATOR_READY_SNAPSHOT = 'neoagent-ready';
|
|
21
24
|
const STATE_DIR = path.join(ARTIFACTS_DIR, 'state');
|
|
22
25
|
const STATE_FILE = path.join(ARTIFACTS_DIR, 'state.json');
|
|
23
26
|
const OWNERSHIP_FILE = path.join(ARTIFACTS_DIR, 'device-ownership.json');
|
|
24
27
|
const ANDROID_BOOTSTRAP_WORKER = path.join(__dirname, 'android_bootstrap_worker.js');
|
|
25
28
|
const ANDROID_JAVA_TOOL_TIMEOUT_MS = 20 * 60 * 1000;
|
|
26
29
|
const DEFAULT_AVD_NAME = 'neoagent-default';
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
+
const DEFAULT_DATA_PARTITION_BYTES = 1024 * 1024 * 1024;
|
|
31
|
+
const DEFAULT_PARTITION_SIZE_MB = 1024;
|
|
32
|
+
const DEFAULT_SDCARD_SIZE_BYTES = 32 * 1024 * 1024;
|
|
33
|
+
const DEFAULT_RAM_SIZE_MB = 768;
|
|
30
34
|
const DEFAULT_KEYEVENTS = Object.freeze({
|
|
31
35
|
home: 3,
|
|
32
36
|
back: 4,
|
|
@@ -44,10 +48,27 @@ const DEFAULT_KEYEVENTS = Object.freeze({
|
|
|
44
48
|
tab: 61,
|
|
45
49
|
});
|
|
46
50
|
|
|
47
|
-
for (const dir of [ANDROID_ROOT, SDK_ROOT, ARTIFACTS_DIR, SCREENSHOTS_DIR, UI_DUMPS_DIR, LOGS_DIR, TMP_DIR, AVD_HOME, STATE_DIR]) {
|
|
51
|
+
for (const dir of [ANDROID_ROOT, SDK_ROOT, EMULATOR_HOME, ARTIFACTS_DIR, SCREENSHOTS_DIR, UI_DUMPS_DIR, LOGS_DIR, TMP_DIR, AVD_HOME, STATE_DIR]) {
|
|
48
52
|
fs.mkdirSync(dir, { recursive: true });
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
function ensureEmulatorAdvancedFeaturesFile() {
|
|
56
|
+
const content = [
|
|
57
|
+
'Vulkan = off',
|
|
58
|
+
'GLDirectMem = off',
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(EMULATOR_ADVANCED_FEATURES_FILE)) {
|
|
63
|
+
const current = fs.readFileSync(EMULATOR_ADVANCED_FEATURES_FILE, 'utf8');
|
|
64
|
+
if (current.trim() === content.trim()) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
fs.writeFileSync(EMULATOR_ADVANCED_FEATURES_FILE, content);
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
function sanitizeScopeKey(value) {
|
|
52
73
|
const normalized = String(value || '')
|
|
53
74
|
.trim()
|
|
@@ -82,12 +103,25 @@ function readOwnershipUnlocked() {
|
|
|
82
103
|
if (!parsed || typeof parsed !== 'object') {
|
|
83
104
|
return {};
|
|
84
105
|
}
|
|
85
|
-
|
|
106
|
+
return parsed;
|
|
86
107
|
} catch {
|
|
87
108
|
return {};
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
112
|
+
function ensureSparseFile(filePath, sizeBytes) {
|
|
113
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
114
|
+
const fd = fs.openSync(filePath, 'a');
|
|
115
|
+
try {
|
|
116
|
+
const currentSize = fs.statSync(filePath).size;
|
|
117
|
+
if (currentSize !== sizeBytes) {
|
|
118
|
+
fs.ftruncateSync(fd, sizeBytes);
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
fs.closeSync(fd);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
function writeOwnershipUnlocked(nextOwners) {
|
|
92
126
|
const tempPath = `${OWNERSHIP_FILE}.tmp`;
|
|
93
127
|
fs.writeFileSync(tempPath, JSON.stringify(nextOwners, null, 2));
|
|
@@ -219,10 +253,37 @@ function platformTag() {
|
|
|
219
253
|
}
|
|
220
254
|
|
|
221
255
|
function systemImageArch() {
|
|
256
|
+
if (process.platform === 'darwin') return 'arm64-v8a';
|
|
222
257
|
if (process.arch === 'arm64') return 'arm64-v8a';
|
|
223
258
|
return 'x86_64';
|
|
224
259
|
}
|
|
225
260
|
|
|
261
|
+
function emulatorHostArch() {
|
|
262
|
+
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'aarch64' : 'x64';
|
|
263
|
+
if (process.arch === 'arm64') return 'aarch64';
|
|
264
|
+
return 'x64';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function emulatorGpuMode() {
|
|
268
|
+
return 'swiftshader_indirect';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function emulatorSnapshotStoragePath(avdName) {
|
|
272
|
+
return path.join(AVD_HOME, `${avdName}.avd`, 'snapshots.img');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function emulatorLaunchArgs(avdName) {
|
|
276
|
+
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
277
|
+
return ['-no-snapshot'];
|
|
278
|
+
}
|
|
279
|
+
return [
|
|
280
|
+
'-snapshot',
|
|
281
|
+
EMULATOR_READY_SNAPSHOT,
|
|
282
|
+
'-snapstorage',
|
|
283
|
+
emulatorSnapshotStoragePath(avdName),
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
|
|
226
287
|
function parseCsvEnv(value) {
|
|
227
288
|
return String(value || '')
|
|
228
289
|
.split(',')
|
|
@@ -252,6 +313,28 @@ function systemImageArchCandidates() {
|
|
|
252
313
|
return [preferred, ...fallbacks].filter((arch, index, list) => list.indexOf(arch) === index);
|
|
253
314
|
}
|
|
254
315
|
|
|
316
|
+
function installedEmulatorMatchesHost() {
|
|
317
|
+
const binary = emulatorBinary();
|
|
318
|
+
if (!isExecutable(binary)) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const probe = spawnSync('file', [binary], { encoding: 'utf8' });
|
|
323
|
+
if (probe.status !== 0) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const output = `${probe.stdout || ''}${probe.stderr || ''}`.toLowerCase();
|
|
328
|
+
const hostArch = emulatorHostArch();
|
|
329
|
+
if (hostArch === 'aarch64') {
|
|
330
|
+
return output.includes('arm64') || output.includes('aarch64');
|
|
331
|
+
}
|
|
332
|
+
if (hostArch === 'x64') {
|
|
333
|
+
return output.includes('x86_64');
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
255
338
|
function parseSystemImagePlatform(platformId) {
|
|
256
339
|
const stable = String(platformId || '').match(/^android-(\d+)$/);
|
|
257
340
|
if (stable) {
|
|
@@ -279,38 +362,107 @@ function parseSystemImagePlatform(platformId) {
|
|
|
279
362
|
}
|
|
280
363
|
|
|
281
364
|
function sdkEnv() {
|
|
365
|
+
ensureEmulatorAdvancedFeaturesFile();
|
|
366
|
+
const sdkRoot = activeAndroidSdkRoot();
|
|
282
367
|
const base = {
|
|
283
368
|
...process.env,
|
|
284
|
-
ANDROID_HOME:
|
|
285
|
-
ANDROID_SDK_ROOT:
|
|
369
|
+
ANDROID_HOME: sdkRoot,
|
|
370
|
+
ANDROID_SDK_ROOT: sdkRoot,
|
|
371
|
+
ANDROID_EMULATOR_HOME: EMULATOR_HOME,
|
|
372
|
+
ANDROID_USER_HOME: EMULATOR_HOME,
|
|
286
373
|
ANDROID_AVD_HOME: AVD_HOME,
|
|
287
374
|
AVD_HOME,
|
|
288
375
|
JAVA_TOOL_OPTIONS: process.env.JAVA_TOOL_OPTIONS || '-Xint',
|
|
289
376
|
};
|
|
290
377
|
const pathParts = [
|
|
291
|
-
path.join(
|
|
292
|
-
path.join(
|
|
293
|
-
path.join(
|
|
378
|
+
path.join(sdkRoot, 'platform-tools'),
|
|
379
|
+
path.join(sdkRoot, 'emulator'),
|
|
380
|
+
path.join(sdkRoot, 'cmdline-tools', 'latest', 'bin'),
|
|
294
381
|
process.env.PATH || '',
|
|
295
382
|
].filter(Boolean);
|
|
296
383
|
base.PATH = pathParts.join(path.delimiter);
|
|
297
384
|
return base;
|
|
298
385
|
}
|
|
299
386
|
|
|
387
|
+
function systemAdbBinary() {
|
|
388
|
+
if (process.platform === 'win32') return null;
|
|
389
|
+
return '/usr/bin/adb';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function hostAdbBinary() {
|
|
393
|
+
const probe = spawnSync('bash', ['-lc', 'command -v adb'], { encoding: 'utf8' });
|
|
394
|
+
if (probe.status !== 0) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const binary = String(probe.stdout || '').trim();
|
|
398
|
+
return binary && isExecutable(binary) ? binary : null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function hostAndroidSdkRoot() {
|
|
402
|
+
const adb = hostAdbBinary();
|
|
403
|
+
if (!adb) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const root = path.dirname(path.dirname(adb));
|
|
408
|
+
const sdkmanager = path.join(root, 'cmdline-tools', 'latest', 'bin', process.platform === 'win32' ? 'sdkmanager.bat' : 'sdkmanager');
|
|
409
|
+
const avdmanager = path.join(root, 'cmdline-tools', 'latest', 'bin', process.platform === 'win32' ? 'avdmanager.bat' : 'avdmanager');
|
|
410
|
+
const emulator = path.join(root, 'emulator', process.platform === 'win32' ? 'emulator.exe' : 'emulator');
|
|
411
|
+
if (isExecutable(sdkmanager) && isExecutable(avdmanager) && isExecutable(emulator)) {
|
|
412
|
+
return root;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const SHARED_ANDROID_SDK_ROOT = hostAndroidSdkRoot();
|
|
419
|
+
|
|
420
|
+
function activeAndroidSdkRoot() {
|
|
421
|
+
return SHARED_ANDROID_SDK_ROOT || SDK_ROOT;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function sharedAndroidSdkReady() {
|
|
425
|
+
if (!SHARED_ANDROID_SDK_ROOT) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
const adb = path.join(SHARED_ANDROID_SDK_ROOT, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
429
|
+
const sdkmanager = path.join(SHARED_ANDROID_SDK_ROOT, 'cmdline-tools', 'latest', 'bin', process.platform === 'win32' ? 'sdkmanager.bat' : 'sdkmanager');
|
|
430
|
+
const avdmanager = path.join(SHARED_ANDROID_SDK_ROOT, 'cmdline-tools', 'latest', 'bin', process.platform === 'win32' ? 'avdmanager.bat' : 'avdmanager');
|
|
431
|
+
const emulator = path.join(SHARED_ANDROID_SDK_ROOT, 'emulator', process.platform === 'win32' ? 'emulator.exe' : 'emulator');
|
|
432
|
+
return [adb, sdkmanager, avdmanager, emulator].every(isExecutable);
|
|
433
|
+
}
|
|
434
|
+
|
|
300
435
|
function adbBinary() {
|
|
301
|
-
|
|
436
|
+
if (process.env.ANDROID_ADB_PATH) {
|
|
437
|
+
return process.env.ANDROID_ADB_PATH;
|
|
438
|
+
}
|
|
439
|
+
if (SHARED_ANDROID_SDK_ROOT) {
|
|
440
|
+
const sharedAdb = path.join(SHARED_ANDROID_SDK_ROOT, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
441
|
+
if (isExecutable(sharedAdb)) {
|
|
442
|
+
return sharedAdb;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const hostAdb = hostAdbBinary();
|
|
446
|
+
if (hostAdb) {
|
|
447
|
+
return hostAdb;
|
|
448
|
+
}
|
|
449
|
+
const distroAdb = systemAdbBinary();
|
|
450
|
+
if (process.platform === 'linux' && process.arch === 'arm64' && distroAdb && isExecutable(distroAdb)) {
|
|
451
|
+
return distroAdb;
|
|
452
|
+
}
|
|
453
|
+
return path.join(SDK_ROOT, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
302
454
|
}
|
|
303
455
|
|
|
304
456
|
function sdkManagerBinary() {
|
|
305
|
-
return path.join(
|
|
457
|
+
return path.join(activeAndroidSdkRoot(), 'cmdline-tools', 'latest', 'bin', process.platform === 'win32' ? 'sdkmanager.bat' : 'sdkmanager');
|
|
306
458
|
}
|
|
307
459
|
|
|
308
460
|
function avdManagerBinary() {
|
|
309
|
-
return path.join(
|
|
461
|
+
return path.join(activeAndroidSdkRoot(), 'cmdline-tools', 'latest', 'bin', process.platform === 'win32' ? 'avdmanager.bat' : 'avdmanager');
|
|
310
462
|
}
|
|
311
463
|
|
|
312
464
|
function emulatorBinary() {
|
|
313
|
-
return path.join(
|
|
465
|
+
return path.join(activeAndroidSdkRoot(), 'emulator', process.platform === 'win32' ? 'emulator.exe' : 'emulator');
|
|
314
466
|
}
|
|
315
467
|
|
|
316
468
|
function isExecutable(filePath) {
|
|
@@ -341,6 +493,47 @@ function fetchText(url) {
|
|
|
341
493
|
}
|
|
342
494
|
|
|
343
495
|
function downloadFile(url, dest) {
|
|
496
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
497
|
+
const curlBin = process.platform === 'win32' ? 'curl.exe' : 'curl';
|
|
498
|
+
if (commandExists(curlBin)) {
|
|
499
|
+
const curlResult = spawnSync(curlBin, [
|
|
500
|
+
'--fail',
|
|
501
|
+
'--location',
|
|
502
|
+
'--silent',
|
|
503
|
+
'--show-error',
|
|
504
|
+
'--retry', '3',
|
|
505
|
+
'--retry-delay', '2',
|
|
506
|
+
'--output', dest,
|
|
507
|
+
url,
|
|
508
|
+
], {
|
|
509
|
+
encoding: 'utf8',
|
|
510
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
511
|
+
});
|
|
512
|
+
if (curlResult.status === 0) {
|
|
513
|
+
return waitForReadyFile(dest);
|
|
514
|
+
}
|
|
515
|
+
const detail = String(curlResult.stderr || curlResult.stdout || curlResult.error?.message || `curl exited with code ${curlResult.status ?? 'unknown'}`).trim();
|
|
516
|
+
console.warn(`[Android] curl download failed for ${url}; falling back to Node HTTPS (${detail || 'no detail'})`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return downloadFileViaHttps(url, dest);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function waitForReadyFile(filePath, timeoutMs = 600000) {
|
|
523
|
+
const deadline = Date.now() + timeoutMs;
|
|
524
|
+
while (Date.now() < deadline) {
|
|
525
|
+
try {
|
|
526
|
+
const stats = fs.statSync(filePath);
|
|
527
|
+
if (stats.isFile() && stats.size > 0) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
} catch {}
|
|
531
|
+
await sleep(250);
|
|
532
|
+
}
|
|
533
|
+
throw new Error(`Downloaded file was not ready at ${filePath}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function downloadFileViaHttps(url, dest) {
|
|
344
537
|
return new Promise((resolve, reject) => {
|
|
345
538
|
const out = fs.createWriteStream(dest);
|
|
346
539
|
https.get(url, (res) => {
|
|
@@ -356,7 +549,13 @@ function downloadFile(url, dest) {
|
|
|
356
549
|
return;
|
|
357
550
|
}
|
|
358
551
|
res.pipe(out);
|
|
359
|
-
out.on('finish', () => out.close(
|
|
552
|
+
out.on('finish', () => out.close((closeErr) => {
|
|
553
|
+
if (closeErr) {
|
|
554
|
+
reject(closeErr);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
resolve(waitForReadyFile(dest));
|
|
558
|
+
}));
|
|
360
559
|
}).on('error', (err) => {
|
|
361
560
|
out.close();
|
|
362
561
|
fs.rmSync(dest, { force: true });
|
|
@@ -366,19 +565,33 @@ function downloadFile(url, dest) {
|
|
|
366
565
|
}
|
|
367
566
|
|
|
368
567
|
function extractZip(zipPath, destDir) {
|
|
568
|
+
if (process.platform === 'darwin' && commandExists('ditto')) {
|
|
569
|
+
const res = spawnSync('ditto', ['-x', '-k', zipPath, destDir], { encoding: 'utf8' });
|
|
570
|
+
if (res.status === 0) return;
|
|
571
|
+
console.warn(`[Android] ditto failed for ${zipPath}; falling back to unzip`);
|
|
572
|
+
}
|
|
573
|
+
|
|
369
574
|
if (commandExists('unzip')) {
|
|
370
575
|
const res = spawnSync('unzip', ['-qo', zipPath, '-d', destDir], { encoding: 'utf8' });
|
|
371
576
|
if (res.status === 0) return;
|
|
372
577
|
throw new Error(res.stderr || `unzip failed for ${zipPath}`);
|
|
373
578
|
}
|
|
374
579
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
580
|
+
throw new Error('Neither unzip nor ditto is available to extract Android SDK archives');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function clearQuarantineAttribute(targetPath) {
|
|
584
|
+
if (process.platform !== 'darwin' || !commandExists('xattr')) {
|
|
585
|
+
return;
|
|
379
586
|
}
|
|
587
|
+
spawnSync('xattr', ['-dr', 'com.apple.quarantine', targetPath], { encoding: 'utf8' });
|
|
588
|
+
}
|
|
380
589
|
|
|
381
|
-
|
|
590
|
+
function codesignAdHoc(targetPath) {
|
|
591
|
+
if (process.platform !== 'darwin' || !commandExists('codesign')) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
spawnSync('codesign', ['--force', '--deep', '--sign', '-', targetPath], { encoding: 'utf8' });
|
|
382
595
|
}
|
|
383
596
|
|
|
384
597
|
function listFilesRecursive(rootDir, predicate, bucket = []) {
|
|
@@ -441,13 +654,14 @@ function parseLatestCmdlineToolsUrl(xml) {
|
|
|
441
654
|
|
|
442
655
|
function parseLatestEmulatorUrl(xml) {
|
|
443
656
|
const tag = platformTag() === 'mac' ? 'macosx' : 'linux';
|
|
657
|
+
const hostArch = emulatorHostArch();
|
|
444
658
|
const packageMatch = xml.match(new RegExp(`<remotePackage\\s+path="emulator">([\\s\\S]*?)<\\/remotePackage>`));
|
|
445
659
|
if (!packageMatch) throw new Error('Could not locate emulator in Android repository metadata');
|
|
446
660
|
|
|
447
661
|
const archiveBlocks = packageMatch[1].match(/<archive>[\s\S]*?<\/archive>/g) || [];
|
|
448
662
|
for (const block of archiveBlocks) {
|
|
449
663
|
if (!new RegExp(`<host-os>${tag}<\\/host-os>`).test(block)) continue;
|
|
450
|
-
if (
|
|
664
|
+
if (!new RegExp(`<host-arch>${hostArch}<\\/host-arch>`).test(block)) continue;
|
|
451
665
|
const urlMatch = block.match(/<url>\s*([^<]*emulator-[^<]+\.zip)\s*<\/url>/);
|
|
452
666
|
if (urlMatch) return `https://dl.google.com/android/repository/${urlMatch[1]}`;
|
|
453
667
|
}
|
|
@@ -479,7 +693,7 @@ function findDirectoryContainingFiles(rootDir, requiredFiles) {
|
|
|
479
693
|
}
|
|
480
694
|
|
|
481
695
|
function parseLatestPlatformToolsUrl(xml) {
|
|
482
|
-
const tag = platformTag() === 'mac' ? '
|
|
696
|
+
const tag = platformTag() === 'mac' ? 'macosx' : 'linux';
|
|
483
697
|
const packageMatch = xml.match(new RegExp(`<remotePackage\\s+path="platform-tools">([\\s\\S]*?)<\\/remotePackage>`));
|
|
484
698
|
if (!packageMatch) throw new Error('Could not locate platform-tools in Android repository metadata');
|
|
485
699
|
|
|
@@ -526,83 +740,109 @@ function parseLatestSystemImageUrl(xml, packageName) {
|
|
|
526
740
|
throw new Error(`Could not find a system image archive for ${packageName}`);
|
|
527
741
|
}
|
|
528
742
|
|
|
743
|
+
async function fetchEmulatorMetadata() {
|
|
744
|
+
const urls = [
|
|
745
|
+
'https://dl.google.com/android/repository/repository2-3.xml',
|
|
746
|
+
'https://dl.google.com/android/repository/repository2-1.xml',
|
|
747
|
+
];
|
|
748
|
+
let lastError = null;
|
|
749
|
+
for (const url of urls) {
|
|
750
|
+
try {
|
|
751
|
+
return await fetchText(url);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
lastError = error;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
throw lastError || new Error('Could not fetch Android emulator repository metadata');
|
|
757
|
+
}
|
|
758
|
+
|
|
529
759
|
async function installEmulatorArchive(metadata) {
|
|
530
760
|
const url = parseLatestEmulatorUrl(metadata);
|
|
531
761
|
const zipPath = path.join(TMP_DIR, path.basename(url));
|
|
532
|
-
const
|
|
762
|
+
const installDir = path.join(activeAndroidSdkRoot(), 'emulator');
|
|
533
763
|
|
|
534
|
-
fs.mkdirSync(extractDir, { recursive: true });
|
|
535
764
|
await downloadFile(url, zipPath);
|
|
536
|
-
|
|
765
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
766
|
+
extractZip(zipPath, activeAndroidSdkRoot());
|
|
537
767
|
|
|
538
|
-
const extractedRoot = findDirectoryContainingFiles(
|
|
768
|
+
const extractedRoot = findDirectoryContainingFiles(installDir, [
|
|
539
769
|
process.platform === 'win32' ? 'emulator.exe' : 'emulator',
|
|
540
770
|
]);
|
|
541
771
|
if (!extractedRoot) {
|
|
542
772
|
throw new Error('Downloaded Android emulator archive did not contain an emulator binary');
|
|
543
773
|
}
|
|
774
|
+
clearQuarantineAttribute(installDir);
|
|
775
|
+
codesignAdHoc(installDir);
|
|
544
776
|
|
|
545
|
-
fs.rmSync(path.join(SDK_ROOT, 'emulator'), { recursive: true, force: true });
|
|
546
|
-
fs.mkdirSync(path.join(SDK_ROOT, 'emulator'), { recursive: true });
|
|
547
|
-
fs.cpSync(extractedRoot, path.join(SDK_ROOT, 'emulator'), { recursive: true });
|
|
548
777
|
fs.rmSync(zipPath, { force: true });
|
|
549
|
-
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
550
778
|
}
|
|
551
779
|
|
|
552
780
|
async function installPlatformToolsArchive(metadata) {
|
|
553
781
|
const url = parseLatestPlatformToolsUrl(metadata);
|
|
554
782
|
const zipPath = path.join(TMP_DIR, path.basename(url));
|
|
555
|
-
const
|
|
783
|
+
const installDir = path.join(activeAndroidSdkRoot(), 'platform-tools');
|
|
556
784
|
|
|
557
|
-
fs.mkdirSync(extractDir, { recursive: true });
|
|
558
785
|
await downloadFile(url, zipPath);
|
|
559
|
-
|
|
786
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
787
|
+
extractZip(zipPath, activeAndroidSdkRoot());
|
|
560
788
|
|
|
561
|
-
const extractedRoot = findDirectoryContainingFiles(
|
|
789
|
+
const extractedRoot = findDirectoryContainingFiles(installDir, [
|
|
562
790
|
process.platform === 'win32' ? 'adb.exe' : 'adb',
|
|
563
791
|
]);
|
|
564
792
|
if (!extractedRoot) {
|
|
565
793
|
throw new Error('Downloaded platform-tools archive did not contain adb');
|
|
566
794
|
}
|
|
567
795
|
|
|
568
|
-
fs.rmSync(path.join(SDK_ROOT, 'platform-tools'), { recursive: true, force: true });
|
|
569
|
-
fs.mkdirSync(path.join(SDK_ROOT, 'platform-tools'), { recursive: true });
|
|
570
|
-
fs.cpSync(extractedRoot, path.join(SDK_ROOT, 'platform-tools'), { recursive: true });
|
|
571
796
|
fs.rmSync(zipPath, { force: true });
|
|
572
|
-
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function shouldInstallPlatformToolsArchive() {
|
|
800
|
+
if (SHARED_ANDROID_SDK_ROOT) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
if (hostAdbBinary()) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
const distroAdb = systemAdbBinary();
|
|
807
|
+
if (process.platform === 'linux' && process.arch === 'arm64' && distroAdb && isExecutable(distroAdb)) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
573
811
|
}
|
|
574
812
|
|
|
575
813
|
async function installSystemImageArchive(metadata, packageName) {
|
|
576
814
|
const url = parseLatestSystemImageUrl(metadata, packageName);
|
|
577
815
|
const zipPath = path.join(TMP_DIR, path.basename(url));
|
|
578
|
-
const
|
|
579
|
-
const
|
|
816
|
+
const targetRoot = path.join(activeAndroidSdkRoot(), ...String(packageName).split(';').filter(Boolean));
|
|
817
|
+
const extractDir = fs.mkdtempSync(path.join(TMP_DIR, 'system-image-'));
|
|
580
818
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
819
|
+
try {
|
|
820
|
+
await downloadFile(url, zipPath);
|
|
821
|
+
extractZip(zipPath, extractDir);
|
|
584
822
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
823
|
+
const extractedRoot = findDirectoryContainingFiles(extractDir, ['userdata.img']) ||
|
|
824
|
+
findDirectoryContainingFiles(extractDir, ['system.img']) ||
|
|
825
|
+
findDirectoryContainingFiles(extractDir, ['package.xml']);
|
|
826
|
+
if (!extractedRoot) {
|
|
827
|
+
throw new Error(`Downloaded Android system image archive for ${packageName} did not contain the expected files`);
|
|
828
|
+
}
|
|
591
829
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
830
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
831
|
+
fs.mkdirSync(path.dirname(targetRoot), { recursive: true });
|
|
832
|
+
fs.cpSync(extractedRoot, targetRoot, { recursive: true, force: true });
|
|
833
|
+
} finally {
|
|
834
|
+
fs.rmSync(zipPath, { force: true });
|
|
835
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
836
|
+
}
|
|
597
837
|
}
|
|
598
838
|
|
|
599
839
|
function systemImageTagScore(tag) {
|
|
600
840
|
const value = String(tag || '').toLowerCase();
|
|
601
|
-
if (value.startsWith('google_apis_playstore')) return
|
|
602
|
-
if (value.startsWith('google_apis')) return
|
|
603
|
-
if (value === '
|
|
604
|
-
if (value === '
|
|
605
|
-
if (value === '
|
|
841
|
+
if (value.startsWith('google_apis_playstore')) return 60;
|
|
842
|
+
if (value.startsWith('google_apis')) return 50;
|
|
843
|
+
if (value === 'default') return 40;
|
|
844
|
+
if (value === 'google_atd') return 20;
|
|
845
|
+
if (value === 'aosp_atd') return 10;
|
|
606
846
|
return 0;
|
|
607
847
|
}
|
|
608
848
|
|
|
@@ -639,7 +879,7 @@ function parseSystemImages(listOutput) {
|
|
|
639
879
|
}
|
|
640
880
|
|
|
641
881
|
function parseInstalledSystemImages() {
|
|
642
|
-
const root = path.join(
|
|
882
|
+
const root = path.join(activeAndroidSdkRoot(), 'system-images');
|
|
643
883
|
if (!fs.existsSync(root)) {
|
|
644
884
|
return [];
|
|
645
885
|
}
|
|
@@ -847,7 +1087,39 @@ class AndroidController {
|
|
|
847
1087
|
this.ownerKey = normalizeOwnerKey(this.userId);
|
|
848
1088
|
this.stateFile = resolveStateFile(this.scopeKey);
|
|
849
1089
|
this.cli = new CLIExecutor();
|
|
850
|
-
|
|
1090
|
+
const state = this.#readState();
|
|
1091
|
+
const desiredArchTag = sanitizeScopeKey(systemImageArch());
|
|
1092
|
+
const stateMatchesArch = state?.systemImageArch === systemImageArch() && String(state?.avdName || '').includes(desiredArchTag);
|
|
1093
|
+
this.previousAvdName = String(state?.avdName || '').trim() || null;
|
|
1094
|
+
this.avdName = stateMatchesArch
|
|
1095
|
+
? state.avdName
|
|
1096
|
+
: `neoagent-${this.scopeKey}-${desiredArchTag}`;
|
|
1097
|
+
if (this.previousAvdName !== this.avdName) {
|
|
1098
|
+
this.#appendState({
|
|
1099
|
+
avdName: this.avdName,
|
|
1100
|
+
serial: null,
|
|
1101
|
+
emulatorPid: null,
|
|
1102
|
+
starting: false,
|
|
1103
|
+
startupPhase: null,
|
|
1104
|
+
lastStartError: null,
|
|
1105
|
+
lastLogLine: null,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
if (String(state?.systemImageArch || '').trim() && state.systemImageArch !== systemImageArch()) {
|
|
1109
|
+
this.#appendState({
|
|
1110
|
+
serial: null,
|
|
1111
|
+
emulatorPid: null,
|
|
1112
|
+
bootstrapped: false,
|
|
1113
|
+
systemImage: null,
|
|
1114
|
+
apiLevel: null,
|
|
1115
|
+
systemImageArch: null,
|
|
1116
|
+
avdSystemImage: null,
|
|
1117
|
+
starting: false,
|
|
1118
|
+
startupPhase: null,
|
|
1119
|
+
lastStartError: null,
|
|
1120
|
+
lastLogLine: null,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
851
1123
|
this.bootstrapPromise = null;
|
|
852
1124
|
this.startPromise = null;
|
|
853
1125
|
this.#registerProcessCleanup();
|
|
@@ -973,6 +1245,39 @@ class AndroidController {
|
|
|
973
1245
|
process.once('unhandledRejection', cleanup);
|
|
974
1246
|
}
|
|
975
1247
|
|
|
1248
|
+
async #terminateStaleEmulatorProcesses(names = [this.avdName]) {
|
|
1249
|
+
const avdNames = [...new Set((Array.isArray(names) ? names : [names]).map((name) => String(name || '').trim()).filter(Boolean))];
|
|
1250
|
+
const pids = [];
|
|
1251
|
+
for (const avdName of avdNames) {
|
|
1252
|
+
const probe = spawnSync('pgrep', ['-f', `@${avdName}`], { encoding: 'utf8' });
|
|
1253
|
+
if (probe.status !== 0) {
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const found = String(probe.stdout || '')
|
|
1257
|
+
.split(/\s+/)
|
|
1258
|
+
.map((pid) => Number(pid))
|
|
1259
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
1260
|
+
pids.push(...found);
|
|
1261
|
+
}
|
|
1262
|
+
const uniquePids = [...new Set(pids)];
|
|
1263
|
+
if (uniquePids.length === 0) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
for (const pid of uniquePids) {
|
|
1268
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
await sleep(1500);
|
|
1272
|
+
|
|
1273
|
+
for (const pid of uniquePids) {
|
|
1274
|
+
try {
|
|
1275
|
+
process.kill(pid, 0);
|
|
1276
|
+
process.kill(pid, 'SIGKILL');
|
|
1277
|
+
} catch {}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
976
1281
|
#stopTrackedEmulatorSync() {
|
|
977
1282
|
const state = this.#readState();
|
|
978
1283
|
const serial = state.serial;
|
|
@@ -1018,10 +1323,80 @@ class AndroidController {
|
|
|
1018
1323
|
}
|
|
1019
1324
|
|
|
1020
1325
|
async ensureBootstrapped() {
|
|
1326
|
+
const desiredArch = systemImageArch();
|
|
1327
|
+
const state = this.#readState();
|
|
1328
|
+
const installedImages = parseInstalledSystemImages();
|
|
1329
|
+
const preferredInstalled =
|
|
1330
|
+
chooseConfiguredSystemImage(installedImages) ||
|
|
1331
|
+
chooseLatestSystemImage(installedImages, [desiredArch]) ||
|
|
1332
|
+
chooseLatestSystemImage(installedImages);
|
|
1333
|
+
const systemImageMetadata = await fetchText('https://dl.google.com/android/repository/sys-img/android/sys-img2-1.xml');
|
|
1334
|
+
const available = parseRepositorySystemImages(systemImageMetadata);
|
|
1335
|
+
const preferredAvailable =
|
|
1336
|
+
chooseConfiguredSystemImage(available) ||
|
|
1337
|
+
chooseLatestSystemImage(available, [desiredArch]) ||
|
|
1338
|
+
chooseLatestSystemImage(available);
|
|
1339
|
+
const stateApiLevel = Number(state.apiLevel || 0) || 0;
|
|
1340
|
+
|
|
1341
|
+
if (!shouldForceSdkRefresh() && sharedAndroidSdkReady() && preferredInstalled && preferredAvailable) {
|
|
1342
|
+
const stateAligned =
|
|
1343
|
+
preferredInstalled.packageName === state.systemImage &&
|
|
1344
|
+
preferredInstalled.apiLevel === stateApiLevel &&
|
|
1345
|
+
preferredInstalled.arch === state.systemImageArch &&
|
|
1346
|
+
state.avdName === this.avdName;
|
|
1347
|
+
|
|
1348
|
+
if (stateAligned && preferredInstalled.packageName === preferredAvailable.packageName) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (preferredInstalled.packageName !== preferredAvailable.packageName) {
|
|
1353
|
+
const changeSummary = describeAutoFixChanges(
|
|
1354
|
+
{
|
|
1355
|
+
avdName: state.avdName || null,
|
|
1356
|
+
systemImage: state.systemImage || null,
|
|
1357
|
+
apiLevel: stateApiLevel || null,
|
|
1358
|
+
systemImageArch: state.systemImageArch || null,
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
avdName: this.avdName,
|
|
1362
|
+
systemImage: preferredAvailable.packageName,
|
|
1363
|
+
apiLevel: preferredAvailable.apiLevel,
|
|
1364
|
+
systemImageArch: preferredAvailable.arch,
|
|
1365
|
+
},
|
|
1366
|
+
['avdName', 'systemImage', 'apiLevel', 'systemImageArch']
|
|
1367
|
+
);
|
|
1368
|
+
if (changeSummary) {
|
|
1369
|
+
console.log(`[Android] Auto-fixed host SDK state (${changeSummary})`);
|
|
1370
|
+
}
|
|
1371
|
+
await installSystemImageArchive(systemImageMetadata, preferredAvailable.packageName);
|
|
1372
|
+
this.#appendState({
|
|
1373
|
+
bootstrapped: true,
|
|
1374
|
+
avdName: this.avdName,
|
|
1375
|
+
serial: null,
|
|
1376
|
+
emulatorPid: null,
|
|
1377
|
+
systemImage: preferredAvailable.packageName,
|
|
1378
|
+
apiLevel: preferredAvailable.apiLevel,
|
|
1379
|
+
systemImageArch: preferredAvailable.arch,
|
|
1380
|
+
});
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
this.#appendState({
|
|
1385
|
+
bootstrapped: true,
|
|
1386
|
+
avdName: this.avdName,
|
|
1387
|
+
serial: null,
|
|
1388
|
+
emulatorPid: null,
|
|
1389
|
+
systemImage: preferredInstalled.packageName,
|
|
1390
|
+
apiLevel: preferredInstalled.apiLevel,
|
|
1391
|
+
systemImageArch: preferredInstalled.arch,
|
|
1392
|
+
});
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1021
1396
|
const binariesReady =
|
|
1022
1397
|
isExecutable(adbBinary()) &&
|
|
1023
|
-
isExecutable(emulatorBinary())
|
|
1024
|
-
|
|
1398
|
+
isExecutable(emulatorBinary()) &&
|
|
1399
|
+
installedEmulatorMatchesHost();
|
|
1025
1400
|
if (!binariesReady) {
|
|
1026
1401
|
if (this.bootstrapPromise) {
|
|
1027
1402
|
await this.bootstrapPromise;
|
|
@@ -1035,70 +1410,74 @@ class AndroidController {
|
|
|
1035
1410
|
}
|
|
1036
1411
|
}
|
|
1037
1412
|
|
|
1038
|
-
const
|
|
1413
|
+
const runtimeNeedsRefresh =
|
|
1414
|
+
state.systemImageArch !== desiredArch ||
|
|
1415
|
+
!installedEmulatorMatchesHost();
|
|
1039
1416
|
if (!shouldForceSdkRefresh()) {
|
|
1040
|
-
|
|
1041
|
-
if (installedImages.length > 0) {
|
|
1042
|
-
const preferredInstalled =
|
|
1043
|
-
chooseConfiguredSystemImage(installedImages) ||
|
|
1044
|
-
chooseLatestSystemImage(installedImages);
|
|
1045
|
-
if (!preferredInstalled) {
|
|
1046
|
-
throw new Error(formatSystemImageError(installedImages));
|
|
1047
|
-
}
|
|
1048
|
-
const stateApiLevel = Number(state.apiLevel || 0) || 0;
|
|
1417
|
+
if (!runtimeNeedsRefresh && preferredInstalled) {
|
|
1049
1418
|
const stateNeedsRefresh =
|
|
1050
1419
|
preferredInstalled.packageName !== state.systemImage ||
|
|
1051
1420
|
preferredInstalled.apiLevel !== stateApiLevel ||
|
|
1052
|
-
preferredInstalled.arch !== state.systemImageArch
|
|
1421
|
+
preferredInstalled.arch !== state.systemImageArch ||
|
|
1422
|
+
state.avdName !== this.avdName;
|
|
1053
1423
|
if (stateNeedsRefresh) {
|
|
1054
1424
|
const changeSummary = describeAutoFixChanges(
|
|
1055
1425
|
{
|
|
1426
|
+
avdName: state.avdName || null,
|
|
1056
1427
|
systemImage: state.systemImage || null,
|
|
1057
1428
|
apiLevel: stateApiLevel || null,
|
|
1058
1429
|
systemImageArch: state.systemImageArch || null,
|
|
1059
1430
|
},
|
|
1060
1431
|
{
|
|
1432
|
+
avdName: this.avdName,
|
|
1061
1433
|
systemImage: preferredInstalled.packageName,
|
|
1062
1434
|
apiLevel: preferredInstalled.apiLevel,
|
|
1063
1435
|
systemImageArch: preferredInstalled.arch,
|
|
1064
1436
|
},
|
|
1065
|
-
['systemImage', 'apiLevel', 'systemImageArch']
|
|
1437
|
+
['avdName', 'systemImage', 'apiLevel', 'systemImageArch']
|
|
1066
1438
|
);
|
|
1067
1439
|
if (changeSummary) {
|
|
1068
1440
|
console.log(`[Android] Auto-fixed preferred system image (${changeSummary})`);
|
|
1069
1441
|
}
|
|
1070
1442
|
this.#appendState({
|
|
1071
1443
|
bootstrapped: true,
|
|
1444
|
+
avdName: this.avdName,
|
|
1445
|
+
serial: null,
|
|
1446
|
+
emulatorPid: null,
|
|
1072
1447
|
systemImage: preferredInstalled.packageName,
|
|
1073
1448
|
apiLevel: preferredInstalled.apiLevel,
|
|
1074
1449
|
systemImageArch: preferredInstalled.arch,
|
|
1075
1450
|
});
|
|
1076
1451
|
}
|
|
1077
1452
|
return;
|
|
1078
|
-
}
|
|
1453
|
+
}
|
|
1454
|
+
if (!runtimeNeedsRefresh && state.bootstrapped === true && state.systemImage) {
|
|
1079
1455
|
return;
|
|
1080
1456
|
}
|
|
1081
1457
|
}
|
|
1082
1458
|
|
|
1083
1459
|
this.#appendState({ bootstrapped: true });
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const metadata = await fetchText('https://dl.google.com/android/repository/repository2-1.xml');
|
|
1089
|
-
await installPlatformToolsArchive(metadata);
|
|
1460
|
+
const metadata = await fetchEmulatorMetadata();
|
|
1461
|
+
if (shouldInstallPlatformToolsArchive()) {
|
|
1462
|
+
await installPlatformToolsArchive(metadata);
|
|
1463
|
+
}
|
|
1090
1464
|
await installEmulatorArchive(metadata);
|
|
1091
|
-
await installSystemImageArchive(systemImageMetadata,
|
|
1465
|
+
await installSystemImageArchive(systemImageMetadata, preferredAvailable?.packageName || preferredInstalled?.packageName);
|
|
1466
|
+
const selectedImage = preferredAvailable || preferredInstalled;
|
|
1467
|
+
if (!selectedImage) {
|
|
1468
|
+
throw new Error(formatSystemImageError(available));
|
|
1469
|
+
}
|
|
1092
1470
|
this.#appendState({
|
|
1093
1471
|
bootstrapped: true,
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1472
|
+
avdName: this.avdName,
|
|
1473
|
+
systemImage: selectedImage.packageName,
|
|
1474
|
+
apiLevel: selectedImage.apiLevel,
|
|
1475
|
+
systemImageArch: selectedImage.arch,
|
|
1097
1476
|
});
|
|
1098
1477
|
}
|
|
1099
1478
|
|
|
1100
1479
|
async #bootstrapRuntime() {
|
|
1101
|
-
const metadata = await
|
|
1480
|
+
const metadata = await fetchEmulatorMetadata();
|
|
1102
1481
|
const url = parseLatestCmdlineToolsUrl(metadata);
|
|
1103
1482
|
const zipPath = path.join(TMP_DIR, path.basename(url));
|
|
1104
1483
|
const extractDir = path.join(TMP_DIR, `cmdline-tools-${Date.now()}`);
|
|
@@ -1120,7 +1499,9 @@ class AndroidController {
|
|
|
1120
1499
|
fs.cpSync(extractedRoot, CMDLINE_LATEST, { recursive: true });
|
|
1121
1500
|
fs.rmSync(zipPath, { force: true });
|
|
1122
1501
|
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
1123
|
-
|
|
1502
|
+
if (shouldInstallPlatformToolsArchive()) {
|
|
1503
|
+
await installPlatformToolsArchive(metadata);
|
|
1504
|
+
}
|
|
1124
1505
|
await installEmulatorArchive(metadata);
|
|
1125
1506
|
|
|
1126
1507
|
const systemImageMetadata = await fetchText('https://dl.google.com/android/repository/sys-img/android/sys-img2-1.xml');
|
|
@@ -1177,6 +1558,14 @@ class AndroidController {
|
|
|
1177
1558
|
const expectedAbi = systemImagePackageToAbi(pkg);
|
|
1178
1559
|
const currentCpuArch = readIniValue(config, 'hw.cpu.arch');
|
|
1179
1560
|
const expectedCpuArch = systemImagePackageToCpuArch(pkg);
|
|
1561
|
+
const currentDataPartitionSize = readIniValue(config, 'disk.dataPartition.size');
|
|
1562
|
+
const expectedDataPartitionSize = String(DEFAULT_DATA_PARTITION_BYTES);
|
|
1563
|
+
const currentSdcardSize = readIniValue(config, 'sdcard.size');
|
|
1564
|
+
const expectedSdcardSize = String(DEFAULT_SDCARD_SIZE_BYTES);
|
|
1565
|
+
const currentRamSize = readIniValue(config, 'hw.ramSize');
|
|
1566
|
+
const expectedRamSize = String(DEFAULT_RAM_SIZE_MB);
|
|
1567
|
+
const currentGpuMode = readIniValue(config, 'hw.gpu.mode');
|
|
1568
|
+
const expectedGpuMode = emulatorGpuMode();
|
|
1180
1569
|
if (expectedImageDir && currentImageDir && currentImageDir !== expectedImageDir) {
|
|
1181
1570
|
avdNeedsRecreate = true;
|
|
1182
1571
|
avdRecreateReasons.push(`image.sysdir.1: ${currentImageDir} -> ${expectedImageDir}`);
|
|
@@ -1189,6 +1578,22 @@ class AndroidController {
|
|
|
1189
1578
|
avdNeedsRecreate = true;
|
|
1190
1579
|
avdRecreateReasons.push(`hw.cpu.arch: ${currentCpuArch} -> ${expectedCpuArch}`);
|
|
1191
1580
|
}
|
|
1581
|
+
if (currentDataPartitionSize && currentDataPartitionSize !== expectedDataPartitionSize) {
|
|
1582
|
+
avdNeedsRecreate = true;
|
|
1583
|
+
avdRecreateReasons.push(`disk.dataPartition.size: ${currentDataPartitionSize} -> ${expectedDataPartitionSize}`);
|
|
1584
|
+
}
|
|
1585
|
+
if (currentSdcardSize && currentSdcardSize !== expectedSdcardSize) {
|
|
1586
|
+
avdNeedsRecreate = true;
|
|
1587
|
+
avdRecreateReasons.push(`sdcard.size: ${currentSdcardSize} -> ${expectedSdcardSize}`);
|
|
1588
|
+
}
|
|
1589
|
+
if (currentRamSize && currentRamSize !== expectedRamSize) {
|
|
1590
|
+
avdNeedsRecreate = true;
|
|
1591
|
+
avdRecreateReasons.push(`hw.ramSize: ${currentRamSize} -> ${expectedRamSize}`);
|
|
1592
|
+
}
|
|
1593
|
+
if (currentGpuMode && currentGpuMode !== expectedGpuMode) {
|
|
1594
|
+
avdNeedsRecreate = true;
|
|
1595
|
+
avdRecreateReasons.push(`hw.gpu.mode: ${currentGpuMode} -> ${expectedGpuMode}`);
|
|
1596
|
+
}
|
|
1192
1597
|
} catch {}
|
|
1193
1598
|
}
|
|
1194
1599
|
|
|
@@ -1199,7 +1604,10 @@ class AndroidController {
|
|
|
1199
1604
|
await this.stopEmulator().catch(() => {});
|
|
1200
1605
|
fs.rmSync(avdDir, { recursive: true, force: true });
|
|
1201
1606
|
fs.rmSync(path.join(AVD_HOME, `${this.avdName}.ini`), { force: true });
|
|
1607
|
+
fs.rmSync(path.join(avdDir, 'userdata-qemu.img'), { force: true });
|
|
1608
|
+
fs.rmSync(emulatorSnapshotStoragePath(this.avdName), { force: true });
|
|
1202
1609
|
} else if (avdExists) {
|
|
1610
|
+
ensureSparseFile(path.join(avdDir, 'userdata-qemu.img'), DEFAULT_DATA_PARTITION_BYTES);
|
|
1203
1611
|
return;
|
|
1204
1612
|
}
|
|
1205
1613
|
|
|
@@ -1224,6 +1632,7 @@ class AndroidController {
|
|
|
1224
1632
|
}
|
|
1225
1633
|
|
|
1226
1634
|
fs.mkdirSync(avdDir, { recursive: true });
|
|
1635
|
+
ensureSparseFile(path.join(avdDir, 'userdata-qemu.img'), DEFAULT_DATA_PARTITION_BYTES);
|
|
1227
1636
|
fs.writeFileSync(
|
|
1228
1637
|
path.join(AVD_HOME, `${this.avdName}.ini`),
|
|
1229
1638
|
[
|
|
@@ -1247,19 +1656,19 @@ class AndroidController {
|
|
|
1247
1656
|
'hw.dPad=no',
|
|
1248
1657
|
'hw.gps=yes',
|
|
1249
1658
|
'hw.gpu.enabled=yes',
|
|
1250
|
-
|
|
1659
|
+
`hw.gpu.mode=${emulatorGpuMode()}`,
|
|
1251
1660
|
'hw.initialOrientation=Portrait',
|
|
1252
1661
|
'hw.keyboard=yes',
|
|
1253
1662
|
'hw.lcd.density=440',
|
|
1254
1663
|
'hw.lcd.height=1920',
|
|
1255
1664
|
'hw.lcd.width=1080',
|
|
1256
1665
|
'hw.mainKeys=no',
|
|
1257
|
-
`hw.ramSize=${
|
|
1666
|
+
`hw.ramSize=${DEFAULT_RAM_SIZE_MB}`,
|
|
1258
1667
|
'hw.sensors.orientation=yes',
|
|
1259
1668
|
'hw.sensors.proximity=yes',
|
|
1260
1669
|
'hw.trackBall=no',
|
|
1261
|
-
`disk.dataPartition.size=${
|
|
1262
|
-
`sdcard.size=${
|
|
1670
|
+
`disk.dataPartition.size=${DEFAULT_DATA_PARTITION_BYTES}`,
|
|
1671
|
+
`sdcard.size=${DEFAULT_SDCARD_SIZE_BYTES}`,
|
|
1263
1672
|
'runtime.network.latency=none',
|
|
1264
1673
|
'runtime.network.speed=full',
|
|
1265
1674
|
'vm.heapSize=256',
|
|
@@ -1269,11 +1678,12 @@ class AndroidController {
|
|
|
1269
1678
|
];
|
|
1270
1679
|
fs.writeFileSync(path.join(avdDir, 'config.ini'), configLines.join('\n'));
|
|
1271
1680
|
|
|
1272
|
-
const systemImageRoot = path.join(
|
|
1681
|
+
const systemImageRoot = path.join(activeAndroidSdkRoot(), ...String(packageName).split(';').filter(Boolean));
|
|
1273
1682
|
const userdataImage = path.join(systemImageRoot, 'userdata.img');
|
|
1274
1683
|
if (fs.existsSync(userdataImage)) {
|
|
1275
1684
|
fs.copyFileSync(userdataImage, path.join(avdDir, 'userdata.img'));
|
|
1276
1685
|
}
|
|
1686
|
+
fs.rmSync(emulatorSnapshotStoragePath(this.avdName), { force: true });
|
|
1277
1687
|
}
|
|
1278
1688
|
|
|
1279
1689
|
#normalizeAvdConfig() {
|
|
@@ -1281,9 +1691,10 @@ class AndroidController {
|
|
|
1281
1691
|
if (!fs.existsSync(configPath)) return;
|
|
1282
1692
|
|
|
1283
1693
|
let content = fs.readFileSync(configPath, 'utf8');
|
|
1284
|
-
content = updateIniValue(content, 'disk.dataPartition.size',
|
|
1285
|
-
content = updateIniValue(content, 'sdcard.size',
|
|
1286
|
-
content = updateIniValue(content, 'hw.ramSize',
|
|
1694
|
+
content = updateIniValue(content, 'disk.dataPartition.size', DEFAULT_DATA_PARTITION_BYTES);
|
|
1695
|
+
content = updateIniValue(content, 'sdcard.size', DEFAULT_SDCARD_SIZE_BYTES);
|
|
1696
|
+
content = updateIniValue(content, 'hw.ramSize', DEFAULT_RAM_SIZE_MB);
|
|
1697
|
+
content = updateIniValue(content, 'hw.gpu.mode', emulatorGpuMode());
|
|
1287
1698
|
fs.writeFileSync(configPath, content);
|
|
1288
1699
|
}
|
|
1289
1700
|
|
|
@@ -1348,6 +1759,7 @@ class AndroidController {
|
|
|
1348
1759
|
startRequestedAt: this.#readState().startRequestedAt || new Date().toISOString(),
|
|
1349
1760
|
});
|
|
1350
1761
|
console.log('[Android] Preparing emulator start');
|
|
1762
|
+
await this.#terminateStaleEmulatorProcesses([this.avdName, this.previousAvdName]).catch(() => {});
|
|
1351
1763
|
await this.ensureAvd();
|
|
1352
1764
|
this.#appendState({
|
|
1353
1765
|
starting: true,
|
|
@@ -1377,18 +1789,21 @@ class AndroidController {
|
|
|
1377
1789
|
const args = [
|
|
1378
1790
|
`@${this.avdName}`,
|
|
1379
1791
|
'-no-boot-anim',
|
|
1792
|
+
...emulatorLaunchArgs(this.avdName),
|
|
1793
|
+
'-data',
|
|
1794
|
+
path.join(AVD_HOME, `${this.avdName}.avd`, 'userdata-qemu.img'),
|
|
1380
1795
|
'-gpu',
|
|
1381
|
-
|
|
1796
|
+
emulatorGpuMode(),
|
|
1797
|
+
'-accel',
|
|
1798
|
+
'auto',
|
|
1799
|
+
'-partition-size',
|
|
1800
|
+
String(DEFAULT_PARTITION_SIZE_MB),
|
|
1382
1801
|
'-netdelay',
|
|
1383
1802
|
'none',
|
|
1384
1803
|
'-netspeed',
|
|
1385
1804
|
'full',
|
|
1386
1805
|
];
|
|
1387
1806
|
|
|
1388
|
-
if (options.headless !== false) {
|
|
1389
|
-
args.push('-no-window', '-no-audio');
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
1807
|
const child = spawn(emulatorBinary(), args, {
|
|
1393
1808
|
detached: true,
|
|
1394
1809
|
stdio: ['ignore', out, out],
|
|
@@ -1405,39 +1820,20 @@ class AndroidController {
|
|
|
1405
1820
|
lastStartError: null,
|
|
1406
1821
|
lastLogLine: 'Android emulator process started. Waiting for boot completion...',
|
|
1407
1822
|
});
|
|
1408
|
-
|
|
1409
|
-
const processExit = new Promise((resolve) => {
|
|
1410
|
-
child.once('exit', (code, signal) => {
|
|
1411
|
-
resolve({ code, signal });
|
|
1412
|
-
});
|
|
1413
|
-
child.once('error', (error) => {
|
|
1414
|
-
resolve({ code: null, signal: null, error });
|
|
1415
|
-
});
|
|
1416
|
-
});
|
|
1417
|
-
|
|
1418
1823
|
child.unref();
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
this.waitForDevice({ timeoutMs: options.timeoutMs ||
|
|
1422
|
-
|
|
1423
|
-
exited: false,
|
|
1424
|
-
})),
|
|
1425
|
-
processExit.then((result) => ({
|
|
1426
|
-
exited: true,
|
|
1427
|
-
...result,
|
|
1428
|
-
})),
|
|
1429
|
-
]);
|
|
1430
|
-
|
|
1431
|
-
if (bootResult.exited) {
|
|
1824
|
+
let onlineSerial;
|
|
1825
|
+
try {
|
|
1826
|
+
onlineSerial = await this.waitForDevice({ timeoutMs: options.timeoutMs || 600000 });
|
|
1827
|
+
} catch (error) {
|
|
1432
1828
|
const recentLogLines = tailFile(logPath, 12);
|
|
1433
1829
|
const lastLine =
|
|
1434
|
-
bootResult.error?.message ||
|
|
1435
1830
|
recentLogLines[recentLogLines.length - 1] ||
|
|
1436
|
-
|
|
1831
|
+
error?.message ||
|
|
1832
|
+
String(error || 'Android emulator did not finish booting.');
|
|
1833
|
+
await this.stopEmulator().catch(() => {});
|
|
1834
|
+
this.markBootstrapFailure(lastLine);
|
|
1437
1835
|
throw new Error(lastLine);
|
|
1438
1836
|
}
|
|
1439
|
-
|
|
1440
|
-
const onlineSerial = bootResult.serial;
|
|
1441
1837
|
this.#appendState({
|
|
1442
1838
|
serial: onlineSerial,
|
|
1443
1839
|
emulatorPid: child.pid,
|
|
@@ -1507,8 +1903,8 @@ class AndroidController {
|
|
|
1507
1903
|
NEOAGENT_ANDROID_BOOTSTRAP_WORKER: '1',
|
|
1508
1904
|
NEOAGENT_ANDROID_BOOTSTRAP_USER_ID: this.userId || '',
|
|
1509
1905
|
NEOAGENT_ANDROID_BOOTSTRAP_SCOPE_KEY: this.scopeKey,
|
|
1510
|
-
NEOAGENT_ANDROID_BOOTSTRAP_HEADLESS: String(options.headless
|
|
1511
|
-
NEOAGENT_ANDROID_BOOTSTRAP_TIMEOUT_MS: String(options.timeoutMs ||
|
|
1906
|
+
NEOAGENT_ANDROID_BOOTSTRAP_HEADLESS: String(options.headless === true),
|
|
1907
|
+
NEOAGENT_ANDROID_BOOTSTRAP_TIMEOUT_MS: String(options.timeoutMs || 600000),
|
|
1512
1908
|
};
|
|
1513
1909
|
const child = spawn(process.execPath, [ANDROID_BOOTSTRAP_WORKER], {
|
|
1514
1910
|
detached: true,
|
|
@@ -1576,7 +1972,7 @@ class AndroidController {
|
|
|
1576
1972
|
|
|
1577
1973
|
async stopEmulator() {
|
|
1578
1974
|
const state = this.#readState();
|
|
1579
|
-
const serial = await this.getPrimarySerial();
|
|
1975
|
+
const serial = await this.getPrimarySerial({ ensureBootstrapped: false }).catch(() => null);
|
|
1580
1976
|
if (serial) {
|
|
1581
1977
|
this.#assertSerialAccess(serial, { claimIfUnowned: true });
|
|
1582
1978
|
await this.#runAllowFailure(`${quoteShell(adbBinary())} -s ${quoteShell(serial)} emu kill`, { timeout: 15000 });
|
|
@@ -1597,7 +1993,7 @@ class AndroidController {
|
|
|
1597
1993
|
|
|
1598
1994
|
const deadline = Date.now() + 30000;
|
|
1599
1995
|
while (Date.now() < deadline) {
|
|
1600
|
-
const devices = await this.listDevices().catch(() => []);
|
|
1996
|
+
const devices = await this.listDevices({ ensureBootstrapped: false }).catch(() => []);
|
|
1601
1997
|
const stillPresent = devices.some((device) => device.emulator && device.status === 'device');
|
|
1602
1998
|
let pidAlive = false;
|
|
1603
1999
|
if (state.emulatorPid) {
|
|
@@ -2115,7 +2511,7 @@ class AndroidController {
|
|
|
2115
2511
|
startupPhase: state.startupPhase || null,
|
|
2116
2512
|
startRequestedAt: state.startRequestedAt || null,
|
|
2117
2513
|
lastStartError: state.lastStartError || null,
|
|
2118
|
-
sdkRoot:
|
|
2514
|
+
sdkRoot: activeAndroidSdkRoot(),
|
|
2119
2515
|
avdHome: AVD_HOME,
|
|
2120
2516
|
avdName: this.avdName,
|
|
2121
2517
|
adbPath: adbBinary(),
|