@react-native-harness/platform-android 1.4.0-rc.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/__tests__/adb-wait.test.d.ts +2 -0
  2. package/dist/__tests__/adb-wait.test.d.ts.map +1 -0
  3. package/dist/__tests__/adb-wait.test.js +69 -0
  4. package/dist/__tests__/app-session.test.d.ts +2 -0
  5. package/dist/__tests__/app-session.test.d.ts.map +1 -0
  6. package/dist/__tests__/app-session.test.js +57 -0
  7. package/dist/__tests__/avd-config.test.js +58 -0
  8. package/dist/__tests__/emulator-startup.test.js +30 -3
  9. package/dist/__tests__/instance.test.js +10 -4
  10. package/dist/adb.d.ts +3 -1
  11. package/dist/adb.d.ts.map +1 -1
  12. package/dist/adb.js +30 -21
  13. package/dist/app-session.d.ts +11 -2
  14. package/dist/app-session.d.ts.map +1 -1
  15. package/dist/app-session.js +17 -12
  16. package/dist/avd-config.d.ts +1 -0
  17. package/dist/avd-config.d.ts.map +1 -1
  18. package/dist/avd-config.js +11 -0
  19. package/dist/emulator-startup.d.ts +23 -0
  20. package/dist/emulator-startup.d.ts.map +1 -1
  21. package/dist/emulator-startup.js +58 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/instance.d.ts.map +1 -1
  26. package/dist/instance.js +21 -15
  27. package/dist/runner.d.ts +2 -2
  28. package/dist/runner.d.ts.map +1 -1
  29. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  30. package/package.json +4 -4
  31. package/src/__tests__/adb-wait.test.ts +95 -0
  32. package/src/__tests__/app-session.test.ts +76 -0
  33. package/src/__tests__/avd-config.test.ts +70 -0
  34. package/src/__tests__/emulator-startup.test.ts +53 -6
  35. package/src/__tests__/instance.test.ts +107 -108
  36. package/src/adb.ts +72 -65
  37. package/src/app-session.ts +28 -12
  38. package/src/avd-config.ts +17 -0
  39. package/src/emulator-startup.ts +74 -0
  40. package/src/index.ts +1 -0
  41. package/src/instance.ts +30 -16
  42. package/src/runner.ts +5 -9
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adb-wait.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adb-wait.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adb-wait.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ const mocks = vi.hoisted(() => ({
3
+ spawn: vi.fn(),
4
+ }));
5
+ // The global test/setup.ts mocks '../adb.js' wholesale (including
6
+ // waitForEmulator itself), which is right for consumers of this module but
7
+ // defeats the purpose here: this suite exercises the *real* polling loop
8
+ // (and its private waitWithSignal helper) to catch timer/listener leaks.
9
+ // Re-declaring the mock for this file restores the actual implementation,
10
+ // while `spawn` and `getAdbBinaryPath` (genuine cross-module imports) are
11
+ // swapped out to keep `getDeviceIds()` from touching a real adb binary.
12
+ vi.mock('../adb.js', async () => {
13
+ const actual = await vi.importActual('../adb.js');
14
+ return actual;
15
+ });
16
+ vi.mock('../environment.js', async () => {
17
+ const actual = await vi.importActual('../environment.js');
18
+ return { ...actual, getAdbBinaryPath: () => '/fake/sdk/platform-tools/adb' };
19
+ });
20
+ vi.mock('@react-native-harness/tools', async () => {
21
+ const actual = await vi.importActual('@react-native-harness/tools');
22
+ return { ...actual, spawn: mocks.spawn };
23
+ });
24
+ const { waitForEmulator } = await import('../adb.js');
25
+ afterEach(() => {
26
+ vi.useRealTimers();
27
+ vi.restoreAllMocks();
28
+ });
29
+ describe('waitForEmulator polling', () => {
30
+ it('does not leak an abort listener per poll iteration when the delay wins', async () => {
31
+ vi.useFakeTimers();
32
+ // No matching device on any poll: `adb devices` reports an empty list.
33
+ mocks.spawn.mockResolvedValue({ stdout: 'List of devices attached\n' });
34
+ const controller = new AbortController();
35
+ const addSpy = vi.spyOn(controller.signal, 'addEventListener');
36
+ const removeSpy = vi.spyOn(controller.signal, 'removeEventListener');
37
+ const resultPromise = waitForEmulator('Test_AVD', controller.signal);
38
+ resultPromise.catch(() => undefined);
39
+ for (let i = 0; i < 3; i += 1) {
40
+ await vi.advanceTimersByTimeAsync(1_000);
41
+ }
42
+ // Regression: each 1s poll iteration used to add an 'abort' listener
43
+ // to the local waitForAbort helper that was never removed when the
44
+ // delay (not the abort) won the race, leaking one listener per second
45
+ // of polling against this session-lifetime signal.
46
+ expect(addSpy.mock.calls.length).toBeGreaterThanOrEqual(3);
47
+ const addCallsBeforeAbort = addSpy.mock.calls.length;
48
+ controller.abort(new Error('teardown'));
49
+ await expect(resultPromise).rejects.toThrow('teardown');
50
+ // Every listener added while losing the race — including the one still
51
+ // pending when the abort itself won — must be removed again.
52
+ expect(removeSpy).toHaveBeenCalledTimes(addCallsBeforeAbort);
53
+ });
54
+ it('clears the pending 1s poll timer when the signal aborts mid-wait', async () => {
55
+ vi.useFakeTimers();
56
+ mocks.spawn.mockResolvedValue({ stdout: 'List of devices attached\n' });
57
+ const controller = new AbortController();
58
+ const resultPromise = waitForEmulator('Test_AVD', controller.signal);
59
+ resultPromise.catch(() => undefined);
60
+ // Let the loop enter its 1s wait, then abort before the timer fires.
61
+ await vi.advanceTimersByTimeAsync(0);
62
+ controller.abort(new Error('teardown'));
63
+ await expect(resultPromise).rejects.toThrow('teardown');
64
+ // Regression: the bare setTimeout-backed `wait()` used to have no
65
+ // handle to clear, so it stayed alive in the event loop for the rest
66
+ // of the 1s interval even after the abort branch won.
67
+ expect(vi.getTimerCount()).toBe(0);
68
+ });
69
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=app-session.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-session.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/app-session.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createAndroidAppSession } from '../app-session.js';
3
+ // Mimics nano-spawn: the async iterator only ends once its abort signal
4
+ // fires, matching how startLogcat's real subprocess reacts to abort.
5
+ const createAbortableLogcatProcess = (signal) => ({
6
+ [Symbol.asyncIterator]: () => ({
7
+ next: () => new Promise((resolve) => {
8
+ if (signal.aborted) {
9
+ resolve({ done: true, value: undefined });
10
+ return;
11
+ }
12
+ signal.addEventListener('abort', () => resolve({ done: true, value: undefined }), { once: true });
13
+ }),
14
+ }),
15
+ catch: () => undefined,
16
+ });
17
+ describe('createAndroidAppSession', () => {
18
+ it('combines the session-lifetime signal with its own logcat abort controller', async () => {
19
+ let capturedSignal;
20
+ const controller = new AbortController();
21
+ const session = await createAndroidAppSession({
22
+ appUid: 1,
23
+ bundleId: 'com.example',
24
+ startApp: async () => undefined,
25
+ stopApp: async () => undefined,
26
+ getAppPid: async () => null,
27
+ getLogcatTimestamp: async () => '00:00:00.000',
28
+ startLogcat: (_args, options) => {
29
+ capturedSignal = options.signal;
30
+ return createAbortableLogcatProcess(options.signal);
31
+ },
32
+ signal: controller.signal,
33
+ });
34
+ expect(capturedSignal?.aborted).toBe(false);
35
+ controller.abort();
36
+ expect(capturedSignal?.aborted).toBe(true);
37
+ await session.dispose();
38
+ });
39
+ it('still aborts the logcat signal via dispose() with no external signal provided', async () => {
40
+ let capturedSignal;
41
+ const session = await createAndroidAppSession({
42
+ appUid: 1,
43
+ bundleId: 'com.example',
44
+ startApp: async () => undefined,
45
+ stopApp: async () => undefined,
46
+ getAppPid: async () => null,
47
+ getLogcatTimestamp: async () => '00:00:00.000',
48
+ startLogcat: (_args, options) => {
49
+ capturedSignal = options.signal;
50
+ return createAbortableLogcatProcess(options.signal);
51
+ },
52
+ });
53
+ expect(capturedSignal?.aborted).toBe(false);
54
+ await session.dispose();
55
+ expect(capturedSignal?.aborted).toBe(true);
56
+ });
57
+ });
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { getNormalizedAvdCacheConfig, isAvdCompatible, parseAvdConfig, resolveAvdCachingEnabled, } from '../avd-config.js';
3
3
  import { AndroidPlatformConfigSchema } from '../config.js';
4
+ import { getEmulatorCpuCores } from '../emulator-startup.js';
5
+ const emulatorCpuCores = getEmulatorCpuCores();
4
6
  describe('AVD config helpers', () => {
5
7
  it('parses snapshot config from Android schema', () => {
6
8
  const config = AndroidPlatformConfigSchema.parse({
@@ -59,7 +61,9 @@ abi.type=x86_64
59
61
  hw.device.name=pixel_8
60
62
  disk.dataPartition.size=1G
61
63
  vm.heapSize=512M
64
+ hw.cpu.ncore=${emulatorCpuCores}
62
65
  `);
66
+ expect(avdConfig.hwCpuNcore).toBe(String(emulatorCpuCores));
63
67
  expect(isAvdCompatible({
64
68
  emulator: {
65
69
  type: 'emulator',
@@ -75,6 +79,59 @@ vm.heapSize=512M
75
79
  hostArch: 'x86_64',
76
80
  })).toEqual({ compatible: true });
77
81
  });
82
+ it('rejects AVDs missing hw.cpu.ncore', () => {
83
+ const avdConfig = parseAvdConfig(`
84
+ image.sysdir.1=system-images/android-35/default/x86_64/
85
+ abi.type=x86_64
86
+ hw.device.name=pixel_8
87
+ disk.dataPartition.size=1G
88
+ vm.heapSize=512M
89
+ `);
90
+ expect(isAvdCompatible({
91
+ emulator: {
92
+ type: 'emulator',
93
+ name: 'Pixel_8_API_35',
94
+ avd: {
95
+ apiLevel: 35,
96
+ profile: 'pixel_8',
97
+ diskSize: '1G',
98
+ heapSize: '512M',
99
+ },
100
+ },
101
+ avdConfig,
102
+ hostArch: 'x86_64',
103
+ })).toMatchObject({
104
+ compatible: false,
105
+ reason: `CPU core count mismatch: expected ${emulatorCpuCores}, got missing.`,
106
+ });
107
+ });
108
+ it('rejects AVDs with a different hw.cpu.ncore', () => {
109
+ const avdConfig = parseAvdConfig(`
110
+ image.sysdir.1=system-images/android-35/default/x86_64/
111
+ abi.type=x86_64
112
+ hw.device.name=pixel_8
113
+ disk.dataPartition.size=1G
114
+ vm.heapSize=512M
115
+ hw.cpu.ncore=${emulatorCpuCores === 4 ? 3 : 4}
116
+ `);
117
+ expect(isAvdCompatible({
118
+ emulator: {
119
+ type: 'emulator',
120
+ name: 'Pixel_8_API_35',
121
+ avd: {
122
+ apiLevel: 35,
123
+ profile: 'pixel_8',
124
+ diskSize: '1G',
125
+ heapSize: '512M',
126
+ },
127
+ },
128
+ avdConfig,
129
+ hostArch: 'x86_64',
130
+ })).toMatchObject({
131
+ compatible: false,
132
+ reason: `CPU core count mismatch: expected ${emulatorCpuCores}, got ${emulatorCpuCores === 4 ? 3 : 4}.`,
133
+ });
134
+ });
78
135
  it('accepts disk partition sizes rewritten to bytes', () => {
79
136
  const avdConfig = parseAvdConfig(`
80
137
  image.sysdir.1=system-images/android-35/default/x86_64/
@@ -82,6 +139,7 @@ abi.type=x86_64
82
139
  hw.device.name=pixel_8
83
140
  disk.dataPartition.size=6442450944
84
141
  vm.heapSize=512M
142
+ hw.cpu.ncore=${emulatorCpuCores}
85
143
  `);
86
144
  expect(isAvdCompatible({
87
145
  emulator: {
@@ -1,20 +1,47 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { getEmulatorStartupArgs } from '../emulator-startup.js';
2
+ import { getEmulatorCpuCores, getEmulatorStartupArgs, isAccelerationUsable, } from '../emulator-startup.js';
3
+ import { getDeviceCoreBudget } from '@react-native-harness/tools';
4
+ import os from 'node:os';
5
+ describe('getEmulatorCpuCores', () => {
6
+ it('uses the shared device-core budget for the current host', () => {
7
+ expect(getEmulatorCpuCores()).toBe(getDeviceCoreBudget(os.availableParallelism()));
8
+ });
9
+ });
3
10
  describe('emulator startup modes', () => {
4
11
  it('builds default boot args', () => {
5
12
  expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).toEqual(expect.arrayContaining([
6
13
  '@Pixel_8_API_35',
7
14
  '-no-snapshot-load',
8
15
  '-no-snapshot-save',
16
+ '-no-metrics',
9
17
  ]));
10
18
  expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).not.toEqual(expect.arrayContaining(['-camera-back', 'none']));
11
19
  });
12
20
  it('builds clean snapshot generation args', () => {
13
- expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).toEqual(expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-load']));
21
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).toEqual(expect.arrayContaining([
22
+ '@Pixel_8_API_35',
23
+ '-no-snapshot-load',
24
+ '-no-metrics',
25
+ ]));
14
26
  expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).not.toContain('-no-snapshot-save');
15
27
  });
16
28
  it('builds snapshot reuse args', () => {
17
- expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).toEqual(expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-save']));
29
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).toEqual(expect.arrayContaining([
30
+ '@Pixel_8_API_35',
31
+ '-no-snapshot-save',
32
+ '-no-metrics',
33
+ ]));
18
34
  expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).not.toContain('-no-snapshot-load');
19
35
  });
20
36
  });
37
+ describe('isAccelerationUsable', () => {
38
+ it('returns true when the emulator reports acceleration is usable', () => {
39
+ expect(isAccelerationUsable('accel:\n0\nKVM (version 12) is installed and usable.\naccel\n')).toBe(true);
40
+ });
41
+ it('returns false when acceleration is disabled', () => {
42
+ expect(isAccelerationUsable('accel:\n0\nKVM is not installed on this machine.\naccel\n')).toBe(false);
43
+ });
44
+ it('returns false for empty output', () => {
45
+ expect(isAccelerationUsable('')).toBe(false);
46
+ });
47
+ });
@@ -5,8 +5,12 @@ import * as adb from '../adb.js';
5
5
  import * as avdConfig from '../avd-config.js';
6
6
  import * as sharedPrefs from '../shared-prefs.js';
7
7
  import { HarnessAppPathError, HarnessEmulatorConfigError } from '../errors.js';
8
+ import * as emulatorStartup from '../emulator-startup.js';
9
+ const { getEmulatorCpuCores } = emulatorStartup;
8
10
  const createLogcatProcess = (lines = []) => {
9
- const process = {
11
+ // Mirrors nano-spawn's real Subprocess, which is both a Promise and an
12
+ // async iterable, since app-session.ts relies on it being thenable.
13
+ const process = Object.assign(Promise.resolve(undefined), {
10
14
  nodeChildProcess: Promise.resolve({
11
15
  kill: vi.fn(),
12
16
  }),
@@ -15,7 +19,7 @@ const createLogcatProcess = (lines = []) => {
15
19
  yield line;
16
20
  }
17
21
  },
18
- };
22
+ });
19
23
  return process;
20
24
  };
21
25
  const harnessConfig = {
@@ -34,6 +38,7 @@ describe('Android platform instance', () => {
34
38
  vi.unstubAllEnvs();
35
39
  vi.useRealTimers();
36
40
  vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorAvailable').mockResolvedValue('/tmp/android-sdk');
41
+ vi.spyOn(emulatorStartup, 'warnIfEmulatorAccelerationUnavailable').mockResolvedValue(undefined);
37
42
  });
38
43
  it('reuses a running emulator and does not shut it down on dispose', async () => {
39
44
  const ensureAndroidEmulatorEnvironment = vi
@@ -162,6 +167,7 @@ describe('Android platform instance', () => {
162
167
  hwDeviceName: 'pixel_8',
163
168
  diskDataPartitionSize: '1G',
164
169
  vmHeapSize: '1G',
170
+ hwCpuNcore: String(getEmulatorCpuCores()),
165
171
  });
166
172
  vi.spyOn(adb, 'startEmulator').mockResolvedValue(undefined);
167
173
  vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
@@ -381,7 +387,7 @@ describe('Android platform instance', () => {
381
387
  '--uid=10234',
382
388
  '-T',
383
389
  '01-01 00:00:00.000',
384
- ]);
390
+ ], { signal: expect.any(AbortSignal) });
385
391
  expect(startLogcat.mock.invocationCallOrder[0]).toBeLessThan(startApp.mock.invocationCallOrder[0]);
386
392
  await expect(appSession.getState()).resolves.toEqual({
387
393
  status: 'running',
@@ -487,7 +493,7 @@ describe('Android platform instance', () => {
487
493
  '--uid=10234',
488
494
  '-T',
489
495
  '01-01 00:00:00.000',
490
- ]);
496
+ ], { signal: expect.any(AbortSignal) });
491
497
  await expect(appSession.getState()).resolves.toEqual({
492
498
  status: 'running',
493
499
  pid: 8765,
package/dist/adb.d.ts CHANGED
@@ -44,7 +44,9 @@ export declare const getAppPid: (adbId: string, bundleId: string) => Promise<num
44
44
  export declare const getAppUid: (adbId: string, bundleId: string) => Promise<number>;
45
45
  export declare const setHideErrorDialogs: (adbId: string, hide: boolean) => Promise<void>;
46
46
  export declare const getLogcatTimestamp: (adbId: string) => Promise<string>;
47
- export declare const startLogcat: (adbId: string, args: readonly string[]) => Subprocess;
47
+ export declare const startLogcat: (adbId: string, args: readonly string[], options?: {
48
+ signal?: AbortSignal;
49
+ }) => Subprocess;
48
50
  export declare const DROPBOX_CRASH_TAGS: readonly ["data_app_crash", "data_app_native_crash"];
49
51
  export declare const getDropboxPrint: (adbId: string, tags?: readonly string[]) => Promise<string>;
50
52
  export declare const getActivityExitInfo: (adbId: string, bundleId: string) => Promise<string>;
package/dist/adb.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
package/dist/adb.js CHANGED
@@ -1,30 +1,27 @@
1
- import { logger, spawn, SubprocessError, } from '@react-native-harness/tools';
1
+ import { delay, logger, spawn, SubprocessError, waitForAbort, } from '@react-native-harness/tools';
2
2
  import { spawn as nodeSpawn } from 'node:child_process';
3
3
  import { access, rm } from 'node:fs/promises';
4
4
  import { getAvdConfigPath, getAvdDirectory, readAvdConfig, } from './avd-config.js';
5
5
  import { ensureAndroidEmulatorAvailable, ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, getAvdManagerBinaryPath, getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from './environment.js';
6
- import { getEmulatorStartupArgs, } from './emulator-startup.js';
6
+ import { getEmulatorCpuCores, getEmulatorStartupArgs, } from './emulator-startup.js';
7
7
  import { AdbAppNotInstalledError, AdbPermissionGrantError, } from './adb-errors.js';
8
- const wait = async (ms) => {
9
- await new Promise((resolve) => {
10
- setTimeout(resolve, ms);
11
- });
12
- };
13
- const waitForAbort = (signal) => {
14
- if (signal.aborted) {
15
- return Promise.reject(signal.reason);
16
- }
17
- return new Promise((_, reject) => {
18
- signal.addEventListener('abort', () => {
19
- reject(signal.reason);
20
- }, { once: true });
21
- });
22
- };
23
8
  const waitWithSignal = async (ms, signal) => {
24
9
  if (signal.aborted) {
25
10
  throw signal.reason;
26
11
  }
27
- await Promise.race([wait(ms), waitForAbort(signal)]);
12
+ // Both sides of this race must be torn down explicitly when they lose:
13
+ // an uncancelled timer keeps a ref'd Timeout alive, and an uncancelled
14
+ // abort wait leaks a listener on `signal`, which is a session-lifetime
15
+ // signal here and gets polled against once per second.
16
+ const timer = delay(ms);
17
+ const abort = waitForAbort(signal);
18
+ try {
19
+ await Promise.race([timer.promise, abort.promise]);
20
+ }
21
+ finally {
22
+ timer.cancel();
23
+ abort.cancel();
24
+ }
28
25
  };
29
26
  const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
30
27
  const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
@@ -311,9 +308,14 @@ export const createAvd = async ({ name, apiLevel, profile, diskSize, heapSize, }
311
308
  ]);
312
309
  await ensureAvdIniExists({ name, apiLevel });
313
310
  const configPath = await ensureAvdConfigExists(name);
311
+ // hw.cpu.ncore is baked into config.ini (rather than passed as a `-cores`
312
+ // boot flag) so it's part of the AVD's persisted hardware profile: boot
313
+ // snapshots require an identical hardware config to load, so the vCPU
314
+ // count must be fixed at AVD-creation time and stay consistent across
315
+ // boots.
314
316
  await spawn('bash', [
315
317
  '-lc',
316
- `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${configPath}"`,
318
+ `printf '%s\n%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' 'hw.cpu.ncore=${getEmulatorCpuCores()}' >> "${configPath}"`,
317
319
  ]);
318
320
  };
319
321
  export const deleteAvd = async (name) => {
@@ -375,11 +377,17 @@ export const startEmulator = async (name, mode = 'default-boot') => {
375
377
  }
376
378
  throw error;
377
379
  });
378
- const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then(() => 'timeout');
380
+ const observationTimer = delay(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS);
381
+ const observationTimeout = observationTimer.promise.then(() => 'timeout');
379
382
  try {
380
383
  await Promise.race([earlyExit, observedBoot, observationTimeout]);
381
384
  }
382
385
  finally {
386
+ // If `earlyExit` or `observedBoot` won the race, this timer would
387
+ // otherwise sit ref'd in the event loop for the rest of its 5s;
388
+ // cancelling here is a no-op on the happy "proceed detached" path
389
+ // where the timer itself won.
390
+ observationTimer.cancel();
383
391
  cleanup();
384
392
  }
385
393
  childProcess.stdout?.destroy();
@@ -497,9 +505,10 @@ export const getLogcatTimestamp = async (adbId) => {
497
505
  ]);
498
506
  return stdout.trim().replace(/^'+|'+$/g, '');
499
507
  };
500
- export const startLogcat = (adbId, args) => spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
508
+ export const startLogcat = (adbId, args, options) => spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
501
509
  stdout: 'pipe',
502
510
  stderr: 'pipe',
511
+ signal: options?.signal,
503
512
  });
