neoagent 2.3.1-beta.87 → 2.3.1-beta.89
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/flutter_app/lib/features/notifications/notification_interceptor.dart +11 -45
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_app_shell.dart +23 -14
- package/flutter_app/lib/main_chat.dart +128 -22
- package/flutter_app/lib/main_controller.dart +18 -7
- package/flutter_app/lib/main_settings.dart +16 -14
- package/flutter_app/lib/main_shared.dart +21 -16
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
- package/flutter_app/pubspec.lock +24 -0
- package/flutter_app/pubspec.yaml +1 -0
- package/flutter_app/web/index.html +0 -1
- package/package.json +1 -1
- package/server/db/database.js +42 -4
- package/server/guest_agent.js +8 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +70 -44
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/index.html +0 -1
- package/server/public/main.dart.js +54424 -54174
- package/server/routes/browser.js +1 -1
- package/server/routes/social_video.js +4 -0
- package/server/services/ai/tools.js +1 -0
- package/server/services/android/android_bootstrap_worker.js +1 -0
- package/server/services/android/controller.js +456 -117
- package/server/services/browser/controller.js +24 -8
- package/server/services/runtime/backends/local-vm.js +37 -5
- package/server/services/social_video/service.js +110 -36
|
@@ -20,7 +20,6 @@ const TMP_DIR = path.join(ARTIFACTS_DIR, 'tmp');
|
|
|
20
20
|
const EMULATOR_HOME = path.join(ANDROID_ROOT, 'home');
|
|
21
21
|
const EMULATOR_ADVANCED_FEATURES_FILE = path.join(EMULATOR_HOME, 'advancedFeatures.ini');
|
|
22
22
|
const AVD_HOME = path.join(ANDROID_ROOT, 'avd');
|
|
23
|
-
const EMULATOR_READY_SNAPSHOT = 'neoagent-ready';
|
|
24
23
|
const STATE_DIR = path.join(ARTIFACTS_DIR, 'state');
|
|
25
24
|
const STATE_FILE = path.join(ARTIFACTS_DIR, 'state.json');
|
|
26
25
|
const OWNERSHIP_FILE = path.join(ARTIFACTS_DIR, 'device-ownership.json');
|
|
@@ -53,19 +52,17 @@ for (const dir of [ANDROID_ROOT, SDK_ROOT, EMULATOR_HOME, ARTIFACTS_DIR, SCREENS
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
function ensureEmulatorAdvancedFeaturesFile() {
|
|
56
|
-
const content = [
|
|
57
|
-
'Vulkan = off',
|
|
58
|
-
'GLDirectMem = off',
|
|
59
|
-
'',
|
|
60
|
-
].join('\n');
|
|
61
55
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
fs.mkdirSync(path.dirname(EMULATOR_ADVANCED_FEATURES_FILE), { recursive: true });
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
EMULATOR_ADVANCED_FEATURES_FILE,
|
|
59
|
+
[
|
|
60
|
+
'QuickbootFileBacked=off',
|
|
61
|
+
'QuickbootSupport=off',
|
|
62
|
+
'',
|
|
63
|
+
].join('\n'),
|
|
64
|
+
'utf8',
|
|
65
|
+
);
|
|
69
66
|
} catch {}
|
|
70
67
|
}
|
|
71
68
|
|
|
@@ -103,7 +100,7 @@ function readOwnershipUnlocked() {
|
|
|
103
100
|
if (!parsed || typeof parsed !== 'object') {
|
|
104
101
|
return {};
|
|
105
102
|
}
|
|
106
|
-
|
|
103
|
+
return parsed;
|
|
107
104
|
} catch {
|
|
108
105
|
return {};
|
|
109
106
|
}
|
|
@@ -185,6 +182,19 @@ function sleep(ms) {
|
|
|
185
182
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
183
|
}
|
|
187
184
|
|
|
185
|
+
function isProcessAlive(pid) {
|
|
186
|
+
const numericPid = Number(pid);
|
|
187
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
process.kill(numericPid, 0);
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
188
198
|
function tailFile(filePath, maxLines = 40) {
|
|
189
199
|
try {
|
|
190
200
|
const lines = fs.readFileSync(filePath, 'utf8')
|
|
@@ -197,6 +207,15 @@ function tailFile(filePath, maxLines = 40) {
|
|
|
197
207
|
}
|
|
198
208
|
}
|
|
199
209
|
|
|
210
|
+
function isLikelyPng(buffer) {
|
|
211
|
+
return Buffer.isBuffer(buffer)
|
|
212
|
+
&& buffer.length > 24
|
|
213
|
+
&& buffer[0] === 0x89
|
|
214
|
+
&& buffer[1] === 0x50
|
|
215
|
+
&& buffer[2] === 0x4e
|
|
216
|
+
&& buffer[3] === 0x47;
|
|
217
|
+
}
|
|
218
|
+
|
|
200
219
|
function commandExists(command) {
|
|
201
220
|
const probe = spawnSync('bash', ['-lc', `command -v "${command}"`], { encoding: 'utf8' });
|
|
202
221
|
return probe.status === 0;
|
|
@@ -265,25 +284,40 @@ function emulatorHostArch() {
|
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
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
287
|
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
277
|
-
return
|
|
288
|
+
return 'swiftshader_indirect';
|
|
278
289
|
}
|
|
290
|
+
if (process.platform === 'linux' && process.arch === 'arm64') {
|
|
291
|
+
return 'swiftshader_indirect';
|
|
292
|
+
}
|
|
293
|
+
return 'auto';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function emulatorLaunchArgs() {
|
|
279
297
|
return [
|
|
280
|
-
'-snapshot',
|
|
281
|
-
|
|
282
|
-
'-
|
|
283
|
-
|
|
298
|
+
'-no-snapshot',
|
|
299
|
+
'-no-snapshot-save',
|
|
300
|
+
'-no-window',
|
|
301
|
+
'-no-audio',
|
|
302
|
+
'-no-metrics',
|
|
303
|
+
'-skip-adb-auth',
|
|
304
|
+
'-crash-report-mode',
|
|
305
|
+
'disabled',
|
|
284
306
|
];
|
|
285
307
|
}
|
|
286
308
|
|
|
309
|
+
function isRecoverableEmulatorStartError(message) {
|
|
310
|
+
const value = String(message || '').toLowerCase();
|
|
311
|
+
return (
|
|
312
|
+
value.includes('failed to restore previous context') ||
|
|
313
|
+
value.includes('emulator exited before boot completed') ||
|
|
314
|
+
value.includes('android framework did not become ready') ||
|
|
315
|
+
value.includes('package manager service did not become ready') ||
|
|
316
|
+
value.includes('failed to process .ini file') ||
|
|
317
|
+
value.includes('error while loading state for instance')
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
287
321
|
function parseCsvEnv(value) {
|
|
288
322
|
return String(value || '')
|
|
289
323
|
.split(',')
|
|
@@ -345,6 +379,15 @@ function parseSystemImagePlatform(platformId) {
|
|
|
345
379
|
};
|
|
346
380
|
}
|
|
347
381
|
|
|
382
|
+
const released = String(platformId || '').match(/^android-(\d+(?:\.\d+)?)$/);
|
|
383
|
+
if (released) {
|
|
384
|
+
return {
|
|
385
|
+
platformId,
|
|
386
|
+
apiLevel: Math.floor(Number(released[1]) || 0),
|
|
387
|
+
stable: false,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
348
391
|
const preview = String(platformId || '').match(/^android-([A-Za-z][A-Za-z0-9_-]*)$/);
|
|
349
392
|
if (preview) {
|
|
350
393
|
return {
|
|
@@ -415,10 +458,10 @@ function hostAndroidSdkRoot() {
|
|
|
415
458
|
return null;
|
|
416
459
|
}
|
|
417
460
|
|
|
418
|
-
const SHARED_ANDROID_SDK_ROOT =
|
|
461
|
+
const SHARED_ANDROID_SDK_ROOT = null;
|
|
419
462
|
|
|
420
463
|
function activeAndroidSdkRoot() {
|
|
421
|
-
return
|
|
464
|
+
return SDK_ROOT;
|
|
422
465
|
}
|
|
423
466
|
|
|
424
467
|
function sharedAndroidSdkReady() {
|
|
@@ -436,20 +479,6 @@ function adbBinary() {
|
|
|
436
479
|
if (process.env.ANDROID_ADB_PATH) {
|
|
437
480
|
return process.env.ANDROID_ADB_PATH;
|
|
438
481
|
}
|
|
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
482
|
return path.join(SDK_ROOT, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
454
483
|
}
|
|
455
484
|
|
|
@@ -797,17 +826,8 @@ async function installPlatformToolsArchive(metadata) {
|
|
|
797
826
|
}
|
|
798
827
|
|
|
799
828
|
function shouldInstallPlatformToolsArchive() {
|
|
800
|
-
|
|
801
|
-
|
|
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;
|
|
829
|
+
const localAdb = path.join(SDK_ROOT, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
830
|
+
return !isExecutable(localAdb);
|
|
811
831
|
}
|
|
812
832
|
|
|
813
833
|
async function installSystemImageArchive(metadata, packageName) {
|
|
@@ -817,6 +837,8 @@ async function installSystemImageArchive(metadata, packageName) {
|
|
|
817
837
|
const extractDir = fs.mkdtempSync(path.join(TMP_DIR, 'system-image-'));
|
|
818
838
|
|
|
819
839
|
try {
|
|
840
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
841
|
+
fs.mkdirSync(path.dirname(targetRoot), { recursive: true });
|
|
820
842
|
await downloadFile(url, zipPath);
|
|
821
843
|
extractZip(zipPath, extractDir);
|
|
822
844
|
|
|
@@ -827,9 +849,15 @@ async function installSystemImageArchive(metadata, packageName) {
|
|
|
827
849
|
throw new Error(`Downloaded Android system image archive for ${packageName} did not contain the expected files`);
|
|
828
850
|
}
|
|
829
851
|
|
|
830
|
-
fs.rmSync(
|
|
831
|
-
|
|
832
|
-
|
|
852
|
+
fs.rmSync(zipPath, { force: true });
|
|
853
|
+
try {
|
|
854
|
+
fs.renameSync(extractedRoot, targetRoot);
|
|
855
|
+
} catch (renameErr) {
|
|
856
|
+
fs.cpSync(extractedRoot, targetRoot, { recursive: true, force: true });
|
|
857
|
+
if (renameErr) {
|
|
858
|
+
console.warn(`[Android] Falling back to copy for ${packageName}: ${renameErr.message}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
833
861
|
} finally {
|
|
834
862
|
fs.rmSync(zipPath, { force: true });
|
|
835
863
|
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
@@ -838,8 +866,8 @@ async function installSystemImageArchive(metadata, packageName) {
|
|
|
838
866
|
|
|
839
867
|
function systemImageTagScore(tag) {
|
|
840
868
|
const value = String(tag || '').toLowerCase();
|
|
841
|
-
if (value.startsWith('google_apis_playstore')) return
|
|
842
|
-
if (value.startsWith('google_apis')) return
|
|
869
|
+
if (value.startsWith('google_apis_playstore')) return 80;
|
|
870
|
+
if (value.startsWith('google_apis')) return 60;
|
|
843
871
|
if (value === 'default') return 40;
|
|
844
872
|
if (value === 'google_atd') return 20;
|
|
845
873
|
if (value === 'aosp_atd') return 10;
|
|
@@ -917,6 +945,25 @@ function parseInstalledSystemImages() {
|
|
|
917
945
|
return parseSystemImageCandidates(matches);
|
|
918
946
|
}
|
|
919
947
|
|
|
948
|
+
function chooseStableRuntimeSystemImage(candidates, currentPackage) {
|
|
949
|
+
const normalizedCurrent = String(currentPackage || '').trim();
|
|
950
|
+
const pool = Array.isArray(candidates)
|
|
951
|
+
? candidates.filter((item) => item && item.packageName && isValidInstalledSystemImage(item.packageName))
|
|
952
|
+
: [];
|
|
953
|
+
if (pool.length === 0) return null;
|
|
954
|
+
const ranked = rankSystemImagePool(pool);
|
|
955
|
+
const recommended = ranked.find((item) =>
|
|
956
|
+
item.arch === 'arm64-v8a'
|
|
957
|
+
&& item.stable
|
|
958
|
+
&& item.apiLevel >= 33
|
|
959
|
+
) || ranked[0];
|
|
960
|
+
if (!recommended) return null;
|
|
961
|
+
if (normalizedCurrent && recommended.packageName === normalizedCurrent) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
return recommended;
|
|
965
|
+
}
|
|
966
|
+
|
|
920
967
|
function rankSystemImagePool(pool) {
|
|
921
968
|
const preferredMatches = pool.filter((candidate) => candidate.tagScore > 0);
|
|
922
969
|
const rankedPool = preferredMatches.length > 0 ? preferredMatches : pool;
|
|
@@ -1034,6 +1081,18 @@ function systemImagePackageToRelativeDir(packageName) {
|
|
|
1034
1081
|
return `${parts.join('/')}/`;
|
|
1035
1082
|
}
|
|
1036
1083
|
|
|
1084
|
+
function isValidInstalledSystemImage(packageName) {
|
|
1085
|
+
const relativeDir = systemImagePackageToRelativeDir(packageName);
|
|
1086
|
+
if (!relativeDir) return false;
|
|
1087
|
+
const root = path.join(activeAndroidSdkRoot(), relativeDir);
|
|
1088
|
+
const required = [
|
|
1089
|
+
path.join(root, 'package.xml'),
|
|
1090
|
+
path.join(root, 'system.img'),
|
|
1091
|
+
path.join(root, 'userdata.img'),
|
|
1092
|
+
];
|
|
1093
|
+
return required.every((filePath) => fs.existsSync(filePath));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1037
1096
|
function systemImagePackageToAbi(packageName) {
|
|
1038
1097
|
const parts = String(packageName || '').split(';').filter(Boolean);
|
|
1039
1098
|
if (parts.length !== 4 || parts[0] !== 'system-images') {
|
|
@@ -1083,6 +1142,7 @@ class AndroidController {
|
|
|
1083
1142
|
this.userId = options?.userId != null ? String(options.userId) : null;
|
|
1084
1143
|
this.artifactStore = options?.artifactStore || null;
|
|
1085
1144
|
this.runtimeBackend = options?.runtimeBackend || 'host';
|
|
1145
|
+
this.manageProcessCleanup = options?.manageProcessCleanup !== false;
|
|
1086
1146
|
this.scopeKey = sanitizeScopeKey(this.userId ? `user-${this.userId}` : 'default');
|
|
1087
1147
|
this.ownerKey = normalizeOwnerKey(this.userId);
|
|
1088
1148
|
this.stateFile = resolveStateFile(this.scopeKey);
|
|
@@ -1122,7 +1182,9 @@ class AndroidController {
|
|
|
1122
1182
|
}
|
|
1123
1183
|
this.bootstrapPromise = null;
|
|
1124
1184
|
this.startPromise = null;
|
|
1125
|
-
this
|
|
1185
|
+
if (this.manageProcessCleanup) {
|
|
1186
|
+
this.#registerProcessCleanup();
|
|
1187
|
+
}
|
|
1126
1188
|
}
|
|
1127
1189
|
|
|
1128
1190
|
static cleanupRegistered = false;
|
|
@@ -1336,20 +1398,37 @@ class AndroidController {
|
|
|
1336
1398
|
chooseConfiguredSystemImage(available) ||
|
|
1337
1399
|
chooseLatestSystemImage(available, [desiredArch]) ||
|
|
1338
1400
|
chooseLatestSystemImage(available);
|
|
1401
|
+
const selectedImage = rankSystemImagePool([preferredInstalled, preferredAvailable].filter(Boolean))[0] || preferredInstalled || preferredAvailable;
|
|
1339
1402
|
const stateApiLevel = Number(state.apiLevel || 0) || 0;
|
|
1340
|
-
|
|
1341
|
-
|
|
1403
|
+
const legacyLinuxArm64Image =
|
|
1404
|
+
process.platform === 'linux'
|
|
1405
|
+
&& process.arch === 'arm64'
|
|
1406
|
+
&& /system-images;android-30;default;arm64-v8a/i.test(String(state.systemImage || ''));
|
|
1407
|
+
const migrationTargetImage =
|
|
1408
|
+
process.platform === 'linux' && process.arch === 'arm64'
|
|
1409
|
+
? (
|
|
1410
|
+
rankSystemImagePool(
|
|
1411
|
+
[preferredAvailable, preferredInstalled]
|
|
1412
|
+
.filter(Boolean)
|
|
1413
|
+
.filter((image) => image.arch === 'arm64-v8a' && image.stable && image.apiLevel >= 33)
|
|
1414
|
+
)[0] || null
|
|
1415
|
+
)
|
|
1416
|
+
: null;
|
|
1417
|
+
const effectiveSelectedImage = migrationTargetImage || selectedImage;
|
|
1418
|
+
const selectedImageInvalid = effectiveSelectedImage?.packageName && !isValidInstalledSystemImage(effectiveSelectedImage.packageName);
|
|
1419
|
+
|
|
1420
|
+
if (!shouldForceSdkRefresh() && !legacyLinuxArm64Image && !selectedImageInvalid && sharedAndroidSdkReady() && effectiveSelectedImage) {
|
|
1342
1421
|
const stateAligned =
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1422
|
+
effectiveSelectedImage.packageName === state.systemImage &&
|
|
1423
|
+
effectiveSelectedImage.apiLevel === stateApiLevel &&
|
|
1424
|
+
effectiveSelectedImage.arch === state.systemImageArch &&
|
|
1346
1425
|
state.avdName === this.avdName;
|
|
1347
1426
|
|
|
1348
|
-
if (stateAligned
|
|
1427
|
+
if (stateAligned) {
|
|
1349
1428
|
return;
|
|
1350
1429
|
}
|
|
1351
1430
|
|
|
1352
|
-
if (preferredInstalled
|
|
1431
|
+
if (effectiveSelectedImage === preferredInstalled && effectiveSelectedImage.packageName) {
|
|
1353
1432
|
const changeSummary = describeAutoFixChanges(
|
|
1354
1433
|
{
|
|
1355
1434
|
avdName: state.avdName || null,
|
|
@@ -1359,38 +1438,26 @@ class AndroidController {
|
|
|
1359
1438
|
},
|
|
1360
1439
|
{
|
|
1361
1440
|
avdName: this.avdName,
|
|
1362
|
-
systemImage:
|
|
1363
|
-
apiLevel:
|
|
1364
|
-
systemImageArch:
|
|
1441
|
+
systemImage: effectiveSelectedImage.packageName,
|
|
1442
|
+
apiLevel: effectiveSelectedImage.apiLevel,
|
|
1443
|
+
systemImageArch: effectiveSelectedImage.arch,
|
|
1365
1444
|
},
|
|
1366
1445
|
['avdName', 'systemImage', 'apiLevel', 'systemImageArch']
|
|
1367
1446
|
);
|
|
1368
1447
|
if (changeSummary) {
|
|
1369
1448
|
console.log(`[Android] Auto-fixed host SDK state (${changeSummary})`);
|
|
1370
1449
|
}
|
|
1371
|
-
await installSystemImageArchive(systemImageMetadata, preferredAvailable.packageName);
|
|
1372
1450
|
this.#appendState({
|
|
1373
1451
|
bootstrapped: true,
|
|
1374
1452
|
avdName: this.avdName,
|
|
1375
1453
|
serial: null,
|
|
1376
1454
|
emulatorPid: null,
|
|
1377
|
-
systemImage:
|
|
1378
|
-
apiLevel:
|
|
1379
|
-
systemImageArch:
|
|
1455
|
+
systemImage: effectiveSelectedImage.packageName,
|
|
1456
|
+
apiLevel: effectiveSelectedImage.apiLevel,
|
|
1457
|
+
systemImageArch: effectiveSelectedImage.arch,
|
|
1380
1458
|
});
|
|
1381
1459
|
return;
|
|
1382
1460
|
}
|
|
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
1461
|
}
|
|
1395
1462
|
|
|
1396
1463
|
const binariesReady =
|
|
@@ -1413,12 +1480,12 @@ class AndroidController {
|
|
|
1413
1480
|
const runtimeNeedsRefresh =
|
|
1414
1481
|
state.systemImageArch !== desiredArch ||
|
|
1415
1482
|
!installedEmulatorMatchesHost();
|
|
1416
|
-
if (!shouldForceSdkRefresh()) {
|
|
1417
|
-
if (!runtimeNeedsRefresh && preferredInstalled) {
|
|
1483
|
+
if (!shouldForceSdkRefresh() && !legacyLinuxArm64Image && !selectedImageInvalid) {
|
|
1484
|
+
if (!runtimeNeedsRefresh && effectiveSelectedImage && effectiveSelectedImage === preferredInstalled) {
|
|
1418
1485
|
const stateNeedsRefresh =
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1486
|
+
effectiveSelectedImage.packageName !== state.systemImage ||
|
|
1487
|
+
effectiveSelectedImage.apiLevel !== stateApiLevel ||
|
|
1488
|
+
effectiveSelectedImage.arch !== state.systemImageArch ||
|
|
1422
1489
|
state.avdName !== this.avdName;
|
|
1423
1490
|
if (stateNeedsRefresh) {
|
|
1424
1491
|
const changeSummary = describeAutoFixChanges(
|
|
@@ -1430,9 +1497,9 @@ class AndroidController {
|
|
|
1430
1497
|
},
|
|
1431
1498
|
{
|
|
1432
1499
|
avdName: this.avdName,
|
|
1433
|
-
systemImage:
|
|
1434
|
-
apiLevel:
|
|
1435
|
-
systemImageArch:
|
|
1500
|
+
systemImage: effectiveSelectedImage.packageName,
|
|
1501
|
+
apiLevel: effectiveSelectedImage.apiLevel,
|
|
1502
|
+
systemImageArch: effectiveSelectedImage.arch,
|
|
1436
1503
|
},
|
|
1437
1504
|
['avdName', 'systemImage', 'apiLevel', 'systemImageArch']
|
|
1438
1505
|
);
|
|
@@ -1444,14 +1511,20 @@ class AndroidController {
|
|
|
1444
1511
|
avdName: this.avdName,
|
|
1445
1512
|
serial: null,
|
|
1446
1513
|
emulatorPid: null,
|
|
1447
|
-
systemImage:
|
|
1448
|
-
apiLevel:
|
|
1449
|
-
systemImageArch:
|
|
1514
|
+
systemImage: effectiveSelectedImage.packageName,
|
|
1515
|
+
apiLevel: effectiveSelectedImage.apiLevel,
|
|
1516
|
+
systemImageArch: effectiveSelectedImage.arch,
|
|
1450
1517
|
});
|
|
1451
1518
|
}
|
|
1452
1519
|
return;
|
|
1453
1520
|
}
|
|
1454
|
-
if (
|
|
1521
|
+
if (
|
|
1522
|
+
!runtimeNeedsRefresh &&
|
|
1523
|
+
state.bootstrapped === true &&
|
|
1524
|
+
state.systemImage &&
|
|
1525
|
+
effectiveSelectedImage &&
|
|
1526
|
+
effectiveSelectedImage.packageName === state.systemImage
|
|
1527
|
+
) {
|
|
1455
1528
|
return;
|
|
1456
1529
|
}
|
|
1457
1530
|
}
|
|
@@ -1462,17 +1535,19 @@ class AndroidController {
|
|
|
1462
1535
|
await installPlatformToolsArchive(metadata);
|
|
1463
1536
|
}
|
|
1464
1537
|
await installEmulatorArchive(metadata);
|
|
1465
|
-
|
|
1466
|
-
const selectedImage = preferredAvailable || preferredInstalled;
|
|
1467
|
-
if (!selectedImage) {
|
|
1538
|
+
if (!effectiveSelectedImage) {
|
|
1468
1539
|
throw new Error(formatSystemImageError(available));
|
|
1469
1540
|
}
|
|
1541
|
+
if (effectiveSelectedImage?.packageName) {
|
|
1542
|
+
await installSystemImageArchive(systemImageMetadata, effectiveSelectedImage.packageName);
|
|
1543
|
+
}
|
|
1470
1544
|
this.#appendState({
|
|
1471
1545
|
bootstrapped: true,
|
|
1472
1546
|
avdName: this.avdName,
|
|
1473
|
-
systemImage:
|
|
1474
|
-
apiLevel:
|
|
1475
|
-
systemImageArch:
|
|
1547
|
+
systemImage: effectiveSelectedImage.packageName,
|
|
1548
|
+
apiLevel: effectiveSelectedImage.apiLevel,
|
|
1549
|
+
systemImageArch: effectiveSelectedImage.arch,
|
|
1550
|
+
avdSystemImage: null,
|
|
1476
1551
|
});
|
|
1477
1552
|
}
|
|
1478
1553
|
|
|
@@ -1531,6 +1606,7 @@ class AndroidController {
|
|
|
1531
1606
|
startupPhase: 'Start failed',
|
|
1532
1607
|
lastStartError: detailedMessage,
|
|
1533
1608
|
lastLogLine: detailedMessage,
|
|
1609
|
+
bootstrapWorkerPid: null,
|
|
1534
1610
|
});
|
|
1535
1611
|
return detailedMessage;
|
|
1536
1612
|
}
|
|
@@ -1539,8 +1615,22 @@ class AndroidController {
|
|
|
1539
1615
|
await this.ensureBootstrapped();
|
|
1540
1616
|
|
|
1541
1617
|
const state = this.#readState();
|
|
1542
|
-
|
|
1618
|
+
let pkg = state.systemImage;
|
|
1543
1619
|
if (!pkg) throw new Error('Android system image not installed');
|
|
1620
|
+
if (process.platform === 'linux' && process.arch === 'arm64') {
|
|
1621
|
+
const installedCandidates = parseInstalledSystemImages();
|
|
1622
|
+
const migratedImage = chooseStableRuntimeSystemImage(installedCandidates, pkg);
|
|
1623
|
+
if (migratedImage) {
|
|
1624
|
+
pkg = migratedImage.packageName;
|
|
1625
|
+
this.#appendState({
|
|
1626
|
+
systemImage: pkg,
|
|
1627
|
+
apiLevel: migratedImage.apiLevel,
|
|
1628
|
+
systemImageArch: migratedImage.arch,
|
|
1629
|
+
avdSystemImage: null,
|
|
1630
|
+
lastLogLine: `Migrated Android runtime image to ${pkg} for stability.`,
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1544
1634
|
const avdDir = path.join(AVD_HOME, `${this.avdName}.avd`);
|
|
1545
1635
|
const configPath = path.join(avdDir, 'config.ini');
|
|
1546
1636
|
const avdExists = fs.existsSync(configPath);
|
|
@@ -1566,6 +1656,8 @@ class AndroidController {
|
|
|
1566
1656
|
const expectedRamSize = String(DEFAULT_RAM_SIZE_MB);
|
|
1567
1657
|
const currentGpuMode = readIniValue(config, 'hw.gpu.mode');
|
|
1568
1658
|
const expectedGpuMode = emulatorGpuMode();
|
|
1659
|
+
const currentPlayStoreEnabled = readIniValue(config, 'PlayStore.enabled');
|
|
1660
|
+
const expectedPlayStoreEnabled = String(String(pkg || '').includes('playstore'));
|
|
1569
1661
|
if (expectedImageDir && currentImageDir && currentImageDir !== expectedImageDir) {
|
|
1570
1662
|
avdNeedsRecreate = true;
|
|
1571
1663
|
avdRecreateReasons.push(`image.sysdir.1: ${currentImageDir} -> ${expectedImageDir}`);
|
|
@@ -1594,6 +1686,10 @@ class AndroidController {
|
|
|
1594
1686
|
avdNeedsRecreate = true;
|
|
1595
1687
|
avdRecreateReasons.push(`hw.gpu.mode: ${currentGpuMode} -> ${expectedGpuMode}`);
|
|
1596
1688
|
}
|
|
1689
|
+
if (currentPlayStoreEnabled && currentPlayStoreEnabled !== expectedPlayStoreEnabled) {
|
|
1690
|
+
avdNeedsRecreate = true;
|
|
1691
|
+
avdRecreateReasons.push(`PlayStore.enabled: ${currentPlayStoreEnabled} -> ${expectedPlayStoreEnabled}`);
|
|
1692
|
+
}
|
|
1597
1693
|
} catch {}
|
|
1598
1694
|
}
|
|
1599
1695
|
|
|
@@ -1605,7 +1701,6 @@ class AndroidController {
|
|
|
1605
1701
|
fs.rmSync(avdDir, { recursive: true, force: true });
|
|
1606
1702
|
fs.rmSync(path.join(AVD_HOME, `${this.avdName}.ini`), { force: true });
|
|
1607
1703
|
fs.rmSync(path.join(avdDir, 'userdata-qemu.img'), { force: true });
|
|
1608
|
-
fs.rmSync(emulatorSnapshotStoragePath(this.avdName), { force: true });
|
|
1609
1704
|
} else if (avdExists) {
|
|
1610
1705
|
ensureSparseFile(path.join(avdDir, 'userdata-qemu.img'), DEFAULT_DATA_PARTITION_BYTES);
|
|
1611
1706
|
return;
|
|
@@ -1625,6 +1720,7 @@ class AndroidController {
|
|
|
1625
1720
|
const tagId = parts[2];
|
|
1626
1721
|
const tagDisplay = tagId === 'google_apis' ? 'Google APIs' : tagId.replace(/_/g, ' ');
|
|
1627
1722
|
const abi = parts[3];
|
|
1723
|
+
const playStoreEnabled = tagId.includes('playstore');
|
|
1628
1724
|
const avdDir = path.join(AVD_HOME, `${this.avdName}.avd`);
|
|
1629
1725
|
const imageSysDir = systemImagePackageToRelativeDir(packageName);
|
|
1630
1726
|
if (!imageSysDir) {
|
|
@@ -1648,7 +1744,7 @@ class AndroidController {
|
|
|
1648
1744
|
'avd.ini.encoding=UTF-8',
|
|
1649
1745
|
`AvdId=${this.avdName}`,
|
|
1650
1746
|
`avd.ini.displayname=${this.avdName}`,
|
|
1651
|
-
|
|
1747
|
+
`PlayStore.enabled=${playStoreEnabled}`,
|
|
1652
1748
|
`image.sysdir.1=${imageSysDir}`,
|
|
1653
1749
|
`abi.type=${abi}`,
|
|
1654
1750
|
`hw.cpu.arch=${systemImagePackageToCpuArch(packageName) || abi}`,
|
|
@@ -1671,6 +1767,8 @@ class AndroidController {
|
|
|
1671
1767
|
`sdcard.size=${DEFAULT_SDCARD_SIZE_BYTES}`,
|
|
1672
1768
|
'runtime.network.latency=none',
|
|
1673
1769
|
'runtime.network.speed=full',
|
|
1770
|
+
'fastboot.forceColdBoot=yes',
|
|
1771
|
+
'fastboot.forceFastBoot=no',
|
|
1674
1772
|
'vm.heapSize=256',
|
|
1675
1773
|
`tag.display=${tagDisplay}`,
|
|
1676
1774
|
`tag.id=${tagId}`,
|
|
@@ -1683,7 +1781,6 @@ class AndroidController {
|
|
|
1683
1781
|
if (fs.existsSync(userdataImage)) {
|
|
1684
1782
|
fs.copyFileSync(userdataImage, path.join(avdDir, 'userdata.img'));
|
|
1685
1783
|
}
|
|
1686
|
-
fs.rmSync(emulatorSnapshotStoragePath(this.avdName), { force: true });
|
|
1687
1784
|
}
|
|
1688
1785
|
|
|
1689
1786
|
#normalizeAvdConfig() {
|
|
@@ -1695,9 +1792,60 @@ class AndroidController {
|
|
|
1695
1792
|
content = updateIniValue(content, 'sdcard.size', DEFAULT_SDCARD_SIZE_BYTES);
|
|
1696
1793
|
content = updateIniValue(content, 'hw.ramSize', DEFAULT_RAM_SIZE_MB);
|
|
1697
1794
|
content = updateIniValue(content, 'hw.gpu.mode', emulatorGpuMode());
|
|
1795
|
+
content = updateIniValue(content, 'fastboot.forceColdBoot', 'yes');
|
|
1796
|
+
content = updateIniValue(content, 'fastboot.forceFastBoot', 'no');
|
|
1797
|
+
content = updateIniValue(content, 'PlayStore.enabled', String(this.#readState()?.systemImage || '').includes('playstore'));
|
|
1698
1798
|
fs.writeFileSync(configPath, content);
|
|
1699
1799
|
}
|
|
1700
1800
|
|
|
1801
|
+
#cleanupAvdTransientState() {
|
|
1802
|
+
const avdDir = path.join(AVD_HOME, `${this.avdName}.avd`);
|
|
1803
|
+
const transientTargets = [
|
|
1804
|
+
'cache.img',
|
|
1805
|
+
'cache.img.qcow2',
|
|
1806
|
+
'hardware-qemu.ini.lock',
|
|
1807
|
+
'multiinstance.lock',
|
|
1808
|
+
'snapshot.lock',
|
|
1809
|
+
'quickbootChoice.ini',
|
|
1810
|
+
'launchParams.txt',
|
|
1811
|
+
'emu-launch-params.txt',
|
|
1812
|
+
'bootcompleted.ini',
|
|
1813
|
+
'userdata-qemu.img.lock',
|
|
1814
|
+
'encryptionkey.img',
|
|
1815
|
+
];
|
|
1816
|
+
|
|
1817
|
+
for (const target of transientTargets) {
|
|
1818
|
+
fs.rmSync(path.join(avdDir, target), { force: true, recursive: true });
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
for (const entry of ['snapshots', '.lock']) {
|
|
1822
|
+
fs.rmSync(path.join(avdDir, entry), { force: true, recursive: true });
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
try {
|
|
1826
|
+
for (const entry of fs.readdirSync(avdDir)) {
|
|
1827
|
+
if (/\.lock$/i.test(entry) || /\.tmp$/i.test(entry)) {
|
|
1828
|
+
fs.rmSync(path.join(avdDir, entry), { force: true, recursive: true });
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
} catch {}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
async #forceRecreateAvdForRecovery() {
|
|
1835
|
+
const avdDir = path.join(AVD_HOME, `${this.avdName}.avd`);
|
|
1836
|
+
await this.stopEmulator().catch(() => {});
|
|
1837
|
+
fs.rmSync(avdDir, { recursive: true, force: true });
|
|
1838
|
+
fs.rmSync(path.join(AVD_HOME, `${this.avdName}.ini`), { force: true });
|
|
1839
|
+
this.#appendState({
|
|
1840
|
+
avdSystemImage: null,
|
|
1841
|
+
serial: null,
|
|
1842
|
+
emulatorPid: null,
|
|
1843
|
+
lastLogLine: 'Recreating AVD after failed framework boot.',
|
|
1844
|
+
});
|
|
1845
|
+
await this.ensureAvd();
|
|
1846
|
+
this.#cleanupAvdTransientState();
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1701
1849
|
async listDevices(options = {}) {
|
|
1702
1850
|
if (options.ensureBootstrapped !== false) {
|
|
1703
1851
|
await this.ensureBootstrapped();
|
|
@@ -1789,7 +1937,7 @@ class AndroidController {
|
|
|
1789
1937
|
const args = [
|
|
1790
1938
|
`@${this.avdName}`,
|
|
1791
1939
|
'-no-boot-anim',
|
|
1792
|
-
...emulatorLaunchArgs(
|
|
1940
|
+
...emulatorLaunchArgs(),
|
|
1793
1941
|
'-data',
|
|
1794
1942
|
path.join(AVD_HOME, `${this.avdName}.avd`, 'userdata-qemu.img'),
|
|
1795
1943
|
'-gpu',
|
|
@@ -1831,6 +1979,16 @@ class AndroidController {
|
|
|
1831
1979
|
error?.message ||
|
|
1832
1980
|
String(error || 'Android emulator did not finish booting.');
|
|
1833
1981
|
await this.stopEmulator().catch(() => {});
|
|
1982
|
+
if (!options._recoveredOnce && isRecoverableEmulatorStartError(lastLine)) {
|
|
1983
|
+
console.warn(`[Android] Recoverable emulator start failure detected. Cleaning transient AVD state and retrying once: ${lastLine}`);
|
|
1984
|
+
this.#cleanupAvdTransientState();
|
|
1985
|
+
return this.#startEmulatorBlocking({ ...options, _recoveredOnce: true });
|
|
1986
|
+
}
|
|
1987
|
+
if (!options._recreatedAvdOnce && isRecoverableEmulatorStartError(lastLine)) {
|
|
1988
|
+
console.warn(`[Android] Emulator recovery escalation: recreating AVD and retrying once: ${lastLine}`);
|
|
1989
|
+
await this.#forceRecreateAvdForRecovery();
|
|
1990
|
+
return this.#startEmulatorBlocking({ ...options, _recoveredOnce: true, _recreatedAvdOnce: true });
|
|
1991
|
+
}
|
|
1834
1992
|
this.markBootstrapFailure(lastLine);
|
|
1835
1993
|
throw new Error(lastLine);
|
|
1836
1994
|
}
|
|
@@ -1889,6 +2047,24 @@ class AndroidController {
|
|
|
1889
2047
|
};
|
|
1890
2048
|
}
|
|
1891
2049
|
|
|
2050
|
+
const currentState = this.#readState();
|
|
2051
|
+
const existingWorkerPid = Number(currentState.bootstrapWorkerPid || 0);
|
|
2052
|
+
if (isProcessAlive(existingWorkerPid)) {
|
|
2053
|
+
return {
|
|
2054
|
+
success: true,
|
|
2055
|
+
pending: true,
|
|
2056
|
+
bootstrapped: currentState.bootstrapped === true,
|
|
2057
|
+
starting: true,
|
|
2058
|
+
startupPhase: currentState.startupPhase || 'Preparing Android runtime',
|
|
2059
|
+
startRequestedAt: currentState.startRequestedAt || null,
|
|
2060
|
+
logPath: currentState.logPath || null,
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (existingWorkerPid) {
|
|
2065
|
+
this.#appendState({ bootstrapWorkerPid: null });
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1892
2068
|
if (!this.startPromise) {
|
|
1893
2069
|
const requestedAt = new Date().toISOString();
|
|
1894
2070
|
this.#appendState({
|
|
@@ -1912,9 +2088,13 @@ class AndroidController {
|
|
|
1912
2088
|
env: workerEnv,
|
|
1913
2089
|
});
|
|
1914
2090
|
child.unref();
|
|
2091
|
+
this.#appendState({
|
|
2092
|
+
bootstrapWorkerPid: child.pid,
|
|
2093
|
+
});
|
|
1915
2094
|
this.startPromise = new Promise((resolve) => {
|
|
1916
2095
|
child.once('exit', (code, signal) => {
|
|
1917
2096
|
this.startPromise = null;
|
|
2097
|
+
this.#appendState({ bootstrapWorkerPid: null });
|
|
1918
2098
|
if (code !== 0) {
|
|
1919
2099
|
console.error('[Android] Emulator bootstrap worker exited', { code, signal });
|
|
1920
2100
|
}
|
|
@@ -1922,6 +2102,7 @@ class AndroidController {
|
|
|
1922
2102
|
});
|
|
1923
2103
|
child.once('error', (error) => {
|
|
1924
2104
|
this.startPromise = null;
|
|
2105
|
+
this.#appendState({ bootstrapWorkerPid: null });
|
|
1925
2106
|
console.error('[Android] Emulator bootstrap worker failed to spawn', error);
|
|
1926
2107
|
resolve({ code: null, signal: null, error });
|
|
1927
2108
|
});
|
|
@@ -1943,15 +2124,110 @@ class AndroidController {
|
|
|
1943
2124
|
async waitForDevice(options = {}) {
|
|
1944
2125
|
const timeoutMs = Math.max(10000, Number(options.timeoutMs) || 180000);
|
|
1945
2126
|
const deadline = Date.now() + timeoutMs;
|
|
2127
|
+
let reconnectCounter = 0;
|
|
2128
|
+
let missingPidSince = null;
|
|
2129
|
+
let firstOnlineAt = null;
|
|
2130
|
+
let offlineSince = null;
|
|
1946
2131
|
|
|
1947
2132
|
while (Date.now() < deadline) {
|
|
1948
2133
|
const serial = await this.getPrimarySerial();
|
|
1949
2134
|
if (serial) {
|
|
1950
2135
|
this.#assertSerialAccess(serial, { claimIfUnowned: true });
|
|
1951
|
-
|
|
1952
|
-
|
|
2136
|
+
if (!firstOnlineAt) {
|
|
2137
|
+
firstOnlineAt = Date.now();
|
|
2138
|
+
}
|
|
2139
|
+
const bootCompleted = await this.#runAllowFailure(
|
|
2140
|
+
`${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell getprop sys.boot_completed`,
|
|
2141
|
+
{ timeout: 10000 },
|
|
2142
|
+
);
|
|
2143
|
+
const devBootComplete = await this.#runAllowFailure(
|
|
2144
|
+
`${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell getprop dev.bootcomplete`,
|
|
2145
|
+
{ timeout: 10000 },
|
|
2146
|
+
);
|
|
2147
|
+
const bootAnim = await this.#runAllowFailure(
|
|
2148
|
+
`${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell getprop init.svc.bootanim`,
|
|
2149
|
+
{ timeout: 10000 },
|
|
2150
|
+
);
|
|
2151
|
+
const shellProbe = await this.#runAllowFailure(
|
|
2152
|
+
`${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell echo ready`,
|
|
2153
|
+
{ timeout: 10000 },
|
|
2154
|
+
);
|
|
2155
|
+
const packageServiceProbe = await this.#runAllowFailure(
|
|
2156
|
+
`${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell service check package`,
|
|
2157
|
+
{ timeout: 10000 },
|
|
2158
|
+
);
|
|
2159
|
+
const pmProbe = await this.#runAllowFailure(
|
|
2160
|
+
`${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell pm path android`,
|
|
2161
|
+
{ timeout: 10000 },
|
|
2162
|
+
);
|
|
2163
|
+
|
|
2164
|
+
const bootValue = String(bootCompleted.stdout || '').trim();
|
|
2165
|
+
const devBootValue = String(devBootComplete.stdout || '').trim();
|
|
2166
|
+
const bootAnimValue = String(bootAnim.stdout || '').trim().toLowerCase();
|
|
2167
|
+
const shellReady = String(shellProbe.stdout || '').trim() === 'ready';
|
|
2168
|
+
const packageServiceReady = /found/i.test(String(packageServiceProbe.stdout || ''));
|
|
2169
|
+
const packageManagerReady = /^package:/m.test(String(pmProbe.stdout || '').trim());
|
|
2170
|
+
if (
|
|
2171
|
+
packageServiceReady
|
|
2172
|
+
&& packageManagerReady
|
|
2173
|
+
&& (
|
|
2174
|
+
bootValue === '1'
|
|
2175
|
+
|| devBootValue === '1'
|
|
2176
|
+
|| (shellReady && (bootAnimValue === 'stopped' || bootAnimValue === ''))
|
|
2177
|
+
)
|
|
2178
|
+
) {
|
|
1953
2179
|
return serial;
|
|
1954
2180
|
}
|
|
2181
|
+
if (
|
|
2182
|
+
firstOnlineAt
|
|
2183
|
+
&& Date.now() - firstOnlineAt > 120000
|
|
2184
|
+
&& (
|
|
2185
|
+
!packageServiceReady
|
|
2186
|
+
|| !packageManagerReady
|
|
2187
|
+
)
|
|
2188
|
+
) {
|
|
2189
|
+
throw new Error('Android framework did not become ready (package manager service did not become ready).');
|
|
2190
|
+
}
|
|
2191
|
+
missingPidSince = null;
|
|
2192
|
+
offlineSince = null;
|
|
2193
|
+
} else {
|
|
2194
|
+
firstOnlineAt = null;
|
|
2195
|
+
const state = this.#readState();
|
|
2196
|
+
const emulatorPid = Number(state.emulatorPid || 0);
|
|
2197
|
+
if (emulatorPid > 0 && !isProcessAlive(emulatorPid)) {
|
|
2198
|
+
throw new Error('Android emulator exited before boot completed.');
|
|
2199
|
+
}
|
|
2200
|
+
if (!emulatorPid) {
|
|
2201
|
+
if (!missingPidSince) {
|
|
2202
|
+
missingPidSince = Date.now();
|
|
2203
|
+
}
|
|
2204
|
+
if (Date.now() - missingPidSince > 20000) {
|
|
2205
|
+
throw new Error('Android emulator is not running.');
|
|
2206
|
+
}
|
|
2207
|
+
} else {
|
|
2208
|
+
missingPidSince = null;
|
|
2209
|
+
const devices = await this.listDevices({ ensureBootstrapped: false }).catch(() => []);
|
|
2210
|
+
const hasOfflineEmulator = devices.some((device) => device.emulator && device.status === 'offline');
|
|
2211
|
+
if (hasOfflineEmulator) {
|
|
2212
|
+
if (!offlineSince) {
|
|
2213
|
+
offlineSince = Date.now();
|
|
2214
|
+
}
|
|
2215
|
+
if (Date.now() - offlineSince > 30000) {
|
|
2216
|
+
await this.#runAllowFailure(`${quoteShell(adbBinary())} kill-server`, { timeout: 10000 });
|
|
2217
|
+
await sleep(600);
|
|
2218
|
+
await this.#runAllowFailure(`${quoteShell(adbBinary())} start-server`, { timeout: 15000 });
|
|
2219
|
+
await this.#runAllowFailure(`${quoteShell(adbBinary())} reconnect`, { timeout: 15000 });
|
|
2220
|
+
await this.#runAllowFailure(`${quoteShell(adbBinary())} wait-for-device`, { timeout: 30000 });
|
|
2221
|
+
offlineSince = Date.now();
|
|
2222
|
+
}
|
|
2223
|
+
} else {
|
|
2224
|
+
offlineSince = null;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
reconnectCounter += 1;
|
|
2229
|
+
if (reconnectCounter % 5 === 0) {
|
|
2230
|
+
await this.#runAllowFailure(`${quoteShell(adbBinary())} reconnect offline`, { timeout: 10000 });
|
|
1955
2231
|
}
|
|
1956
2232
|
await sleep(3000);
|
|
1957
2233
|
}
|
|
@@ -2016,8 +2292,17 @@ class AndroidController {
|
|
|
2016
2292
|
return this.#run(`${quoteShell(adbBinary())} -s ${quoteShell(serial)} ${command}`, options);
|
|
2017
2293
|
}
|
|
2018
2294
|
|
|
2295
|
+
async #prepareScreenForCapture(serial) {
|
|
2296
|
+
await this.#adb(serial, 'shell input keyevent 224', { timeout: 10000 }).catch(() => {});
|
|
2297
|
+
await this.#adb(serial, 'shell wm dismiss-keyguard', { timeout: 10000 }).catch(() => {});
|
|
2298
|
+
await this.#adb(serial, 'shell input keyevent 82', { timeout: 10000 }).catch(() => {});
|
|
2299
|
+
await this.#adb(serial, 'shell input keyevent 3', { timeout: 10000 }).catch(() => {});
|
|
2300
|
+
await sleep(350);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2019
2303
|
async screenshot(options = {}) {
|
|
2020
2304
|
const serial = options.serial || await this.ensureDevice();
|
|
2305
|
+
await this.#prepareScreenForCapture(serial).catch(() => {});
|
|
2021
2306
|
let artifactRecord = null;
|
|
2022
2307
|
let filename = `android_${Date.now()}.png`;
|
|
2023
2308
|
let fullPath = path.join(SCREENSHOTS_DIR, filename);
|
|
@@ -2035,7 +2320,32 @@ class AndroidController {
|
|
|
2035
2320
|
fullPath = artifactRecord.storagePath;
|
|
2036
2321
|
filename = path.basename(fullPath);
|
|
2037
2322
|
}
|
|
2038
|
-
|
|
2323
|
+
let captured = false;
|
|
2324
|
+
const localTmp = path.join(TMP_DIR, `shot-${Date.now()}-${Math.random().toString(16).slice(2)}.png`);
|
|
2325
|
+
const remoteTmp = `/sdcard/neoagent-shot-${Date.now()}.png`;
|
|
2326
|
+
for (let attempt = 0; attempt < 3 && !captured; attempt += 1) {
|
|
2327
|
+
try {
|
|
2328
|
+
if (attempt === 0) {
|
|
2329
|
+
await this.#adb(serial, `exec-out screencap -p > ${quoteShell(fullPath)}`, { timeout: 30000 });
|
|
2330
|
+
} else {
|
|
2331
|
+
await this.#adb(serial, `shell screencap -p ${quoteShell(remoteTmp)}`, { timeout: 30000 });
|
|
2332
|
+
await this.#adb(serial, `pull ${quoteShell(remoteTmp)} ${quoteShell(localTmp)}`, { timeout: 30000 });
|
|
2333
|
+
if (fs.existsSync(localTmp)) {
|
|
2334
|
+
fs.copyFileSync(localTmp, fullPath);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
const data = fs.readFileSync(fullPath);
|
|
2338
|
+
captured = isLikelyPng(data);
|
|
2339
|
+
} catch {}
|
|
2340
|
+
if (!captured) {
|
|
2341
|
+
await sleep(500);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
fs.rmSync(localTmp, { force: true });
|
|
2345
|
+
await this.#adb(serial, `shell rm -f ${quoteShell(remoteTmp)}`, { timeout: 10000 }).catch(() => {});
|
|
2346
|
+
if (!captured) {
|
|
2347
|
+
throw new Error('Failed to capture a valid Android screenshot.');
|
|
2348
|
+
}
|
|
2039
2349
|
if (artifactRecord) {
|
|
2040
2350
|
this.artifactStore.finalizeFile(artifactRecord.artifactId, fullPath);
|
|
2041
2351
|
}
|
|
@@ -2393,9 +2703,36 @@ class AndroidController {
|
|
|
2393
2703
|
}
|
|
2394
2704
|
|
|
2395
2705
|
async listApps(args = {}) {
|
|
2396
|
-
|
|
2706
|
+
let serial = null;
|
|
2707
|
+
try {
|
|
2708
|
+
serial = await this.ensureDevice();
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
return {
|
|
2711
|
+
success: false,
|
|
2712
|
+
serial: null,
|
|
2713
|
+
count: 0,
|
|
2714
|
+
packages: [],
|
|
2715
|
+
error: String(error?.message || 'Android device is not ready.'),
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2397
2718
|
const cmd = args.includeSystem === true ? 'shell pm list packages' : 'shell pm list packages -3';
|
|
2398
|
-
|
|
2719
|
+
let out = '';
|
|
2720
|
+
try {
|
|
2721
|
+
out = await this.#adb(serial, cmd, { timeout: 30000 });
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
await sleep(1000);
|
|
2724
|
+
try {
|
|
2725
|
+
out = await this.#adb(serial, cmd, { timeout: 30000 });
|
|
2726
|
+
} catch (retryError) {
|
|
2727
|
+
return {
|
|
2728
|
+
success: false,
|
|
2729
|
+
serial,
|
|
2730
|
+
count: 0,
|
|
2731
|
+
packages: [],
|
|
2732
|
+
error: String(retryError?.message || error?.message || 'Failed to list Android apps.'),
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2399
2736
|
const packages = out
|
|
2400
2737
|
.split('\n')
|
|
2401
2738
|
.map((line) => line.trim())
|
|
@@ -2474,8 +2811,9 @@ class AndroidController {
|
|
|
2474
2811
|
|
|
2475
2812
|
async getStatus() {
|
|
2476
2813
|
const state = this.#readState();
|
|
2477
|
-
const
|
|
2478
|
-
const
|
|
2814
|
+
const bootstrapWorkerPid = Number(state.bootstrapWorkerPid || 0) || null;
|
|
2815
|
+
const bootstrapWorkerAlive = isProcessAlive(bootstrapWorkerPid);
|
|
2816
|
+
const devices = isExecutable(adbBinary())
|
|
2479
2817
|
? await this.listDevices({ ensureBootstrapped: false }).catch(() => [])
|
|
2480
2818
|
: [];
|
|
2481
2819
|
const serialInState = String(state.serial || '').trim();
|
|
@@ -2507,8 +2845,8 @@ class AndroidController {
|
|
|
2507
2845
|
}
|
|
2508
2846
|
return {
|
|
2509
2847
|
bootstrapped: state.bootstrapped === true,
|
|
2510
|
-
starting: state.starting === true || this.startPromise != null,
|
|
2511
|
-
startupPhase: state.startupPhase || null,
|
|
2848
|
+
starting: state.starting === true || this.startPromise != null || bootstrapWorkerAlive,
|
|
2849
|
+
startupPhase: state.startupPhase || (bootstrapWorkerAlive ? 'Preparing Android runtime' : null),
|
|
2512
2850
|
startRequestedAt: state.startRequestedAt || null,
|
|
2513
2851
|
lastStartError: state.lastStartError || null,
|
|
2514
2852
|
sdkRoot: activeAndroidSdkRoot(),
|
|
@@ -2519,6 +2857,7 @@ class AndroidController {
|
|
|
2519
2857
|
serial: state.serial,
|
|
2520
2858
|
serialOwnedByCurrentUser,
|
|
2521
2859
|
emulatorPid: state.emulatorPid,
|
|
2860
|
+
bootstrapWorkerPid,
|
|
2522
2861
|
systemImage: state.systemImage || null,
|
|
2523
2862
|
systemImageArch: state.systemImageArch || null,
|
|
2524
2863
|
preferredSystemImageArchs: systemImageArchCandidates(),
|