@react-native-harness/platform-android 1.4.0-rc.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adb.d.ts +3 -1
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +30 -21
- package/dist/app-session.d.ts +11 -2
- package/dist/app-session.d.ts.map +1 -1
- package/dist/app-session.js +17 -12
- package/dist/avd-config.d.ts +1 -0
- package/dist/avd-config.d.ts.map +1 -1
- package/dist/avd-config.js +11 -0
- package/dist/emulator-startup.d.ts +23 -0
- package/dist/emulator-startup.d.ts.map +1 -1
- package/dist/emulator-startup.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +21 -15
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/adb.ts +72 -65
- package/src/app-session.ts +28 -12
- package/src/avd-config.ts +17 -0
- package/src/emulator-startup.ts +74 -0
- package/src/index.ts +1 -0
- package/src/instance.ts +30 -16
- package/src/runner.ts +5 -9
- package/dist/__tests__/adb-id.test.d.ts +0 -2
- package/dist/__tests__/adb-id.test.d.ts.map +0 -1
- package/dist/__tests__/adb-id.test.js +0 -41
- package/dist/__tests__/adb.test.d.ts +0 -2
- package/dist/__tests__/adb.test.d.ts.map +0 -1
- package/dist/__tests__/adb.test.js +0 -552
- package/dist/__tests__/app-monitor.test.d.ts +0 -2
- package/dist/__tests__/app-monitor.test.d.ts.map +0 -1
- package/dist/__tests__/app-monitor.test.js +0 -257
- package/dist/__tests__/avd-config.test.d.ts +0 -2
- package/dist/__tests__/avd-config.test.d.ts.map +0 -1
- package/dist/__tests__/avd-config.test.js +0 -174
- package/dist/__tests__/ci-action.test.d.ts +0 -2
- package/dist/__tests__/ci-action.test.d.ts.map +0 -1
- package/dist/__tests__/ci-action.test.js +0 -46
- package/dist/__tests__/crash-diagnostics.test.d.ts +0 -2
- package/dist/__tests__/crash-diagnostics.test.d.ts.map +0 -1
- package/dist/__tests__/crash-diagnostics.test.js +0 -119
- package/dist/__tests__/crash-parser.test.d.ts +0 -2
- package/dist/__tests__/crash-parser.test.d.ts.map +0 -1
- package/dist/__tests__/crash-parser.test.js +0 -45
- package/dist/__tests__/crash-reporter.test.d.ts +0 -2
- package/dist/__tests__/crash-reporter.test.d.ts.map +0 -1
- package/dist/__tests__/crash-reporter.test.js +0 -150
- package/dist/__tests__/emulator-startup.test.d.ts +0 -2
- package/dist/__tests__/emulator-startup.test.d.ts.map +0 -1
- package/dist/__tests__/emulator-startup.test.js +0 -20
- package/dist/__tests__/environment.test.d.ts +0 -2
- package/dist/__tests__/environment.test.d.ts.map +0 -1
- package/dist/__tests__/environment.test.js +0 -117
- package/dist/__tests__/instance.test.d.ts +0 -2
- package/dist/__tests__/instance.test.d.ts.map +0 -1
- package/dist/__tests__/instance.test.js +0 -593
- package/dist/__tests__/shared-prefs.test.d.ts +0 -2
- package/dist/__tests__/shared-prefs.test.d.ts.map +0 -1
- package/dist/__tests__/shared-prefs.test.js +0 -87
- package/dist/__tests__/targets.test.d.ts +0 -2
- package/dist/__tests__/targets.test.d.ts.map +0 -1
- package/dist/__tests__/targets.test.js +0 -49
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/src/__tests__/adb-id.test.ts +0 -51
- package/src/__tests__/avd-config.test.ts +0 -206
- package/src/__tests__/ci-action.test.ts +0 -81
- package/src/__tests__/crash-diagnostics.test.ts +0 -144
- package/src/__tests__/crash-parser.test.ts +0 -52
- package/src/__tests__/crash-reporter.test.ts +0 -167
- package/src/__tests__/emulator-startup.test.ts +0 -35
- package/src/__tests__/instance.test.ts +0 -845
- package/src/__tests__/shared-prefs.test.ts +0 -144
- package/src/__tests__/targets.test.ts +0 -53
- package/tsconfig.tsbuildinfo +0 -1
package/src/adb.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
|
|
2
2
|
import {
|
|
3
|
+
delay,
|
|
3
4
|
logger,
|
|
4
5
|
spawn,
|
|
5
6
|
SubprocessError,
|
|
7
|
+
waitForAbort,
|
|
6
8
|
type Subprocess,
|
|
7
9
|
} from '@react-native-harness/tools';
|
|
8
10
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
getRequiredAndroidSdkPackages,
|
|
26
28
|
} from './environment.js';
|
|
27
29
|
import {
|
|
30
|
+
getEmulatorCpuCores,
|
|
28
31
|
getEmulatorStartupArgs,
|
|
29
32
|
type EmulatorBootMode,
|
|
30
33
|
} from './emulator-startup.js';
|
|
@@ -33,37 +36,26 @@ import {
|
|
|
33
36
|
AdbPermissionGrantError,
|
|
34
37
|
} from './adb-errors.js';
|
|
35
38
|
|
|
36
|
-
const wait = async (ms: number): Promise<void> => {
|
|
37
|
-
await new Promise((resolve) => {
|
|
38
|
-
setTimeout(resolve, ms);
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const waitForAbort = (signal: AbortSignal): Promise<never> => {
|
|
43
|
-
if (signal.aborted) {
|
|
44
|
-
return Promise.reject(signal.reason);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return new Promise((_, reject) => {
|
|
48
|
-
signal.addEventListener(
|
|
49
|
-
'abort',
|
|
50
|
-
() => {
|
|
51
|
-
reject(signal.reason);
|
|
52
|
-
},
|
|
53
|
-
{ once: true },
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
39
|
const waitWithSignal = async (
|
|
59
40
|
ms: number,
|
|
60
|
-
signal: AbortSignal
|
|
41
|
+
signal: AbortSignal
|
|
61
42
|
): Promise<void> => {
|
|
62
43
|
if (signal.aborted) {
|
|
63
44
|
throw signal.reason;
|
|
64
45
|
}
|
|
65
46
|
|
|
66
|
-
|
|
47
|
+
// Both sides of this race must be torn down explicitly when they lose:
|
|
48
|
+
// an uncancelled timer keeps a ref'd Timeout alive, and an uncancelled
|
|
49
|
+
// abort wait leaks a listener on `signal`, which is a session-lifetime
|
|
50
|
+
// signal here and gets polled against once per second.
|
|
51
|
+
const timer = delay(ms);
|
|
52
|
+
const abort = waitForAbort(signal);
|
|
53
|
+
try {
|
|
54
|
+
await Promise.race([timer.promise, abort.promise]);
|
|
55
|
+
} finally {
|
|
56
|
+
timer.cancel();
|
|
57
|
+
abort.cancel();
|
|
58
|
+
}
|
|
67
59
|
};
|
|
68
60
|
|
|
69
61
|
const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
|
|
@@ -73,7 +65,7 @@ const androidAdbLogger = logger.child('android-adb');
|
|
|
73
65
|
export const emulatorProcess = {
|
|
74
66
|
startDetachedProcess: (
|
|
75
67
|
file: string,
|
|
76
|
-
args: readonly string[]
|
|
68
|
+
args: readonly string[]
|
|
77
69
|
): ChildProcessByStdio<null, Readable, Readable> =>
|
|
78
70
|
nodeSpawn(file, args, {
|
|
79
71
|
detached: true,
|
|
@@ -84,7 +76,7 @@ export const emulatorProcess = {
|
|
|
84
76
|
const appendBoundedOutput = (
|
|
85
77
|
output: string,
|
|
86
78
|
chunk: string,
|
|
87
|
-
limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT
|
|
79
|
+
limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT
|
|
88
80
|
): string => {
|
|
89
81
|
const nextOutput = output + chunk;
|
|
90
82
|
|
|
@@ -165,7 +157,7 @@ export const getRequiredEmulatorPackages = (apiLevel: number): string[] => {
|
|
|
165
157
|
};
|
|
166
158
|
|
|
167
159
|
export const verifyAndroidEmulatorSdk = async (
|
|
168
|
-
apiLevel: number
|
|
160
|
+
apiLevel: number
|
|
169
161
|
): Promise<void> => {
|
|
170
162
|
await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
|
|
171
163
|
};
|
|
@@ -192,7 +184,7 @@ const ensureAvdProfileAvailable = async (profile: string): Promise<void> => {
|
|
|
192
184
|
: 'None reported by avdmanager.';
|
|
193
185
|
|
|
194
186
|
throw new Error(
|
|
195
|
-
`Android AVD profile "${profile}" is not available on this machine. Available profiles: ${availableProfilesList}
|
|
187
|
+
`Android AVD profile "${profile}" is not available on this machine. Available profiles: ${availableProfilesList}`
|
|
196
188
|
);
|
|
197
189
|
};
|
|
198
190
|
|
|
@@ -201,7 +193,7 @@ const ensureAvdConfigExists = async (name: string): Promise<string> => {
|
|
|
201
193
|
|
|
202
194
|
if ((await readAvdConfig(name)) == null) {
|
|
203
195
|
throw new Error(
|
|
204
|
-
`Android AVD "${name}" was created, but config.ini was not found at ${configPath}
|
|
196
|
+
`Android AVD "${name}" was created, but config.ini was not found at ${configPath}.`
|
|
205
197
|
);
|
|
206
198
|
}
|
|
207
199
|
|
|
@@ -235,7 +227,7 @@ const ensureAvdIniExists = async ({
|
|
|
235
227
|
export const getStartAppArgs = (
|
|
236
228
|
bundleId: string,
|
|
237
229
|
activityName: string,
|
|
238
|
-
options?: AndroidAppLaunchOptions
|
|
230
|
+
options?: AndroidAppLaunchOptions
|
|
239
231
|
): string[] => {
|
|
240
232
|
const args = [
|
|
241
233
|
'shell',
|
|
@@ -264,7 +256,7 @@ export const getStartAppArgs = (
|
|
|
264
256
|
|
|
265
257
|
if (!Number.isSafeInteger(value)) {
|
|
266
258
|
throw new Error(
|
|
267
|
-
`Android app launch option "${key}" must be a safe integer
|
|
259
|
+
`Android app launch option "${key}" must be a safe integer.`
|
|
268
260
|
);
|
|
269
261
|
}
|
|
270
262
|
|
|
@@ -276,7 +268,7 @@ export const getStartAppArgs = (
|
|
|
276
268
|
|
|
277
269
|
export const isAppInstalled = async (
|
|
278
270
|
adbId: string,
|
|
279
|
-
bundleId: string
|
|
271
|
+
bundleId: string
|
|
280
272
|
): Promise<boolean> => {
|
|
281
273
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
282
274
|
'-s',
|
|
@@ -293,7 +285,7 @@ export const isAppInstalled = async (
|
|
|
293
285
|
export const reversePort = async (
|
|
294
286
|
adbId: string,
|
|
295
287
|
port: number,
|
|
296
|
-
hostPort: number = port
|
|
288
|
+
hostPort: number = port
|
|
297
289
|
): Promise<void> => {
|
|
298
290
|
await spawn(getAdbBinaryPath(), [
|
|
299
291
|
'-s',
|
|
@@ -306,7 +298,7 @@ export const reversePort = async (
|
|
|
306
298
|
|
|
307
299
|
export const stopApp = async (
|
|
308
300
|
adbId: string,
|
|
309
|
-
bundleId: string
|
|
301
|
+
bundleId: string
|
|
310
302
|
): Promise<void> => {
|
|
311
303
|
await spawn(getAdbBinaryPath(), [
|
|
312
304
|
'-s',
|
|
@@ -322,7 +314,7 @@ export const startApp = async (
|
|
|
322
314
|
adbId: string,
|
|
323
315
|
bundleId: string,
|
|
324
316
|
activityName: string,
|
|
325
|
-
options?: AndroidAppLaunchOptions
|
|
317
|
+
options?: AndroidAppLaunchOptions
|
|
326
318
|
): Promise<void> => {
|
|
327
319
|
await spawn(getAdbBinaryPath(), [
|
|
328
320
|
'-s',
|
|
@@ -341,7 +333,7 @@ export const getDeviceIds = async (): Promise<string[]> => {
|
|
|
341
333
|
};
|
|
342
334
|
|
|
343
335
|
export const getEmulatorName = async (
|
|
344
|
-
adbId: string
|
|
336
|
+
adbId: string
|
|
345
337
|
): Promise<string | null> => {
|
|
346
338
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
347
339
|
'-s',
|
|
@@ -355,7 +347,7 @@ export const getEmulatorName = async (
|
|
|
355
347
|
|
|
356
348
|
export const getShellProperty = async (
|
|
357
349
|
adbId: string,
|
|
358
|
-
property: string
|
|
350
|
+
property: string
|
|
359
351
|
): Promise<string | null> => {
|
|
360
352
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
361
353
|
'-s',
|
|
@@ -377,7 +369,7 @@ export type DeviceInfo = {
|
|
|
377
369
|
};
|
|
378
370
|
|
|
379
371
|
export const getDeviceInfo = async (
|
|
380
|
-
adbId: string
|
|
372
|
+
adbId: string
|
|
381
373
|
): Promise<DeviceInfo | null> => {
|
|
382
374
|
const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer');
|
|
383
375
|
const model = await getShellProperty(adbId, 'ro.product.model');
|
|
@@ -386,7 +378,7 @@ export const getDeviceInfo = async (
|
|
|
386
378
|
|
|
387
379
|
const getRequestedPermissions = async (
|
|
388
380
|
adbId: string,
|
|
389
|
-
bundleId: string
|
|
381
|
+
bundleId: string
|
|
390
382
|
): Promise<string[]> => {
|
|
391
383
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
392
384
|
'-s',
|
|
@@ -476,14 +468,14 @@ export const stopEmulator = async (adbId: string): Promise<void> => {
|
|
|
476
468
|
|
|
477
469
|
export const installApp = async (
|
|
478
470
|
adbId: string,
|
|
479
|
-
appPath: string
|
|
471
|
+
appPath: string
|
|
480
472
|
): Promise<void> => {
|
|
481
473
|
await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
|
|
482
474
|
};
|
|
483
475
|
|
|
484
476
|
export const uninstallApp = async (
|
|
485
477
|
adbId: string,
|
|
486
|
-
bundleId: string
|
|
478
|
+
bundleId: string
|
|
487
479
|
): Promise<void> => {
|
|
488
480
|
await spawn(getAdbBinaryPath(), ['-s', adbId, 'uninstall', bundleId]);
|
|
489
481
|
};
|
|
@@ -502,20 +494,27 @@ export const createAvd = async ({
|
|
|
502
494
|
}: CreateAvdOptions): Promise<void> => {
|
|
503
495
|
const systemImagePackage = getAndroidSystemImagePackage(
|
|
504
496
|
apiLevel,
|
|
505
|
-
getHostAndroidSystemImageArch()
|
|
497
|
+
getHostAndroidSystemImageArch()
|
|
506
498
|
);
|
|
507
499
|
|
|
508
500
|
await verifyAndroidEmulatorSdk(apiLevel);
|
|
509
501
|
await ensureAvdProfileAvailable(profile);
|
|
510
502
|
await spawn('bash', [
|
|
511
503
|
'-lc',
|
|
512
|
-
`printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}" -p "${getAvdDirectory(
|
|
504
|
+
`printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}" -p "${getAvdDirectory(
|
|
505
|
+
name
|
|
506
|
+
)}"`,
|
|
513
507
|
]);
|
|
514
508
|
await ensureAvdIniExists({ name, apiLevel });
|
|
515
509
|
const configPath = await ensureAvdConfigExists(name);
|
|
510
|
+
// hw.cpu.ncore is baked into config.ini (rather than passed as a `-cores`
|
|
511
|
+
// boot flag) so it's part of the AVD's persisted hardware profile: boot
|
|
512
|
+
// snapshots require an identical hardware config to load, so the vCPU
|
|
513
|
+
// count must be fixed at AVD-creation time and stay consistent across
|
|
514
|
+
// boots.
|
|
516
515
|
await spawn('bash', [
|
|
517
516
|
'-lc',
|
|
518
|
-
`printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${configPath}"`,
|
|
517
|
+
`printf '%s\n%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' 'hw.cpu.ncore=${getEmulatorCpuCores()}' >> "${configPath}"`,
|
|
519
518
|
]);
|
|
520
519
|
};
|
|
521
520
|
|
|
@@ -527,7 +526,7 @@ export const deleteAvd = async (name: string): Promise<void> => {
|
|
|
527
526
|
{
|
|
528
527
|
force: true,
|
|
529
528
|
recursive: true,
|
|
530
|
-
}
|
|
529
|
+
}
|
|
531
530
|
);
|
|
532
531
|
await rm(
|
|
533
532
|
`${
|
|
@@ -535,18 +534,18 @@ export const deleteAvd = async (name: string): Promise<void> => {
|
|
|
535
534
|
}/${name}.ini`,
|
|
536
535
|
{
|
|
537
536
|
force: true,
|
|
538
|
-
}
|
|
537
|
+
}
|
|
539
538
|
);
|
|
540
539
|
};
|
|
541
540
|
|
|
542
541
|
export const startEmulator = async (
|
|
543
542
|
name: string,
|
|
544
|
-
mode: EmulatorBootMode = 'default-boot'
|
|
543
|
+
mode: EmulatorBootMode = 'default-boot'
|
|
545
544
|
): Promise<void> => {
|
|
546
545
|
const emulatorBinaryPath = await ensureEmulatorInstalled();
|
|
547
546
|
const childProcess = emulatorProcess.startDetachedProcess(
|
|
548
547
|
emulatorBinaryPath,
|
|
549
|
-
getEmulatorStartupArgs(name, mode)
|
|
548
|
+
getEmulatorStartupArgs(name, mode)
|
|
550
549
|
);
|
|
551
550
|
|
|
552
551
|
let stdout = '';
|
|
@@ -582,7 +581,7 @@ export const startEmulator = async (
|
|
|
582
581
|
stdout,
|
|
583
582
|
stderr,
|
|
584
583
|
error,
|
|
585
|
-
})
|
|
584
|
+
})
|
|
586
585
|
);
|
|
587
586
|
});
|
|
588
587
|
|
|
@@ -594,7 +593,7 @@ export const startEmulator = async (
|
|
|
594
593
|
stderr,
|
|
595
594
|
exitCode,
|
|
596
595
|
signal,
|
|
597
|
-
})
|
|
596
|
+
})
|
|
598
597
|
);
|
|
599
598
|
});
|
|
600
599
|
});
|
|
@@ -609,13 +608,19 @@ export const startEmulator = async (
|
|
|
609
608
|
throw error;
|
|
610
609
|
});
|
|
611
610
|
|
|
612
|
-
const
|
|
613
|
-
|
|
611
|
+
const observationTimer = delay(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS);
|
|
612
|
+
const observationTimeout = observationTimer.promise.then(
|
|
613
|
+
() => 'timeout' as const
|
|
614
614
|
);
|
|
615
615
|
|
|
616
616
|
try {
|
|
617
617
|
await Promise.race([earlyExit, observedBoot, observationTimeout]);
|
|
618
618
|
} finally {
|
|
619
|
+
// If `earlyExit` or `observedBoot` won the race, this timer would
|
|
620
|
+
// otherwise sit ref'd in the event loop for the rest of its 5s;
|
|
621
|
+
// cancelling here is a no-op on the happy "proceed detached" path
|
|
622
|
+
// where the timer itself won.
|
|
623
|
+
observationTimer.cancel();
|
|
619
624
|
cleanup();
|
|
620
625
|
}
|
|
621
626
|
|
|
@@ -626,7 +631,7 @@ export const startEmulator = async (
|
|
|
626
631
|
|
|
627
632
|
export const waitForEmulator = async (
|
|
628
633
|
name: string,
|
|
629
|
-
signal: AbortSignal
|
|
634
|
+
signal: AbortSignal
|
|
630
635
|
): Promise<string> => {
|
|
631
636
|
while (!signal.aborted) {
|
|
632
637
|
const adbIds = await getDeviceIds();
|
|
@@ -651,7 +656,7 @@ export const waitForEmulator = async (
|
|
|
651
656
|
|
|
652
657
|
export const waitForEmulatorDisconnect = async (
|
|
653
658
|
adbId: string,
|
|
654
|
-
signal: AbortSignal
|
|
659
|
+
signal: AbortSignal
|
|
655
660
|
): Promise<void> => {
|
|
656
661
|
while (!signal.aborted) {
|
|
657
662
|
const adbIds = await getDeviceIds();
|
|
@@ -668,7 +673,7 @@ export const waitForEmulatorDisconnect = async (
|
|
|
668
673
|
|
|
669
674
|
export const waitForBoot = async (
|
|
670
675
|
name: string,
|
|
671
|
-
signal: AbortSignal
|
|
676
|
+
signal: AbortSignal
|
|
672
677
|
): Promise<string> => {
|
|
673
678
|
while (!signal.aborted) {
|
|
674
679
|
const adbIds = await getDeviceIds();
|
|
@@ -697,14 +702,14 @@ export const waitForBoot = async (
|
|
|
697
702
|
|
|
698
703
|
export const isAppRunning = async (
|
|
699
704
|
adbId: string,
|
|
700
|
-
bundleId: string
|
|
705
|
+
bundleId: string
|
|
701
706
|
): Promise<boolean> => {
|
|
702
707
|
return (await getAppPid(adbId, bundleId)) != null;
|
|
703
708
|
};
|
|
704
709
|
|
|
705
710
|
export const getAppPid = async (
|
|
706
711
|
adbId: string,
|
|
707
|
-
bundleId: string
|
|
712
|
+
bundleId: string
|
|
708
713
|
): Promise<number | null> => {
|
|
709
714
|
try {
|
|
710
715
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
@@ -731,7 +736,7 @@ export const getAppPid = async (
|
|
|
731
736
|
|
|
732
737
|
export const getAppUid = async (
|
|
733
738
|
adbId: string,
|
|
734
|
-
bundleId: string
|
|
739
|
+
bundleId: string
|
|
735
740
|
): Promise<number> => {
|
|
736
741
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
737
742
|
'-s',
|
|
@@ -756,7 +761,7 @@ export const getAppUid = async (
|
|
|
756
761
|
|
|
757
762
|
export const setHideErrorDialogs = async (
|
|
758
763
|
adbId: string,
|
|
759
|
-
hide: boolean
|
|
764
|
+
hide: boolean
|
|
760
765
|
): Promise<void> => {
|
|
761
766
|
await spawn(getAdbBinaryPath(), [
|
|
762
767
|
'-s',
|
|
@@ -785,10 +790,12 @@ export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
|
|
|
785
790
|
export const startLogcat = (
|
|
786
791
|
adbId: string,
|
|
787
792
|
args: readonly string[],
|
|
793
|
+
options?: { signal?: AbortSignal }
|
|
788
794
|
): Subprocess =>
|
|
789
795
|
spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
|
|
790
796
|
stdout: 'pipe',
|
|
791
797
|
stderr: 'pipe',
|
|
798
|
+
signal: options?.signal,
|
|
792
799
|
});
|
|
793
800
|
|
|
794
801
|
export const DROPBOX_CRASH_TAGS = [
|
|
@@ -798,7 +805,7 @@ export const DROPBOX_CRASH_TAGS = [
|
|
|
798
805
|
|
|
799
806
|
export const getDropboxPrint = async (
|
|
800
807
|
adbId: string,
|
|
801
|
-
tags: readonly string[] = DROPBOX_CRASH_TAGS
|
|
808
|
+
tags: readonly string[] = DROPBOX_CRASH_TAGS
|
|
802
809
|
): Promise<string> => {
|
|
803
810
|
// Android treats multiple args after --print as one search string, so each
|
|
804
811
|
// tag must be queried separately and merged on the host.
|
|
@@ -815,7 +822,7 @@ export const getDropboxPrint = async (
|
|
|
815
822
|
]);
|
|
816
823
|
|
|
817
824
|
return stdout;
|
|
818
|
-
})
|
|
825
|
+
})
|
|
819
826
|
);
|
|
820
827
|
|
|
821
828
|
return outputs.join('\n');
|
|
@@ -823,7 +830,7 @@ export const getDropboxPrint = async (
|
|
|
823
830
|
|
|
824
831
|
export const getActivityExitInfo = async (
|
|
825
832
|
adbId: string,
|
|
826
|
-
bundleId: string
|
|
833
|
+
bundleId: string
|
|
827
834
|
): Promise<string> => {
|
|
828
835
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
829
836
|
'-s',
|
|
@@ -891,7 +898,7 @@ export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
|
|
|
891
898
|
|
|
892
899
|
export const grantPermissions = async (
|
|
893
900
|
adbId: string,
|
|
894
|
-
bundleId: string
|
|
901
|
+
bundleId: string
|
|
895
902
|
): Promise<void> => {
|
|
896
903
|
androidAdbLogger.debug('grantPermissions:start %o', {
|
|
897
904
|
adbId,
|
|
@@ -908,7 +915,7 @@ export const grantPermissions = async (
|
|
|
908
915
|
getDangerousPermissions(adbId),
|
|
909
916
|
]);
|
|
910
917
|
const permissions = requestedPermissions.filter((permission) =>
|
|
911
|
-
dangerousPermissions.has(permission)
|
|
918
|
+
dangerousPermissions.has(permission)
|
|
912
919
|
);
|
|
913
920
|
|
|
914
921
|
androidAdbLogger.debug('grantPermissions:resolved %o', {
|
|
@@ -944,7 +951,7 @@ export const grantPermissions = async (
|
|
|
944
951
|
});
|
|
945
952
|
|
|
946
953
|
await Promise.all(
|
|
947
|
-
grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[]))
|
|
954
|
+
grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[]))
|
|
948
955
|
);
|
|
949
956
|
|
|
950
957
|
androidAdbLogger.debug('grantPermissions:success %o', {
|
package/src/app-session.ts
CHANGED
|
@@ -62,14 +62,6 @@ const isCrashSignal = (line: string, bundleId: string): boolean => {
|
|
|
62
62
|
);
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
const stopSubprocess = async (child: Subprocess) => {
|
|
66
|
-
try {
|
|
67
|
-
(await child.nodeChildProcess).kill();
|
|
68
|
-
} catch {
|
|
69
|
-
// Ignore termination failures for already-ended background processes.
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
65
|
const isExitedState = (
|
|
74
66
|
state: AppSessionState
|
|
75
67
|
): state is Extract<AppSessionState, { status: 'exited' }> =>
|
|
@@ -82,10 +74,20 @@ type CreateAndroidAppSessionOptions = {
|
|
|
82
74
|
stopApp: () => Promise<void>;
|
|
83
75
|
getAppPid: () => Promise<number | null>;
|
|
84
76
|
getLogcatTimestamp: () => Promise<string>;
|
|
85
|
-
startLogcat: (
|
|
77
|
+
startLogcat: (
|
|
78
|
+
args: readonly string[],
|
|
79
|
+
options: { signal: AbortSignal }
|
|
80
|
+
) => Subprocess;
|
|
86
81
|
getDropboxOutput?: () => Promise<string>;
|
|
87
82
|
getExitInfo?: () => Promise<string>;
|
|
88
83
|
crashArtifactWriter?: CrashArtifactWriter;
|
|
84
|
+
/**
|
|
85
|
+
* Session-lifetime abort signal (see HarnessPlatformInitOptions.signal).
|
|
86
|
+
* Combined with the app session's own logcatAbortController so that
|
|
87
|
+
* aborting the session stops the logcat stream even if dispose() hasn't
|
|
88
|
+
* been called yet (e.g. SIGTERM during teardown of other resources).
|
|
89
|
+
*/
|
|
90
|
+
signal?: AbortSignal;
|
|
89
91
|
};
|
|
90
92
|
|
|
91
93
|
export const createAndroidAppSession = async ({
|
|
@@ -99,6 +101,7 @@ export const createAndroidAppSession = async ({
|
|
|
99
101
|
getDropboxOutput,
|
|
100
102
|
getExitInfo,
|
|
101
103
|
crashArtifactWriter,
|
|
104
|
+
signal,
|
|
102
105
|
}: CreateAndroidAppSessionOptions): Promise<AppSession> => {
|
|
103
106
|
const emitter = createAppSessionEmitter();
|
|
104
107
|
const logBuffer = createBoundedLogBuffer();
|
|
@@ -193,7 +196,20 @@ export const createAndroidAppSession = async ({
|
|
|
193
196
|
|
|
194
197
|
const logcatTimestamp = await getLogcatTimestamp();
|
|
195
198
|
const sessionStartedAt = Date.now();
|
|
196
|
-
const
|
|
199
|
+
const logcatAbortController = new AbortController();
|
|
200
|
+
const logcatSignal = signal
|
|
201
|
+
? AbortSignal.any([signal, logcatAbortController.signal])
|
|
202
|
+
: logcatAbortController.signal;
|
|
203
|
+
const logcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp), {
|
|
204
|
+
signal: logcatSignal,
|
|
205
|
+
});
|
|
206
|
+
// Aborting is nano-spawn's own cancellation path, so the resulting
|
|
207
|
+
// SubprocessError is settled the same way regardless of which of the
|
|
208
|
+
// merged stdout/stderr iterators observes it first. Without this, killing
|
|
209
|
+
// the underlying process directly can make both iterators reject at once,
|
|
210
|
+
// and nano-spawn's internal `Promise.race` only reports one of them,
|
|
211
|
+
// leaving the other an unhandled rejection.
|
|
212
|
+
logcatProcess.catch(() => undefined);
|
|
197
213
|
const crashReporter = createAndroidCrashReporter({
|
|
198
214
|
bundleId,
|
|
199
215
|
crashArtifactWriter,
|
|
@@ -239,7 +255,7 @@ export const createAndroidAppSession = async ({
|
|
|
239
255
|
disposed = true;
|
|
240
256
|
stopPolling = true;
|
|
241
257
|
emitter.clear();
|
|
242
|
-
|
|
258
|
+
logcatAbortController.abort();
|
|
243
259
|
await Promise.allSettled([logTask]);
|
|
244
260
|
throw error;
|
|
245
261
|
}
|
|
@@ -287,7 +303,7 @@ export const createAndroidAppSession = async ({
|
|
|
287
303
|
cancelPendingPollDelay();
|
|
288
304
|
|
|
289
305
|
emitter.clear();
|
|
290
|
-
|
|
306
|
+
logcatAbortController.abort();
|
|
291
307
|
await stopApp();
|
|
292
308
|
await Promise.allSettled([logTask, pollTask]);
|
|
293
309
|
},
|
package/src/avd-config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, readFile } from 'node:fs/promises';
|
|
2
2
|
import type { AndroidSystemImageArch } from './environment.js';
|
|
3
3
|
import type { AndroidEmulator, AndroidEmulatorAVDConfig } from './config.js';
|
|
4
|
+
import { getEmulatorCpuCores } from './emulator-startup.js';
|
|
4
5
|
|
|
5
6
|
export type AvdConfig = {
|
|
6
7
|
imageSysdir1?: string;
|
|
@@ -8,6 +9,7 @@ export type AvdConfig = {
|
|
|
8
9
|
hwDeviceName?: string;
|
|
9
10
|
diskDataPartitionSize?: string;
|
|
10
11
|
vmHeapSize?: string;
|
|
12
|
+
hwCpuNcore?: string;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export type AvdCompatibilityResult =
|
|
@@ -123,6 +125,9 @@ export const parseAvdConfig = (contents: string): AvdConfig => {
|
|
|
123
125
|
case 'vm.heapSize':
|
|
124
126
|
config.vmHeapSize = value;
|
|
125
127
|
break;
|
|
128
|
+
case 'hw.cpu.ncore':
|
|
129
|
+
config.hwCpuNcore = value;
|
|
130
|
+
break;
|
|
126
131
|
default:
|
|
127
132
|
break;
|
|
128
133
|
}
|
|
@@ -235,6 +240,18 @@ export const isAvdCompatible = ({
|
|
|
235
240
|
};
|
|
236
241
|
}
|
|
237
242
|
|
|
243
|
+
if (
|
|
244
|
+
normalizeConfigValue(avdConfig.hwCpuNcore ?? '') !==
|
|
245
|
+
normalizeConfigValue(String(getEmulatorCpuCores()))
|
|
246
|
+
) {
|
|
247
|
+
return {
|
|
248
|
+
compatible: false,
|
|
249
|
+
reason: `CPU core count mismatch: expected ${getEmulatorCpuCores()}, got ${
|
|
250
|
+
avdConfig.hwCpuNcore ?? 'missing'
|
|
251
|
+
}.`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
238
255
|
return { compatible: true };
|
|
239
256
|
};
|
|
240
257
|
|
package/src/emulator-startup.ts
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDeviceCoreBudget,
|
|
3
|
+
logger,
|
|
4
|
+
spawn,
|
|
5
|
+
SubprocessError,
|
|
6
|
+
} from '@react-native-harness/tools';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { getEmulatorBinaryPath } from './environment.js';
|
|
9
|
+
|
|
10
|
+
const emulatorStartupLogger = logger.child('android-emulator-startup');
|
|
11
|
+
|
|
1
12
|
export type EmulatorBootMode =
|
|
2
13
|
| 'default-boot'
|
|
3
14
|
| 'clean-snapshot-generation'
|
|
4
15
|
| 'snapshot-reuse';
|
|
5
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Returns the vCPU count baked into an AVD for this runner.
|
|
19
|
+
*
|
|
20
|
+
* AVD snapshots require an identical hardware configuration to load, so all
|
|
21
|
+
* AVD creation, compatibility, and cache-key callers must use this value on
|
|
22
|
+
* the same host.
|
|
23
|
+
*/
|
|
24
|
+
export const getEmulatorCpuCores = (): number => {
|
|
25
|
+
return getDeviceCoreBudget(os.availableParallelism());
|
|
26
|
+
};
|
|
27
|
+
|
|
6
28
|
const COMMON_EMULATOR_ARGS = [
|
|
7
29
|
'-no-window',
|
|
8
30
|
'-gpu',
|
|
9
31
|
'swiftshader_indirect',
|
|
10
32
|
'-noaudio',
|
|
11
33
|
'-no-boot-anim',
|
|
34
|
+
'-no-metrics',
|
|
12
35
|
] as const;
|
|
13
36
|
|
|
14
37
|
export const getEmulatorStartupArgs = (
|
|
@@ -24,3 +47,54 @@ export const getEmulatorStartupArgs = (
|
|
|
24
47
|
|
|
25
48
|
return [`@${name}`, ...modeArgs, ...COMMON_EMULATOR_ARGS];
|
|
26
49
|
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parses the output of `emulator -accel-check` to determine whether
|
|
53
|
+
* hardware acceleration (KVM on Linux, HVF on macOS) is usable. The
|
|
54
|
+
* emulator prints a message containing "is installed and usable" when
|
|
55
|
+
* acceleration is available.
|
|
56
|
+
*/
|
|
57
|
+
export const isAccelerationUsable = (accelCheckOutput: string): boolean => {
|
|
58
|
+
return accelCheckOutput.includes('is installed and usable');
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Runs `emulator -accel-check` and logs a non-fatal warning when hardware
|
|
63
|
+
* acceleration (KVM/HVF) appears unavailable, in which case the emulator
|
|
64
|
+
* falls back to very slow software emulation. This check must never fail or
|
|
65
|
+
* block emulator startup - any error running it is swallowed and only
|
|
66
|
+
* debug-logged.
|
|
67
|
+
*/
|
|
68
|
+
export const warnIfEmulatorAccelerationUnavailable =
|
|
69
|
+
async (): Promise<void> => {
|
|
70
|
+
let combinedOutput: string;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const emulatorBinaryPath = getEmulatorBinaryPath();
|
|
74
|
+
const { stdout, stderr } = await spawn(emulatorBinaryPath, [
|
|
75
|
+
'-accel-check',
|
|
76
|
+
]);
|
|
77
|
+
combinedOutput = `${stdout}\n${stderr}`;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof SubprocessError) {
|
|
80
|
+
// The check itself ran but exited non-zero - this is how
|
|
81
|
+
// `-accel-check` reports that acceleration is unavailable, so treat
|
|
82
|
+
// its output as a normal (non-fatal) result rather than swallowing it.
|
|
83
|
+
combinedOutput = `${error.stdout}\n${error.stderr}`;
|
|
84
|
+
} else {
|
|
85
|
+
// Failed to even run the check (e.g. binary missing). This must never
|
|
86
|
+
// fail or block emulator startup.
|
|
87
|
+
emulatorStartupLogger.debug(
|
|
88
|
+
'Failed to run "emulator -accel-check": %o',
|
|
89
|
+
error
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isAccelerationUsable(combinedOutput)) {
|
|
96
|
+
logger.warn(
|
|
97
|
+
'Hardware acceleration (KVM/HVF) appears unavailable for the Android emulator. It will fall back to very slow software emulation.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -9,5 +9,6 @@ export {
|
|
|
9
9
|
resolveAvdCachingEnabled,
|
|
10
10
|
} from './avd-config.js';
|
|
11
11
|
export { getHostAndroidSystemImageArch } from './environment.js';
|
|
12
|
+
export { getEmulatorCpuCores } from './emulator-startup.js';
|
|
12
13
|
export { HarnessAppPathError, HarnessEmulatorConfigError } from './errors.js';
|
|
13
14
|
export { getRunTargets } from './targets.js';
|