504
513
  export const DROPBOX_CRASH_TAGS = [
505
514
  'data_app_crash',
@@ -7,11 +7,20 @@ type CreateAndroidAppSessionOptions = {
7
7
  stopApp: () => Promise<void>;
8
8
  getAppPid: () => Promise<number | null>;
9
9
  getLogcatTimestamp: () => Promise<string>;
10
- startLogcat: (args: readonly string[]) => Subprocess;
10
+ startLogcat: (args: readonly string[], options: {
11
+ signal: AbortSignal;
12
+ }) => Subprocess;
11
13
  getDropboxOutput?: () => Promise<string>;
12
14
  getExitInfo?: () => Promise<string>;
13
15
  crashArtifactWriter?: CrashArtifactWriter;
16
+ /**
17
+ * Session-lifetime abort signal (see HarnessPlatformInitOptions.signal).
18
+ * Combined with the app session's own logcatAbortController so that
19
+ * aborting the session stops the logcat stream even if dispose() hasn't
20
+ * been called yet (e.g. SIGTERM during teardown of other resources).
21
+ */
22
+ signal?: AbortSignal;
14
23
  };
15
- export declare const createAndroidAppSession: ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, }: CreateAndroidAppSessionOptions) => Promise<AppSession>;
24
+ export declare const createAndroidAppSession: ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, signal, }: CreateAndroidAppSessionOptions) => Promise<AppSession>;
16
25
  export {};
