neoagent 2.3.1-beta.89 → 2.3.1-beta.90
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/README.md +16 -7
- package/flutter_app/lib/features/location/location_service.dart +2 -4
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_app_shell.dart +11 -11
- package/flutter_app/lib/main_chat.dart +46 -42
- package/flutter_app/lib/main_controller.dart +1 -1
- package/flutter_app/lib/main_devices.dart +10 -1
- package/flutter_app/lib/main_integrations.dart +3 -3
- package/flutter_app/lib/main_spacing.dart +18 -0
- package/flutter_app/lib/main_theme.dart +9 -0
- package/flutter_app/lib/main_unified.dart +3 -3
- package/lib/manager.js +33 -0
- package/package.json +1 -1
- package/server/db/database.js +74 -16
- package/server/guest_agent.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +11977 -11964
- package/server/services/android/android_bootstrap_worker.js +18 -3
- package/server/services/android/controller.js +426 -324
- package/server/services/runtime/backends/local-vm.js +31 -9
|
@@ -27,9 +27,11 @@ const ANDROID_BOOTSTRAP_WORKER = path.join(__dirname, 'android_bootstrap_worker.
|
|
|
27
27
|
const ANDROID_JAVA_TOOL_TIMEOUT_MS = 20 * 60 * 1000;
|
|
28
28
|
const DEFAULT_AVD_NAME = 'neoagent-default';
|
|
29
29
|
const DEFAULT_DATA_PARTITION_BYTES = 1024 * 1024 * 1024;
|
|
30
|
-
const DEFAULT_PARTITION_SIZE_MB = 1024;
|
|
31
30
|
const DEFAULT_SDCARD_SIZE_BYTES = 32 * 1024 * 1024;
|
|
32
|
-
const DEFAULT_RAM_SIZE_MB =
|
|
31
|
+
const DEFAULT_RAM_SIZE_MB = 1536;
|
|
32
|
+
const MIN_SYSTEM_IMAGE_INSTALL_FREE_BYTES = 5 * 1024 * 1024 * 1024;
|
|
33
|
+
const EMULATOR_CONSOLE_PORT_MIN = 5554;
|
|
34
|
+
const EMULATOR_CONSOLE_PORT_MAX = 5584;
|
|
33
35
|
const DEFAULT_KEYEVENTS = Object.freeze({
|
|
34
36
|
home: 3,
|
|
35
37
|
back: 4,
|
|
@@ -59,6 +61,9 @@ function ensureEmulatorAdvancedFeaturesFile() {
|
|
|
59
61
|
[
|
|
60
62
|
'QuickbootFileBacked=off',
|
|
61
63
|
'QuickbootSupport=off',
|
|
64
|
+
// Disable FBE so Android doesn't boot into Before-First-Unlock state,
|
|
65
|
+
// which blocks ADB from transitioning offline → device.
|
|
66
|
+
'EncryptUserData=off',
|
|
62
67
|
'',
|
|
63
68
|
].join('\n'),
|
|
64
69
|
'utf8',
|
|
@@ -207,6 +212,40 @@ function tailFile(filePath, maxLines = 40) {
|
|
|
207
212
|
}
|
|
208
213
|
}
|
|
209
214
|
|
|
215
|
+
function buildAndroidBootstrapError(message, details = {}) {
|
|
216
|
+
const error = new Error(message);
|
|
217
|
+
error.code = details.code || 'ANDROID_BOOTSTRAP_FAILED';
|
|
218
|
+
error.details = details;
|
|
219
|
+
return error;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function selectAndroidFailureMessage(logPath, error) {
|
|
223
|
+
const structuredTail = Array.isArray(error?.details?.logTail) ? error.details.logTail : null;
|
|
224
|
+
if (structuredTail && structuredTail.length > 0) {
|
|
225
|
+
return structuredTail.slice(-100).join(' | ');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const errorMessage = error?.message || (error ? String(error) : '');
|
|
229
|
+
const sanitizedErrorMessage = errorMessage
|
|
230
|
+
.split('\n')
|
|
231
|
+
.map((line) => line.trim())
|
|
232
|
+
.filter((line) => line && line !== 'null' && !/^Picked up JAVA_TOOL_OPTIONS:/i.test(line))
|
|
233
|
+
.join('\n');
|
|
234
|
+
if (sanitizedErrorMessage && !/^INFO\s+\|/i.test(sanitizedErrorMessage) && !/^WARNING\s+\|/i.test(sanitizedErrorMessage)) {
|
|
235
|
+
return sanitizedErrorMessage;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const seriousLine = tailFile(logPath, 100)
|
|
239
|
+
.reverse()
|
|
240
|
+
.find((line) => /FATAL|PANIC|ERROR|disk full|broken avd|failed|exited/i.test(line));
|
|
241
|
+
if (seriousLine) {
|
|
242
|
+
return seriousLine;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return sanitizedErrorMessage || tailFile(logPath, 100).at(-1) || 'Android bootstrap failed.';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
210
249
|
function isLikelyPng(buffer) {
|
|
211
250
|
return Buffer.isBuffer(buffer)
|
|
212
251
|
&& buffer.length > 24
|
|
@@ -216,6 +255,58 @@ function isLikelyPng(buffer) {
|
|
|
216
255
|
&& buffer[3] === 0x47;
|
|
217
256
|
}
|
|
218
257
|
|
|
258
|
+
function freeBytesForPath(targetPath) {
|
|
259
|
+
try {
|
|
260
|
+
const stats = fs.statfsSync(targetPath);
|
|
261
|
+
return Number(stats.bavail || 0) * Number(stats.bsize || 0);
|
|
262
|
+
} catch {
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function pruneAndroidRuntimeCache(keepPackageName = null) {
|
|
268
|
+
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
|
269
|
+
fs.mkdirSync(TMP_DIR, { recursive: true });
|
|
270
|
+
|
|
271
|
+
for (const orphan of ['arm64-v8a', 'x86_64', 'x86']) {
|
|
272
|
+
fs.rmSync(path.join(activeAndroidSdkRoot(), orphan), { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const keep = String(keepPackageName || '').trim();
|
|
276
|
+
const root = path.join(activeAndroidSdkRoot(), 'system-images');
|
|
277
|
+
if (!fs.existsSync(root)) return;
|
|
278
|
+
|
|
279
|
+
for (const candidate of parseInstalledSystemImages()) {
|
|
280
|
+
if (keep && candidate.packageName === keep) continue;
|
|
281
|
+
const candidateRoot = path.join(activeAndroidSdkRoot(), ...String(candidate.packageName).split(';').filter(Boolean));
|
|
282
|
+
const notSelectedImage = Boolean(keep && candidate.packageName !== keep);
|
|
283
|
+
const wrongArch = candidate.arch !== systemImageArch();
|
|
284
|
+
const oldProblemImage = /system-images;android-30;default;arm64-v8a/i.test(candidate.packageName);
|
|
285
|
+
if (notSelectedImage || wrongArch || oldProblemImage || !isValidInstalledSystemImage(candidate.packageName)) {
|
|
286
|
+
console.warn('[Android][runtime_cache_prune]', {
|
|
287
|
+
packageName: candidate.packageName,
|
|
288
|
+
reason: notSelectedImage ? 'not_selected_image' : wrongArch ? 'wrong_arch' : oldProblemImage ? 'known_unstable_image' : 'invalid_image',
|
|
289
|
+
});
|
|
290
|
+
fs.rmSync(candidateRoot, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isBlankSparseFile(filePath) {
|
|
296
|
+
try {
|
|
297
|
+
const stats = fs.statSync(filePath);
|
|
298
|
+
if (!stats.isFile()) return false;
|
|
299
|
+
if (stats.size === 0) return true;
|
|
300
|
+
if (stats.blocks !== 0) return false;
|
|
301
|
+
// Sparse backing file is normal when a qcow2 overlay exists alongside it.
|
|
302
|
+
const qcow2Path = `${filePath}.qcow2`;
|
|
303
|
+
const qcow2Stats = fs.statSync(qcow2Path);
|
|
304
|
+
return !qcow2Stats.isFile() || qcow2Stats.size < 8192;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
219
310
|
function commandExists(command) {
|
|
220
311
|
const probe = spawnSync('bash', ['-lc', `command -v "${command}"`], { encoding: 'utf8' });
|
|
221
312
|
return probe.status === 0;
|
|
@@ -284,38 +375,14 @@ function emulatorHostArch() {
|
|
|
284
375
|
}
|
|
285
376
|
|
|
286
377
|
function emulatorGpuMode() {
|
|
287
|
-
if (process.platform === 'darwin'
|
|
288
|
-
return '
|
|
289
|
-
}
|
|
290
|
-
if (process.platform === 'linux' && process.arch === 'arm64') {
|
|
291
|
-
return 'swiftshader_indirect';
|
|
378
|
+
if (process.platform === 'darwin') {
|
|
379
|
+
return 'host';
|
|
292
380
|
}
|
|
293
381
|
return 'auto';
|
|
294
382
|
}
|
|
295
383
|
|
|
296
|
-
function
|
|
297
|
-
|
|
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',
|
|
306
|
-
];
|
|
307
|
-
}
|
|
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
|
-
);
|
|
384
|
+
function logAndroidIssue(event, details = {}) {
|
|
385
|
+
console.warn(`[Android][${event}]`, details);
|
|
319
386
|
}
|
|
320
387
|
|
|
321
388
|
function parseCsvEnv(value) {
|
|
@@ -415,6 +482,7 @@ function sdkEnv() {
|
|
|
415
482
|
ANDROID_USER_HOME: EMULATOR_HOME,
|
|
416
483
|
ANDROID_AVD_HOME: AVD_HOME,
|
|
417
484
|
AVD_HOME,
|
|
485
|
+
ADB_VENDOR_KEYS: EMULATOR_HOME,
|
|
418
486
|
JAVA_TOOL_OPTIONS: process.env.JAVA_TOOL_OPTIONS || '-Xint',
|
|
419
487
|
};
|
|
420
488
|
const pathParts = [
|
|
@@ -752,7 +820,11 @@ function parseRepositorySystemImages(xml) {
|
|
|
752
820
|
return parseSystemImageCandidates(matches);
|
|
753
821
|
}
|
|
754
822
|
|
|
755
|
-
function parseLatestSystemImageUrl(
|
|
823
|
+
function parseLatestSystemImageUrl(source, packageName) {
|
|
824
|
+
const xml = typeof source === 'string' ? source : source?.xml;
|
|
825
|
+
const baseUrl = typeof source === 'string'
|
|
826
|
+
? 'https://dl.google.com/android/repository/sys-img/android/'
|
|
827
|
+
: source?.baseUrl || 'https://dl.google.com/android/repository/sys-img/android/';
|
|
756
828
|
const packageMatch = xml.match(new RegExp(`<remotePackage\\s+path="${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}">([\\s\\S]*?)<\\/remotePackage>`));
|
|
757
829
|
if (!packageMatch) throw new Error(`Could not locate ${packageName} in Android repository metadata`);
|
|
758
830
|
|
|
@@ -762,13 +834,52 @@ function parseLatestSystemImageUrl(xml, packageName) {
|
|
|
762
834
|
if (urlMatch) {
|
|
763
835
|
const urlPart = urlMatch[1];
|
|
764
836
|
if (urlPart.startsWith('http')) return urlPart;
|
|
765
|
-
return
|
|
837
|
+
return `${baseUrl}${urlPart}`;
|
|
766
838
|
}
|
|
767
839
|
}
|
|
768
840
|
|
|
769
841
|
throw new Error(`Could not find a system image archive for ${packageName}`);
|
|
770
842
|
}
|
|
771
843
|
|
|
844
|
+
async function fetchSystemImageRepositories() {
|
|
845
|
+
const sources = [
|
|
846
|
+
{
|
|
847
|
+
url: 'https://dl.google.com/android/repository/sys-img/android/sys-img2-1.xml',
|
|
848
|
+
baseUrl: 'https://dl.google.com/android/repository/sys-img/android/',
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
url: 'https://dl.google.com/android/repository/sys-img/google_apis/sys-img2-1.xml',
|
|
852
|
+
baseUrl: 'https://dl.google.com/android/repository/sys-img/google_apis/',
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
url: 'https://dl.google.com/android/repository/sys-img/google_apis_playstore/sys-img2-1.xml',
|
|
856
|
+
baseUrl: 'https://dl.google.com/android/repository/sys-img/google_apis_playstore/',
|
|
857
|
+
},
|
|
858
|
+
];
|
|
859
|
+
const results = [];
|
|
860
|
+
for (const source of sources) {
|
|
861
|
+
try {
|
|
862
|
+
results.push({
|
|
863
|
+
...source,
|
|
864
|
+
xml: await fetchText(source.url),
|
|
865
|
+
});
|
|
866
|
+
} catch (error) {
|
|
867
|
+
logAndroidIssue('system_image_repository_fetch_failed', {
|
|
868
|
+
url: source.url,
|
|
869
|
+
message: String(error?.message || error),
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (results.length === 0) {
|
|
874
|
+
throw new Error('Could not fetch Android system image repository metadata.');
|
|
875
|
+
}
|
|
876
|
+
return results;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function parseRepositorySystemImagesFromSources(sources) {
|
|
880
|
+
return sources.flatMap((source) => parseRepositorySystemImages(source.xml));
|
|
881
|
+
}
|
|
882
|
+
|
|
772
883
|
async function fetchEmulatorMetadata() {
|
|
773
884
|
const urls = [
|
|
774
885
|
'https://dl.google.com/android/repository/repository2-3.xml',
|
|
@@ -831,15 +942,43 @@ function shouldInstallPlatformToolsArchive() {
|
|
|
831
942
|
}
|
|
832
943
|
|
|
833
944
|
async function installSystemImageArchive(metadata, packageName) {
|
|
834
|
-
const
|
|
945
|
+
const sources = Array.isArray(metadata) ? metadata : [{ xml: metadata }];
|
|
946
|
+
const source = sources.find((entry) => {
|
|
947
|
+
try {
|
|
948
|
+
parseLatestSystemImageUrl(entry, packageName);
|
|
949
|
+
return true;
|
|
950
|
+
} catch {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
if (!source) {
|
|
955
|
+
throw new Error(`Could not locate ${packageName} in Android repository metadata`);
|
|
956
|
+
}
|
|
957
|
+
const url = parseLatestSystemImageUrl(source, packageName);
|
|
835
958
|
const zipPath = path.join(TMP_DIR, path.basename(url));
|
|
836
959
|
const targetRoot = path.join(activeAndroidSdkRoot(), ...String(packageName).split(';').filter(Boolean));
|
|
837
960
|
const extractDir = fs.mkdtempSync(path.join(TMP_DIR, 'system-image-'));
|
|
838
961
|
|
|
839
962
|
try {
|
|
963
|
+
pruneAndroidRuntimeCache(packageName);
|
|
964
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
965
|
+
let freeBytes = freeBytesForPath(ANDROID_ROOT);
|
|
966
|
+
if (freeBytes < MIN_SYSTEM_IMAGE_INSTALL_FREE_BYTES) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
`Not enough disk space to install Android system image ${packageName}. ` +
|
|
969
|
+
`Free ${Math.round(freeBytes / 1024 / 1024)} MB, need at least ${Math.round(MIN_SYSTEM_IMAGE_INSTALL_FREE_BYTES / 1024 / 1024)} MB.`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
840
972
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
841
973
|
fs.mkdirSync(path.dirname(targetRoot), { recursive: true });
|
|
842
974
|
await downloadFile(url, zipPath);
|
|
975
|
+
freeBytes = freeBytesForPath(ANDROID_ROOT);
|
|
976
|
+
if (freeBytes < MIN_SYSTEM_IMAGE_INSTALL_FREE_BYTES / 2) {
|
|
977
|
+
throw new Error(
|
|
978
|
+
`Not enough disk space to extract Android system image ${packageName}. ` +
|
|
979
|
+
`Free ${Math.round(freeBytes / 1024 / 1024)} MB after download.`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
843
982
|
extractZip(zipPath, extractDir);
|
|
844
983
|
|
|
845
984
|
const extractedRoot = findDirectoryContainingFiles(extractDir, ['userdata.img']) ||
|
|
@@ -853,6 +992,13 @@ async function installSystemImageArchive(metadata, packageName) {
|
|
|
853
992
|
try {
|
|
854
993
|
fs.renameSync(extractedRoot, targetRoot);
|
|
855
994
|
} catch (renameErr) {
|
|
995
|
+
freeBytes = freeBytesForPath(ANDROID_ROOT);
|
|
996
|
+
if (freeBytes < MIN_SYSTEM_IMAGE_INSTALL_FREE_BYTES / 2) {
|
|
997
|
+
throw new Error(
|
|
998
|
+
`Not enough disk space to move Android system image ${packageName}. ` +
|
|
999
|
+
`Free ${Math.round(freeBytes / 1024 / 1024)} MB.`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
856
1002
|
fs.cpSync(extractedRoot, targetRoot, { recursive: true, force: true });
|
|
857
1003
|
if (renameErr) {
|
|
858
1004
|
console.warn(`[Android] Falling back to copy for ${packageName}: ${renameErr.message}`);
|
|
@@ -964,11 +1110,59 @@ function chooseStableRuntimeSystemImage(candidates, currentPackage) {
|
|
|
964
1110
|
return recommended;
|
|
965
1111
|
}
|
|
966
1112
|
|
|
1113
|
+
function runtimeSystemImagePreferenceRank(candidate) {
|
|
1114
|
+
if (!candidate) return Number.MAX_SAFE_INTEGER;
|
|
1115
|
+
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
1116
|
+
const preferredApis = [31, 34, 35, 33, 32];
|
|
1117
|
+
const index = preferredApis.indexOf(Number(candidate.apiLevel || 0));
|
|
1118
|
+
return index === -1 ? 1000 + Math.abs(Number(candidate.apiLevel || 0) - 31) : index;
|
|
1119
|
+
}
|
|
1120
|
+
if (process.platform === 'linux' && process.arch === 'arm64') {
|
|
1121
|
+
const preferredApis = [34, 35, 33, 31, 36];
|
|
1122
|
+
const index = preferredApis.indexOf(Number(candidate.apiLevel || 0));
|
|
1123
|
+
return index === -1 ? 1000 + Math.abs(Number(candidate.apiLevel || 0) - 34) : index;
|
|
1124
|
+
}
|
|
1125
|
+
return 0;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function preferredRuntimeSystemImageCandidate(candidates = []) {
|
|
1129
|
+
if (!(process.platform === 'darwin' && process.arch === 'arm64')) {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const desired = [
|
|
1134
|
+
['android-31', 'google_apis', 'arm64-v8a'],
|
|
1135
|
+
['android-34', 'google_apis', 'arm64-v8a'],
|
|
1136
|
+
['android-33', 'google_apis', 'arm64-v8a'],
|
|
1137
|
+
['android-31', 'google_apis_playstore', 'arm64-v8a'],
|
|
1138
|
+
['android-34', 'google_apis_playstore', 'arm64-v8a'],
|
|
1139
|
+
['android-33', 'google_apis_playstore', 'arm64-v8a'],
|
|
1140
|
+
];
|
|
1141
|
+
const parsed = Array.isArray(candidates) ? candidates : [];
|
|
1142
|
+
for (const [platformId, tag, arch] of desired) {
|
|
1143
|
+
const found = parsed.find((candidate) =>
|
|
1144
|
+
candidate.platformId === platformId &&
|
|
1145
|
+
candidate.tag === tag &&
|
|
1146
|
+
candidate.arch === arch
|
|
1147
|
+
);
|
|
1148
|
+
if (found) return found;
|
|
1149
|
+
}
|
|
1150
|
+
const [platformId, tag, arch] = desired[0];
|
|
1151
|
+
return parseSystemImageCandidates([{
|
|
1152
|
+
packageName: `system-images;${platformId};${tag};${arch}`,
|
|
1153
|
+
platformId,
|
|
1154
|
+
tag,
|
|
1155
|
+
arch,
|
|
1156
|
+
}])[0];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
|
|
967
1160
|
function rankSystemImagePool(pool) {
|
|
968
1161
|
const preferredMatches = pool.filter((candidate) => candidate.tagScore > 0);
|
|
969
1162
|
const rankedPool = preferredMatches.length > 0 ? preferredMatches : pool;
|
|
970
1163
|
|
|
971
1164
|
rankedPool.sort((a, b) =>
|
|
1165
|
+
runtimeSystemImagePreferenceRank(a) - runtimeSystemImagePreferenceRank(b) ||
|
|
972
1166
|
Number(b.stable) - Number(a.stable) ||
|
|
973
1167
|
b.tagScore - a.tagScore ||
|
|
974
1168
|
b.apiLevel - a.apiLevel ||
|
|
@@ -1085,12 +1279,24 @@ function isValidInstalledSystemImage(packageName) {
|
|
|
1085
1279
|
const relativeDir = systemImagePackageToRelativeDir(packageName);
|
|
1086
1280
|
if (!relativeDir) return false;
|
|
1087
1281
|
const root = path.join(activeAndroidSdkRoot(), relativeDir);
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
path.join(root, 'system.img')
|
|
1091
|
-
path.join(root, 'userdata.img')
|
|
1092
|
-
|
|
1093
|
-
|
|
1282
|
+
const hasMetadata = fs.existsSync(path.join(root, 'package.xml')) || fs.existsSync(path.join(root, 'source.properties'));
|
|
1283
|
+
return hasMetadata
|
|
1284
|
+
&& fs.existsSync(path.join(root, 'system.img'))
|
|
1285
|
+
&& fs.existsSync(path.join(root, 'userdata.img'));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function systemImagePackageRoot(packageName) {
|
|
1289
|
+
const relativeDir = systemImagePackageToRelativeDir(packageName);
|
|
1290
|
+
if (!relativeDir) return null;
|
|
1291
|
+
return path.join(activeAndroidSdkRoot(), relativeDir);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function ensureSystemImageUserdataImage(packageName) {
|
|
1295
|
+
const root = systemImagePackageRoot(packageName);
|
|
1296
|
+
if (!root) return;
|
|
1297
|
+
const userdataImage = path.join(root, 'userdata.img');
|
|
1298
|
+
if (fs.existsSync(userdataImage)) return;
|
|
1299
|
+
ensureSparseFile(userdataImage, DEFAULT_DATA_PARTITION_BYTES);
|
|
1094
1300
|
}
|
|
1095
1301
|
|
|
1096
1302
|
function systemImagePackageToAbi(packageName) {
|
|
@@ -1371,7 +1577,30 @@ class AndroidController {
|
|
|
1371
1577
|
cwd: options.cwd || ANDROID_ROOT,
|
|
1372
1578
|
});
|
|
1373
1579
|
if (result.exitCode !== 0) {
|
|
1374
|
-
|
|
1580
|
+
const stderr = String(result.stderr || '').trim();
|
|
1581
|
+
const stdout = String(result.stdout || '').trim();
|
|
1582
|
+
const fragments = [];
|
|
1583
|
+
if (result.timedOut) {
|
|
1584
|
+
fragments.push(`Command timed out after ${result.durationMs || options.timeout || 120000}ms: ${command}`);
|
|
1585
|
+
} else {
|
|
1586
|
+
fragments.push(`Command failed (exit ${result.exitCode ?? 'unknown'}): ${command}`);
|
|
1587
|
+
}
|
|
1588
|
+
if (stderr) {
|
|
1589
|
+
fragments.push(`stderr: ${stderr}`);
|
|
1590
|
+
}
|
|
1591
|
+
if (stdout) {
|
|
1592
|
+
fragments.push(`stdout: ${stdout}`);
|
|
1593
|
+
}
|
|
1594
|
+
const error = new Error(fragments.join('\n'));
|
|
1595
|
+
error.details = {
|
|
1596
|
+
command,
|
|
1597
|
+
exitCode: result.exitCode,
|
|
1598
|
+
timedOut: result.timedOut === true,
|
|
1599
|
+
durationMs: result.durationMs,
|
|
1600
|
+
stderr,
|
|
1601
|
+
stdout,
|
|
1602
|
+
};
|
|
1603
|
+
throw error;
|
|
1375
1604
|
}
|
|
1376
1605
|
return result.stdout || '';
|
|
1377
1606
|
}
|
|
@@ -1385,6 +1614,11 @@ class AndroidController {
|
|
|
1385
1614
|
}
|
|
1386
1615
|
|
|
1387
1616
|
async ensureBootstrapped() {
|
|
1617
|
+
this.#appendState({
|
|
1618
|
+
starting: this.#readState().starting === true,
|
|
1619
|
+
startupPhase: 'Checking Android runtime',
|
|
1620
|
+
lastLogLine: 'Checking Android SDK, emulator, and system image.',
|
|
1621
|
+
});
|
|
1388
1622
|
const desiredArch = systemImageArch();
|
|
1389
1623
|
const state = this.#readState();
|
|
1390
1624
|
const installedImages = parseInstalledSystemImages();
|
|
@@ -1392,13 +1626,22 @@ class AndroidController {
|
|
|
1392
1626
|
chooseConfiguredSystemImage(installedImages) ||
|
|
1393
1627
|
chooseLatestSystemImage(installedImages, [desiredArch]) ||
|
|
1394
1628
|
chooseLatestSystemImage(installedImages);
|
|
1395
|
-
const systemImageMetadata = await
|
|
1396
|
-
const available =
|
|
1629
|
+
const systemImageMetadata = await fetchSystemImageRepositories();
|
|
1630
|
+
const available = parseRepositorySystemImagesFromSources(systemImageMetadata);
|
|
1397
1631
|
const preferredAvailable =
|
|
1398
1632
|
chooseConfiguredSystemImage(available) ||
|
|
1633
|
+
preferredRuntimeSystemImageCandidate(available) ||
|
|
1399
1634
|
chooseLatestSystemImage(available, [desiredArch]) ||
|
|
1400
1635
|
chooseLatestSystemImage(available);
|
|
1401
|
-
const
|
|
1636
|
+
const shouldPreferLighterAvailableImage =
|
|
1637
|
+
process.platform === 'darwin' &&
|
|
1638
|
+
process.arch === 'arm64' &&
|
|
1639
|
+
preferredAvailable?.packageName &&
|
|
1640
|
+
/;google_apis;/.test(preferredAvailable.packageName) &&
|
|
1641
|
+
/;google_apis_playstore;/.test(String(preferredInstalled?.packageName || ''));
|
|
1642
|
+
const selectedImage = shouldPreferLighterAvailableImage
|
|
1643
|
+
? preferredAvailable
|
|
1644
|
+
: (rankSystemImagePool([preferredInstalled, preferredAvailable].filter(Boolean))[0] || preferredInstalled || preferredAvailable);
|
|
1402
1645
|
const stateApiLevel = Number(state.apiLevel || 0) || 0;
|
|
1403
1646
|
const legacyLinuxArm64Image =
|
|
1404
1647
|
process.platform === 'linux'
|
|
@@ -1465,6 +1708,10 @@ class AndroidController {
|
|
|
1465
1708
|
isExecutable(emulatorBinary()) &&
|
|
1466
1709
|
installedEmulatorMatchesHost();
|
|
1467
1710
|
if (!binariesReady) {
|
|
1711
|
+
this.#appendState({
|
|
1712
|
+
startupPhase: 'Installing Android tools',
|
|
1713
|
+
lastLogLine: 'Installing Android platform tools and emulator.',
|
|
1714
|
+
});
|
|
1468
1715
|
if (this.bootstrapPromise) {
|
|
1469
1716
|
await this.bootstrapPromise;
|
|
1470
1717
|
} else {
|
|
@@ -1532,13 +1779,25 @@ class AndroidController {
|
|
|
1532
1779
|
this.#appendState({ bootstrapped: true });
|
|
1533
1780
|
const metadata = await fetchEmulatorMetadata();
|
|
1534
1781
|
if (shouldInstallPlatformToolsArchive()) {
|
|
1782
|
+
this.#appendState({
|
|
1783
|
+
startupPhase: 'Installing Android platform tools',
|
|
1784
|
+
lastLogLine: 'Installing Android platform tools.',
|
|
1785
|
+
});
|
|
1535
1786
|
await installPlatformToolsArchive(metadata);
|
|
1536
1787
|
}
|
|
1788
|
+
this.#appendState({
|
|
1789
|
+
startupPhase: 'Installing Android emulator',
|
|
1790
|
+
lastLogLine: 'Installing or updating the Android emulator.',
|
|
1791
|
+
});
|
|
1537
1792
|
await installEmulatorArchive(metadata);
|
|
1538
1793
|
if (!effectiveSelectedImage) {
|
|
1539
1794
|
throw new Error(formatSystemImageError(available));
|
|
1540
1795
|
}
|
|
1541
1796
|
if (effectiveSelectedImage?.packageName) {
|
|
1797
|
+
this.#appendState({
|
|
1798
|
+
startupPhase: 'Installing Android system image',
|
|
1799
|
+
lastLogLine: `Installing ${effectiveSelectedImage.packageName}.`,
|
|
1800
|
+
});
|
|
1542
1801
|
await installSystemImageArchive(systemImageMetadata, effectiveSelectedImage.packageName);
|
|
1543
1802
|
}
|
|
1544
1803
|
this.#appendState({
|
|
@@ -1579,8 +1838,8 @@ class AndroidController {
|
|
|
1579
1838
|
}
|
|
1580
1839
|
await installEmulatorArchive(metadata);
|
|
1581
1840
|
|
|
1582
|
-
const systemImageMetadata = await
|
|
1583
|
-
const available =
|
|
1841
|
+
const systemImageMetadata = await fetchSystemImageRepositories();
|
|
1842
|
+
const available = parseRepositorySystemImagesFromSources(systemImageMetadata);
|
|
1584
1843
|
const systemImage = chooseConfiguredSystemImage(available) || chooseLatestSystemImage(available);
|
|
1585
1844
|
if (!systemImage) throw new Error(formatSystemImageError(available));
|
|
1586
1845
|
|
|
@@ -1599,8 +1858,7 @@ class AndroidController {
|
|
|
1599
1858
|
|
|
1600
1859
|
markBootstrapFailure(error) {
|
|
1601
1860
|
const state = this.#readState();
|
|
1602
|
-
const
|
|
1603
|
-
const detailedMessage = recentLogLines[recentLogLines.length - 1] || error?.message || String(error || 'Android bootstrap failed.');
|
|
1861
|
+
const detailedMessage = selectAndroidFailureMessage(state.logPath, error);
|
|
1604
1862
|
this.#appendState({
|
|
1605
1863
|
starting: false,
|
|
1606
1864
|
startupPhase: 'Start failed',
|
|
@@ -1608,6 +1866,12 @@ class AndroidController {
|
|
|
1608
1866
|
lastLogLine: detailedMessage,
|
|
1609
1867
|
bootstrapWorkerPid: null,
|
|
1610
1868
|
});
|
|
1869
|
+
logAndroidIssue('bootstrap_failure', {
|
|
1870
|
+
scopeKey: this.scopeKey,
|
|
1871
|
+
avdName: this.avdName,
|
|
1872
|
+
message: detailedMessage,
|
|
1873
|
+
logPath: state.logPath || null,
|
|
1874
|
+
});
|
|
1611
1875
|
return detailedMessage;
|
|
1612
1876
|
}
|
|
1613
1877
|
|
|
@@ -1617,172 +1881,40 @@ class AndroidController {
|
|
|
1617
1881
|
const state = this.#readState();
|
|
1618
1882
|
let pkg = state.systemImage;
|
|
1619
1883
|
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
|
-
}
|
|
1634
1884
|
const avdDir = path.join(AVD_HOME, `${this.avdName}.avd`);
|
|
1635
1885
|
const configPath = path.join(avdDir, 'config.ini');
|
|
1636
1886
|
const avdExists = fs.existsSync(configPath);
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
if (avdNeedsRecreate && state.avdSystemImage !== pkg) {
|
|
1640
|
-
avdRecreateReasons.push(`systemImage: ${state.avdSystemImage || 'null'} -> ${pkg}`);
|
|
1641
|
-
}
|
|
1642
|
-
if (avdExists && fs.existsSync(configPath)) {
|
|
1643
|
-
try {
|
|
1644
|
-
const config = fs.readFileSync(configPath, 'utf8');
|
|
1645
|
-
const currentImageDir = readIniValue(config, 'image.sysdir.1');
|
|
1646
|
-
const expectedImageDir = systemImagePackageToRelativeDir(pkg);
|
|
1647
|
-
const currentAbi = readIniValue(config, 'abi.type');
|
|
1648
|
-
const expectedAbi = systemImagePackageToAbi(pkg);
|
|
1649
|
-
const currentCpuArch = readIniValue(config, 'hw.cpu.arch');
|
|
1650
|
-
const expectedCpuArch = systemImagePackageToCpuArch(pkg);
|
|
1651
|
-
const currentDataPartitionSize = readIniValue(config, 'disk.dataPartition.size');
|
|
1652
|
-
const expectedDataPartitionSize = String(DEFAULT_DATA_PARTITION_BYTES);
|
|
1653
|
-
const currentSdcardSize = readIniValue(config, 'sdcard.size');
|
|
1654
|
-
const expectedSdcardSize = String(DEFAULT_SDCARD_SIZE_BYTES);
|
|
1655
|
-
const currentRamSize = readIniValue(config, 'hw.ramSize');
|
|
1656
|
-
const expectedRamSize = String(DEFAULT_RAM_SIZE_MB);
|
|
1657
|
-
const currentGpuMode = readIniValue(config, 'hw.gpu.mode');
|
|
1658
|
-
const expectedGpuMode = emulatorGpuMode();
|
|
1659
|
-
const currentPlayStoreEnabled = readIniValue(config, 'PlayStore.enabled');
|
|
1660
|
-
const expectedPlayStoreEnabled = String(String(pkg || '').includes('playstore'));
|
|
1661
|
-
if (expectedImageDir && currentImageDir && currentImageDir !== expectedImageDir) {
|
|
1662
|
-
avdNeedsRecreate = true;
|
|
1663
|
-
avdRecreateReasons.push(`image.sysdir.1: ${currentImageDir} -> ${expectedImageDir}`);
|
|
1664
|
-
}
|
|
1665
|
-
if (expectedAbi && currentAbi && currentAbi !== expectedAbi) {
|
|
1666
|
-
avdNeedsRecreate = true;
|
|
1667
|
-
avdRecreateReasons.push(`abi.type: ${currentAbi} -> ${expectedAbi}`);
|
|
1668
|
-
}
|
|
1669
|
-
if (expectedCpuArch && currentCpuArch && currentCpuArch !== expectedCpuArch) {
|
|
1670
|
-
avdNeedsRecreate = true;
|
|
1671
|
-
avdRecreateReasons.push(`hw.cpu.arch: ${currentCpuArch} -> ${expectedCpuArch}`);
|
|
1672
|
-
}
|
|
1673
|
-
if (currentDataPartitionSize && currentDataPartitionSize !== expectedDataPartitionSize) {
|
|
1674
|
-
avdNeedsRecreate = true;
|
|
1675
|
-
avdRecreateReasons.push(`disk.dataPartition.size: ${currentDataPartitionSize} -> ${expectedDataPartitionSize}`);
|
|
1676
|
-
}
|
|
1677
|
-
if (currentSdcardSize && currentSdcardSize !== expectedSdcardSize) {
|
|
1678
|
-
avdNeedsRecreate = true;
|
|
1679
|
-
avdRecreateReasons.push(`sdcard.size: ${currentSdcardSize} -> ${expectedSdcardSize}`);
|
|
1680
|
-
}
|
|
1681
|
-
if (currentRamSize && currentRamSize !== expectedRamSize) {
|
|
1682
|
-
avdNeedsRecreate = true;
|
|
1683
|
-
avdRecreateReasons.push(`hw.ramSize: ${currentRamSize} -> ${expectedRamSize}`);
|
|
1684
|
-
}
|
|
1685
|
-
if (currentGpuMode && currentGpuMode !== expectedGpuMode) {
|
|
1686
|
-
avdNeedsRecreate = true;
|
|
1687
|
-
avdRecreateReasons.push(`hw.gpu.mode: ${currentGpuMode} -> ${expectedGpuMode}`);
|
|
1688
|
-
}
|
|
1689
|
-
if (currentPlayStoreEnabled && currentPlayStoreEnabled !== expectedPlayStoreEnabled) {
|
|
1690
|
-
avdNeedsRecreate = true;
|
|
1691
|
-
avdRecreateReasons.push(`PlayStore.enabled: ${currentPlayStoreEnabled} -> ${expectedPlayStoreEnabled}`);
|
|
1692
|
-
}
|
|
1693
|
-
} catch {}
|
|
1887
|
+
if (avdExists && state.avdSystemImage === pkg) {
|
|
1888
|
+
return;
|
|
1694
1889
|
}
|
|
1695
1890
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1891
|
+
ensureSystemImageUserdataImage(pkg);
|
|
1892
|
+
|
|
1893
|
+
const createAvdCommand =
|
|
1894
|
+
`printf 'no\\n' | ${quoteShell(avdManagerBinary())} create avd -n ${quoteShell(this.avdName)} -k "${pkg}" --force`;
|
|
1895
|
+
try {
|
|
1896
|
+
await this.#run(createAvdCommand, { timeout: 300000 });
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
const message = String(error?.message || error || '');
|
|
1899
|
+
if (/Unable to find a 'userdata\.img' file/i.test(message)) {
|
|
1900
|
+
ensureSystemImageUserdataImage(pkg);
|
|
1901
|
+
await this.#run(createAvdCommand, { timeout: 300000 });
|
|
1902
|
+
} else if (/"emulator" package must be installed/i.test(message)) {
|
|
1903
|
+
this.#appendState({
|
|
1904
|
+
startupPhase: 'Repairing Android emulator package',
|
|
1905
|
+
lastLogLine: 'Reinstalling Android emulator package.',
|
|
1906
|
+
});
|
|
1907
|
+
const metadata = await fetchEmulatorMetadata();
|
|
1908
|
+
await installEmulatorArchive(metadata);
|
|
1909
|
+
await this.#run(createAvdCommand, { timeout: 300000 });
|
|
1910
|
+
} else {
|
|
1911
|
+
throw error;
|
|
1699
1912
|
}
|
|
1700
|
-
await this.stopEmulator().catch(() => {});
|
|
1701
|
-
fs.rmSync(avdDir, { recursive: true, force: true });
|
|
1702
|
-
fs.rmSync(path.join(AVD_HOME, `${this.avdName}.ini`), { force: true });
|
|
1703
|
-
fs.rmSync(path.join(avdDir, 'userdata-qemu.img'), { force: true });
|
|
1704
|
-
} else if (avdExists) {
|
|
1705
|
-
ensureSparseFile(path.join(avdDir, 'userdata-qemu.img'), DEFAULT_DATA_PARTITION_BYTES);
|
|
1706
|
-
return;
|
|
1707
1913
|
}
|
|
1708
|
-
|
|
1709
|
-
this.#writeAvdFiles(pkg);
|
|
1710
1914
|
this.#normalizeAvdConfig();
|
|
1711
1915
|
this.#appendState({ avdSystemImage: pkg });
|
|
1712
1916
|
}
|
|
1713
1917
|
|
|
1714
|
-
#writeAvdFiles(packageName) {
|
|
1715
|
-
const parts = String(packageName || '').split(';').filter(Boolean);
|
|
1716
|
-
if (parts.length !== 4 || parts[0] !== 'system-images') {
|
|
1717
|
-
throw new Error(`Invalid Android system image package: ${packageName}`);
|
|
1718
|
-
}
|
|
1719
|
-
const apiLevel = parts[1].replace(/^android-/, '');
|
|
1720
|
-
const tagId = parts[2];
|
|
1721
|
-
const tagDisplay = tagId === 'google_apis' ? 'Google APIs' : tagId.replace(/_/g, ' ');
|
|
1722
|
-
const abi = parts[3];
|
|
1723
|
-
const playStoreEnabled = tagId.includes('playstore');
|
|
1724
|
-
const avdDir = path.join(AVD_HOME, `${this.avdName}.avd`);
|
|
1725
|
-
const imageSysDir = systemImagePackageToRelativeDir(packageName);
|
|
1726
|
-
if (!imageSysDir) {
|
|
1727
|
-
throw new Error(`Invalid Android system image directory for package: ${packageName}`);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
fs.mkdirSync(avdDir, { recursive: true });
|
|
1731
|
-
ensureSparseFile(path.join(avdDir, 'userdata-qemu.img'), DEFAULT_DATA_PARTITION_BYTES);
|
|
1732
|
-
fs.writeFileSync(
|
|
1733
|
-
path.join(AVD_HOME, `${this.avdName}.ini`),
|
|
1734
|
-
[
|
|
1735
|
-
'avd.ini.encoding=UTF-8',
|
|
1736
|
-
`path=${avdDir}`,
|
|
1737
|
-
`path.rel=avd/${this.avdName}.avd`,
|
|
1738
|
-
`target=android-${apiLevel}`,
|
|
1739
|
-
'',
|
|
1740
|
-
].join('\n')
|
|
1741
|
-
);
|
|
1742
|
-
|
|
1743
|
-
const configLines = [
|
|
1744
|
-
'avd.ini.encoding=UTF-8',
|
|
1745
|
-
`AvdId=${this.avdName}`,
|
|
1746
|
-
`avd.ini.displayname=${this.avdName}`,
|
|
1747
|
-
`PlayStore.enabled=${playStoreEnabled}`,
|
|
1748
|
-
`image.sysdir.1=${imageSysDir}`,
|
|
1749
|
-
`abi.type=${abi}`,
|
|
1750
|
-
`hw.cpu.arch=${systemImagePackageToCpuArch(packageName) || abi}`,
|
|
1751
|
-
'hw.cpu.ncore=2',
|
|
1752
|
-
'hw.dPad=no',
|
|
1753
|
-
'hw.gps=yes',
|
|
1754
|
-
'hw.gpu.enabled=yes',
|
|
1755
|
-
`hw.gpu.mode=${emulatorGpuMode()}`,
|
|
1756
|
-
'hw.initialOrientation=Portrait',
|
|
1757
|
-
'hw.keyboard=yes',
|
|
1758
|
-
'hw.lcd.density=440',
|
|
1759
|
-
'hw.lcd.height=1920',
|
|
1760
|
-
'hw.lcd.width=1080',
|
|
1761
|
-
'hw.mainKeys=no',
|
|
1762
|
-
`hw.ramSize=${DEFAULT_RAM_SIZE_MB}`,
|
|
1763
|
-
'hw.sensors.orientation=yes',
|
|
1764
|
-
'hw.sensors.proximity=yes',
|
|
1765
|
-
'hw.trackBall=no',
|
|
1766
|
-
`disk.dataPartition.size=${DEFAULT_DATA_PARTITION_BYTES}`,
|
|
1767
|
-
`sdcard.size=${DEFAULT_SDCARD_SIZE_BYTES}`,
|
|
1768
|
-
'runtime.network.latency=none',
|
|
1769
|
-
'runtime.network.speed=full',
|
|
1770
|
-
'fastboot.forceColdBoot=yes',
|
|
1771
|
-
'fastboot.forceFastBoot=no',
|
|
1772
|
-
'vm.heapSize=256',
|
|
1773
|
-
`tag.display=${tagDisplay}`,
|
|
1774
|
-
`tag.id=${tagId}`,
|
|
1775
|
-
'',
|
|
1776
|
-
];
|
|
1777
|
-
fs.writeFileSync(path.join(avdDir, 'config.ini'), configLines.join('\n'));
|
|
1778
|
-
|
|
1779
|
-
const systemImageRoot = path.join(activeAndroidSdkRoot(), ...String(packageName).split(';').filter(Boolean));
|
|
1780
|
-
const userdataImage = path.join(systemImageRoot, 'userdata.img');
|
|
1781
|
-
if (fs.existsSync(userdataImage)) {
|
|
1782
|
-
fs.copyFileSync(userdataImage, path.join(avdDir, 'userdata.img'));
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
1918
|
#normalizeAvdConfig() {
|
|
1787
1919
|
const configPath = path.join(AVD_HOME, `${this.avdName}.avd`, 'config.ini');
|
|
1788
1920
|
if (!fs.existsSync(configPath)) return;
|
|
@@ -1792,60 +1924,9 @@ class AndroidController {
|
|
|
1792
1924
|
content = updateIniValue(content, 'sdcard.size', DEFAULT_SDCARD_SIZE_BYTES);
|
|
1793
1925
|
content = updateIniValue(content, 'hw.ramSize', DEFAULT_RAM_SIZE_MB);
|
|
1794
1926
|
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'));
|
|
1798
1927
|
fs.writeFileSync(configPath, content);
|
|
1799
1928
|
}
|
|
1800
1929
|
|
|
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
|
-
|
|
1849
1930
|
async listDevices(options = {}) {
|
|
1850
1931
|
if (options.ensureBootstrapped !== false) {
|
|
1851
1932
|
await this.ensureBootstrapped();
|
|
@@ -1875,6 +1956,14 @@ class AndroidController {
|
|
|
1875
1956
|
const devices = await this.listDevices(options);
|
|
1876
1957
|
const owners = this.#readOwnership();
|
|
1877
1958
|
const canUse = (device) => device.status === 'device' && !this.#isSerialOwnedByAnother(device.serial, owners);
|
|
1959
|
+
const expected = state.expectedSerial
|
|
1960
|
+
? devices.find((device) => device.serial === state.expectedSerial && canUse(device))
|
|
1961
|
+
: null;
|
|
1962
|
+
if (expected) {
|
|
1963
|
+
this.#claimSerial(expected.serial);
|
|
1964
|
+
this.#appendState({ serial: expected.serial });
|
|
1965
|
+
return expected.serial;
|
|
1966
|
+
}
|
|
1878
1967
|
|
|
1879
1968
|
const preferred = state.serial ? devices.find((device) => device.serial === state.serial && canUse(device)) : null;
|
|
1880
1969
|
if (preferred) {
|
|
@@ -1907,15 +1996,22 @@ class AndroidController {
|
|
|
1907
1996
|
startRequestedAt: this.#readState().startRequestedAt || new Date().toISOString(),
|
|
1908
1997
|
});
|
|
1909
1998
|
console.log('[Android] Preparing emulator start');
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1999
|
+
let serial = null;
|
|
2000
|
+
try {
|
|
2001
|
+
await this.ensureBootstrapped();
|
|
2002
|
+
await this.#terminateStaleEmulatorProcesses([this.avdName, this.previousAvdName]).catch(() => {});
|
|
2003
|
+
await this.ensureAvd();
|
|
2004
|
+
this.#appendState({
|
|
2005
|
+
starting: true,
|
|
2006
|
+
startupPhase: 'Checking for an existing Android device',
|
|
2007
|
+
lastStartError: null,
|
|
2008
|
+
});
|
|
2009
|
+
this.#normalizeAvdConfig();
|
|
2010
|
+
serial = await this.getPrimarySerial();
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
this.markBootstrapFailure(error);
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
1919
2015
|
if (serial) {
|
|
1920
2016
|
this.#appendState({
|
|
1921
2017
|
starting: false,
|
|
@@ -1937,21 +2033,14 @@ class AndroidController {
|
|
|
1937
2033
|
const args = [
|
|
1938
2034
|
`@${this.avdName}`,
|
|
1939
2035
|
'-no-boot-anim',
|
|
1940
|
-
...emulatorLaunchArgs(),
|
|
1941
|
-
'-data',
|
|
1942
|
-
path.join(AVD_HOME, `${this.avdName}.avd`, 'userdata-qemu.img'),
|
|
1943
2036
|
'-gpu',
|
|
1944
2037
|
emulatorGpuMode(),
|
|
1945
|
-
'-
|
|
1946
|
-
'
|
|
1947
|
-
'-partition-size',
|
|
1948
|
-
String(DEFAULT_PARTITION_SIZE_MB),
|
|
1949
|
-
'-netdelay',
|
|
1950
|
-
'none',
|
|
1951
|
-
'-netspeed',
|
|
1952
|
-
'full',
|
|
2038
|
+
'-no-window',
|
|
2039
|
+
'-no-audio',
|
|
1953
2040
|
];
|
|
1954
2041
|
|
|
2042
|
+
await this.#runAllowFailure(`${quoteShell(adbBinary())} kill-server`, { timeout: 15000 });
|
|
2043
|
+
await this.#runAllowFailure(`${quoteShell(adbBinary())} start-server`, { timeout: 15000 });
|
|
1955
2044
|
const child = spawn(emulatorBinary(), args, {
|
|
1956
2045
|
detached: true,
|
|
1957
2046
|
stdio: ['ignore', out, out],
|
|
@@ -1973,23 +2062,12 @@ class AndroidController {
|
|
|
1973
2062
|
try {
|
|
1974
2063
|
onlineSerial = await this.waitForDevice({ timeoutMs: options.timeoutMs || 600000 });
|
|
1975
2064
|
} catch (error) {
|
|
1976
|
-
const
|
|
1977
|
-
const lastLine =
|
|
1978
|
-
recentLogLines[recentLogLines.length - 1] ||
|
|
1979
|
-
error?.message ||
|
|
1980
|
-
String(error || 'Android emulator did not finish booting.');
|
|
2065
|
+
const lastLine = selectAndroidFailureMessage(logPath, error);
|
|
1981
2066
|
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
|
-
}
|
|
1992
2067
|
this.markBootstrapFailure(lastLine);
|
|
2068
|
+
if (error?.details?.logTail) {
|
|
2069
|
+
throw error;
|
|
2070
|
+
}
|
|
1993
2071
|
throw new Error(lastLine);
|
|
1994
2072
|
}
|
|
1995
2073
|
this.#appendState({
|
|
@@ -2065,6 +2143,8 @@ class AndroidController {
|
|
|
2065
2143
|
this.#appendState({ bootstrapWorkerPid: null });
|
|
2066
2144
|
}
|
|
2067
2145
|
|
|
2146
|
+
await this.ensureBootstrapped();
|
|
2147
|
+
|
|
2068
2148
|
if (!this.startPromise) {
|
|
2069
2149
|
const requestedAt = new Date().toISOString();
|
|
2070
2150
|
this.#appendState({
|
|
@@ -2122,15 +2202,14 @@ class AndroidController {
|
|
|
2122
2202
|
}
|
|
2123
2203
|
|
|
2124
2204
|
async waitForDevice(options = {}) {
|
|
2125
|
-
const timeoutMs = Math.max(10000, Number(options.timeoutMs) ||
|
|
2205
|
+
const timeoutMs = Math.max(10000, Number(options.timeoutMs) || 600000);
|
|
2126
2206
|
const deadline = Date.now() + timeoutMs;
|
|
2127
|
-
let reconnectCounter = 0;
|
|
2128
2207
|
let missingPidSince = null;
|
|
2129
2208
|
let firstOnlineAt = null;
|
|
2130
|
-
let
|
|
2209
|
+
let lastDiagnosticAt = 0;
|
|
2131
2210
|
|
|
2132
2211
|
while (Date.now() < deadline) {
|
|
2133
|
-
const serial = await this.getPrimarySerial();
|
|
2212
|
+
const serial = await this.getPrimarySerial({ ensureBootstrapped: false });
|
|
2134
2213
|
if (serial) {
|
|
2135
2214
|
this.#assertSerialAccess(serial, { claimIfUnowned: true });
|
|
2136
2215
|
if (!firstOnlineAt) {
|
|
@@ -2167,6 +2246,13 @@ class AndroidController {
|
|
|
2167
2246
|
const shellReady = String(shellProbe.stdout || '').trim() === 'ready';
|
|
2168
2247
|
const packageServiceReady = /found/i.test(String(packageServiceProbe.stdout || ''));
|
|
2169
2248
|
const packageManagerReady = /^package:/m.test(String(pmProbe.stdout || '').trim());
|
|
2249
|
+
if (Date.now() - lastDiagnosticAt > 15000) {
|
|
2250
|
+
lastDiagnosticAt = Date.now();
|
|
2251
|
+
this.#appendState({
|
|
2252
|
+
startupPhase: 'Waiting for Android emulator to boot',
|
|
2253
|
+
lastLogLine: `ADB connected to ${serial}; boot=${bootValue || 'unknown'}, devBoot=${devBootValue || 'unknown'}, packageService=${packageServiceReady}, packageManager=${packageManagerReady}.`,
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2170
2256
|
if (
|
|
2171
2257
|
packageServiceReady
|
|
2172
2258
|
&& packageManagerReady
|
|
@@ -2178,18 +2264,7 @@ class AndroidController {
|
|
|
2178
2264
|
) {
|
|
2179
2265
|
return serial;
|
|
2180
2266
|
}
|
|
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
2267
|
missingPidSince = null;
|
|
2192
|
-
offlineSince = null;
|
|
2193
2268
|
} else {
|
|
2194
2269
|
firstOnlineAt = null;
|
|
2195
2270
|
const state = this.#readState();
|
|
@@ -2207,32 +2282,37 @@ class AndroidController {
|
|
|
2207
2282
|
} else {
|
|
2208
2283
|
missingPidSince = null;
|
|
2209
2284
|
const devices = await this.listDevices({ ensureBootstrapped: false }).catch(() => []);
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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;
|
|
2285
|
+
if (Date.now() - lastDiagnosticAt > 15000) {
|
|
2286
|
+
lastDiagnosticAt = Date.now();
|
|
2287
|
+
this.#appendState({
|
|
2288
|
+
startupPhase: 'Waiting for Android emulator to connect',
|
|
2289
|
+
lastLogLine: devices.length > 0
|
|
2290
|
+
? `Emulator process running; ADB: ${devices.map((d) => `${d.serial}:${d.status}`).join(', ')}.`
|
|
2291
|
+
: 'Emulator running, no ADB transport yet.',
|
|
2292
|
+
});
|
|
2225
2293
|
}
|
|
2226
2294
|
}
|
|
2227
2295
|
}
|
|
2228
|
-
reconnectCounter += 1;
|
|
2229
|
-
if (reconnectCounter % 5 === 0) {
|
|
2230
|
-
await this.#runAllowFailure(`${quoteShell(adbBinary())} reconnect offline`, { timeout: 10000 });
|
|
2231
|
-
}
|
|
2232
2296
|
await sleep(3000);
|
|
2233
2297
|
}
|
|
2234
2298
|
|
|
2235
|
-
|
|
2299
|
+
const state = this.#readState();
|
|
2300
|
+
const logTail = tailFile(state.logPath || null, 100);
|
|
2301
|
+
throw buildAndroidBootstrapError(
|
|
2302
|
+
`Android emulator did not finish booting within ${timeoutMs} ms`,
|
|
2303
|
+
{
|
|
2304
|
+
code: 'ANDROID_BOOTSTRAP_TIMEOUT',
|
|
2305
|
+
timeoutMs,
|
|
2306
|
+
logPath: state.logPath || null,
|
|
2307
|
+
logTail,
|
|
2308
|
+
state: {
|
|
2309
|
+
emulatorPid: state.emulatorPid || null,
|
|
2310
|
+
serial: state.serial || null,
|
|
2311
|
+
startupPhase: state.startupPhase || null,
|
|
2312
|
+
lastStartError: state.lastStartError || null,
|
|
2313
|
+
},
|
|
2314
|
+
}
|
|
2315
|
+
);
|
|
2236
2316
|
}
|
|
2237
2317
|
|
|
2238
2318
|
async ensureDevice() {
|
|
@@ -2338,12 +2418,22 @@ class AndroidController {
|
|
|
2338
2418
|
captured = isLikelyPng(data);
|
|
2339
2419
|
} catch {}
|
|
2340
2420
|
if (!captured) {
|
|
2421
|
+
logAndroidIssue('screenshot_attempt_failed', {
|
|
2422
|
+
scopeKey: this.scopeKey,
|
|
2423
|
+
serial,
|
|
2424
|
+
attempt: attempt + 1,
|
|
2425
|
+
});
|
|
2341
2426
|
await sleep(500);
|
|
2342
2427
|
}
|
|
2343
2428
|
}
|
|
2344
2429
|
fs.rmSync(localTmp, { force: true });
|
|
2345
2430
|
await this.#adb(serial, `shell rm -f ${quoteShell(remoteTmp)}`, { timeout: 10000 }).catch(() => {});
|
|
2346
2431
|
if (!captured) {
|
|
2432
|
+
logAndroidIssue('screenshot_capture_failed', {
|
|
2433
|
+
scopeKey: this.scopeKey,
|
|
2434
|
+
serial,
|
|
2435
|
+
fullPath,
|
|
2436
|
+
});
|
|
2347
2437
|
throw new Error('Failed to capture a valid Android screenshot.');
|
|
2348
2438
|
}
|
|
2349
2439
|
if (artifactRecord) {
|
|
@@ -2707,6 +2797,10 @@ class AndroidController {
|
|
|
2707
2797
|
try {
|
|
2708
2798
|
serial = await this.ensureDevice();
|
|
2709
2799
|
} catch (error) {
|
|
2800
|
+
logAndroidIssue('list_apps_device_not_ready', {
|
|
2801
|
+
scopeKey: this.scopeKey,
|
|
2802
|
+
message: String(error?.message || error || 'Android device is not ready.'),
|
|
2803
|
+
});
|
|
2710
2804
|
return {
|
|
2711
2805
|
success: false,
|
|
2712
2806
|
serial: null,
|
|
@@ -2724,6 +2818,12 @@ class AndroidController {
|
|
|
2724
2818
|
try {
|
|
2725
2819
|
out = await this.#adb(serial, cmd, { timeout: 30000 });
|
|
2726
2820
|
} catch (retryError) {
|
|
2821
|
+
logAndroidIssue('list_apps_command_failed', {
|
|
2822
|
+
scopeKey: this.scopeKey,
|
|
2823
|
+
serial,
|
|
2824
|
+
command: cmd,
|
|
2825
|
+
message: String(retryError?.message || error?.message || 'Failed to list Android apps.'),
|
|
2826
|
+
});
|
|
2727
2827
|
return {
|
|
2728
2828
|
success: false,
|
|
2729
2829
|
serial,
|
|
@@ -2846,7 +2946,9 @@ class AndroidController {
|
|
|
2846
2946
|
return {
|
|
2847
2947
|
bootstrapped: state.bootstrapped === true,
|
|
2848
2948
|
starting: state.starting === true || this.startPromise != null || bootstrapWorkerAlive,
|
|
2849
|
-
startupPhase: state.
|
|
2949
|
+
startupPhase: (state.starting === true || this.startPromise != null || bootstrapWorkerAlive)
|
|
2950
|
+
? (state.startupPhase || (bootstrapWorkerAlive ? 'Preparing Android runtime' : null))
|
|
2951
|
+
: null,
|
|
2850
2952
|
startRequestedAt: state.startRequestedAt || null,
|
|
2851
2953
|
lastStartError: state.lastStartError || null,
|
|
2852
2954
|
sdkRoot: activeAndroidSdkRoot(),
|