17
26
  //# sourceMappingURL=app-session.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-session.d.ts","sourceRoot":"","sources":["../src/app-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,6BAA6B,CAAC;AAkErC,KAAK,8BAA8B,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,kBAAkB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,UAAU,CAAC;IACrD,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAU,0IAW3C,8BAA8B,KAAG,OAAO,CAAC,UAAU,CAsMrD,CAAC"}
1
+ {"version":3,"file":"app-session.d.ts","sourceRoot":"","sources":["../src/app-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,6BAA6B,CAAC;AA0DrC,KAAK,8BAA8B,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,kBAAkB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,WAAW,EAAE,CACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;QAAE,MAAM,EAAE,WAAW,CAAA;KAAE,KAC7B,UAAU,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAU,kJAY3C,8BAA8B,KAAG,OAAO,CAAC,UAAU,CAmNrD,CAAC"}
@@ -30,16 +30,8 @@ const isCrashSignal = (line, bundleId) => {
30
30
  (line.includes(bundleId) &&
31
31
  /fatal|crash|signal 11|signal 6|backtrace/i.test(line)));
32
32
  };
33
- const stopSubprocess = async (child) => {
34
- try {
35
- (await child.nodeChildProcess).kill();
36
- }
37
- catch {
38
- // Ignore termination failures for already-ended background processes.
39
- }
40
- };
41
33
  const isExitedState = (state) => state.status === 'exited';
42
- export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, }) => {
34
+ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, signal, }) => {
43
35
  const emitter = createAppSessionEmitter();
44
36
  const logBuffer = createBoundedLogBuffer();
45
37
  let state = { status: 'running' };
@@ -113,7 +105,20 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
113
105
  };
114
106
  const logcatTimestamp = await getLogcatTimestamp();
115
107
  const sessionStartedAt = Date.now();
116
- const logcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp));
108
+ const logcatAbortController = new AbortController();
109
+ const logcatSignal = signal
110
+ ? AbortSignal.any([signal, logcatAbortController.signal])
111
+ : logcatAbortController.signal;
112
+ const logcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp), {
113
+ signal: logcatSignal,
114
+ });
115
+ // Aborting is nano-spawn's own cancellation path, so the resulting
116
+ // SubprocessError is settled the same way regardless of which of the
117
+ // merged stdout/stderr iterators observes it first. Without this, killing
118
+ // the underlying process directly can make both iterators reject at once,
119
+ // and nano-spawn's internal `Promise.race` only reports one of them,
120
+ // leaving the other an unhandled rejection.
121
+ logcatProcess.catch(() => undefined);
117
122
  const crashReporter = createAndroidCrashReporter({
118
123
  bundleId,
119
124
  crashArtifactWriter,
@@ -156,7 +161,7 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
156
161
  disposed = true;
157
162
  stopPolling = true;
158
163
  emitter.clear();
159
- await stopSubprocess(logcatProcess);
164
+ logcatAbortController.abort();
160
165
  await Promise.allSettled([logTask]);
161
166
  throw error;
162
167
  }
@@ -196,7 +201,7 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
196
201
  }
197
202
  cancelPendingPollDelay();
198
203
  emitter.clear();
199
- await stopSubprocess(logcatProcess);
204
+ logcatAbortController.abort();
200
205
  await stopApp();
201
206
  await Promise.allSettled([logTask, pollTask]);
202
207
  },
@@ -6,6 +6,7 @@ export type AvdConfig = {
6
6
  hwDeviceName?: string;
7
7
  diskDataPartitionSize?: string;
8
8
  vmHeapSize?: string;
9
+ hwCpuNcore?: string;
9
10
  };
10
11
  export type AvdCompatibilityResult = {
11
12
  compatible: true;
@@ -1 +1 @@
1
- {"version":3,"file":"avd-config.d.ts","sourceRoot":"","sources":["../src/avd-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE7E,MAAM,MAAM,SAAS,GAAG;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,UAAU,EAAE,IAAI,CAAA;CAAE,GACpB;IAAE,UAAU,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAI9C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAE/C,CAAC;AAkEF,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,KAAG,SAyCjD,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,KACX,OAAO,CAAC,SAAS,GAAG,IAAI,CAU1B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,oCAI7B;IACD,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,sBAmFH,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,yBAGzC;IACD,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG;IACF,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,sBAAsB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAeH,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,8BAItC;IACD,GAAG,CAAC,EAAE,wBAAwB,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,KAAG,OAWH,CAAC"}
1
+ {"version":3,"file":"avd-config.d.ts","sourceRoot":"","sources":["../src/avd-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAG7E,MAAM,MAAM,SAAS,GAAG;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,UAAU,EAAE,IAAI,CAAA;CAAE,GACpB;IAAE,UAAU,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAI9C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAE/C,CAAC;AAkEF,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,KAAG,SA4CjD,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,KACX,OAAO,CAAC,SAAS,GAAG,IAAI,CAU1B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,oCAI7B;IACD,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,sBA+FH,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,yBAGzC;IACD,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG;IACF,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,sBAAsB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAeH,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,8BAItC;IACD,GAAG,CAAC,EAAE,wBAAwB,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,KAAG,OAWH,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { access, readFile } from 'node:fs/promises';
2
+ import { getEmulatorCpuCores } from './emulator-startup.js';
2
3
  export const getAvdDirectory = (name) => {
3
4
  return `${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd`;
4
5
  };
@@ -81,6 +82,9 @@ export const parseAvdConfig = (contents) => {
81
82
  case 'vm.heapSize':
82
83
  config.vmHeapSize = value;
83
84
  break;
85
+ case 'hw.cpu.ncore':
86
+ config.hwCpuNcore = value;
87
+ break;
84
88
  default:
85
89
  break;
86
90
  }
@@ -146,6 +150,13 @@ export const isAvdCompatible = ({ emulator, avdConfig, hostArch, }) => {
146
150
  reason: `Heap size mismatch: expected ${requestedAvdConfig.heapSize}, got ${avdConfig.vmHeapSize ?? 'missing'}.`,
147
151
  };
148
152
  }
153
+ if (normalizeConfigValue(avdConfig.hwCpuNcore ?? '') !==
154
+ normalizeConfigValue(String(getEmulatorCpuCores()))) {
155
+ return {
156
+ compatible: false,
157
+ reason: `CPU core count mismatch: expected ${getEmulatorCpuCores()}, got ${avdConfig.hwCpuNcore ?? 'missing'}.`,
158
+ };
159
+ }
149
160
  return { compatible: true };
150
161
  };
151
162
  export const getNormalizedAvdCacheConfig = ({ emulator, hostArch, }) => {
@@ -1,3 +1,26 @@
1
1
  export type EmulatorBootMode = 'default-boot' | 'clean-snapshot-generation' | 'snapshot-reuse';
2
+ /**
3
+ * Returns the vCPU count baked into an AVD for this runner.
4
+ *
5
+ * AVD snapshots require an identical hardware configuration to load, so all
6
+ * AVD creation, compatibility, and cache-key callers must use this value on
7
+ * the same host.
8
+ */
9
+ export declare const getEmulatorCpuCores: () => number;
2
10
  export declare const getEmulatorStartupArgs: (name: string, mode: EmulatorBootMode) => string[];
11
+ /**
12
+ * Parses the output of `emulator -accel-check` to determine whether
13
+ * hardware acceleration (KVM on Linux, HVF on macOS) is usable. The
14
+ * emulator prints a message containing "is installed and usable" when
15
+ * acceleration is available.
16
+ */
17
+ export declare const isAccelerationUsable: (accelCheckOutput: string) => boolean;
18
+ /**
19
+ * Runs `emulator -accel-check` and logs a non-fatal warning when hardware
20
+ * acceleration (KVM/HVF) appears unavailable, in which case the emulator
21
+ * falls back to very slow software emulation. This check must never fail or
22
+ * block emulator startup - any error running it is swallowed and only
23
+ * debug-logged.
24
+ */
25
+ export declare const warnIfEmulatorAccelerationUnavailable: () => Promise<void>;
3
26
  //# sourceMappingURL=emulator-startup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"emulator-startup.d.ts","sourceRoot":"","sources":["../src/emulator-startup.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,2BAA2B,GAC3B,gBAAgB,CAAC;AAUrB,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,EACZ,MAAM,gBAAgB,KACrB,MAAM,EASR,CAAC"}
1
+ {"version":3,"file":"emulator-startup.d.ts","sourceRoot":"","sources":["../src/emulator-startup.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,2BAA2B,GAC3B,gBAAgB,CAAC;AAErB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAO,MAEtC,CAAC;AAWF,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,EACZ,MAAM,gBAAgB,KACrB,MAAM,EASR,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,kBAAkB,MAAM,KAAG,OAE/D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qCAAqC,QACtC,OAAO,CAAC,IAAI,CA+BrB,CAAC"}