@react-native-harness/platform-android 1.3.0 → 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 (62) 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__/crash-diagnostics.test.d.ts +2 -0
  9. package/dist/__tests__/crash-diagnostics.test.d.ts.map +1 -0
  10. package/dist/__tests__/crash-diagnostics.test.js +119 -0
  11. package/dist/__tests__/crash-reporter.test.d.ts +2 -0
  12. package/dist/__tests__/crash-reporter.test.d.ts.map +1 -0
  13. package/dist/__tests__/crash-reporter.test.js +150 -0
  14. package/dist/__tests__/emulator-startup.test.js +30 -3
  15. package/dist/__tests__/instance.test.js +139 -24
  16. package/dist/adb.d.ts +7 -1
  17. package/dist/adb.d.ts.map +1 -1
  18. package/dist/adb.js +72 -23
  19. package/dist/app-session.d.ts +26 -0
  20. package/dist/app-session.d.ts.map +1 -0
  21. package/dist/app-session.js +214 -0
  22. package/dist/avd-config.d.ts +1 -0
  23. package/dist/avd-config.d.ts.map +1 -1
  24. package/dist/avd-config.js +11 -0
  25. package/dist/crash-diagnostics.d.ts +40 -0
  26. package/dist/crash-diagnostics.d.ts.map +1 -0
  27. package/dist/crash-diagnostics.js +256 -0
  28. package/dist/crash-reporter.d.ts +12 -0
  29. package/dist/crash-reporter.d.ts.map +1 -0
  30. package/dist/crash-reporter.js +150 -0
  31. package/dist/emulator-startup.d.ts +23 -0
  32. package/dist/emulator-startup.d.ts.map +1 -1
  33. package/dist/emulator-startup.js +58 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/instance.d.ts +1 -1
  38. package/dist/instance.d.ts.map +1 -1
  39. package/dist/instance.js +51 -72
  40. package/dist/runner.d.ts +2 -2
  41. package/dist/runner.d.ts.map +1 -1
  42. package/dist/runner.js +1 -1
  43. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  44. package/package.json +4 -4
  45. package/src/__tests__/adb-wait.test.ts +95 -0
  46. package/src/__tests__/app-session.test.ts +76 -0
  47. package/src/__tests__/avd-config.test.ts +70 -0
  48. package/src/__tests__/crash-diagnostics.test.ts +144 -0
  49. package/src/__tests__/crash-reporter.test.ts +167 -0
  50. package/src/__tests__/emulator-startup.test.ts +53 -6
  51. package/src/__tests__/instance.test.ts +226 -84
  52. package/src/adb.ts +129 -63
  53. package/src/app-session.ts +316 -0
  54. package/src/avd-config.ts +17 -0
  55. package/src/crash-diagnostics.ts +471 -0
  56. package/src/crash-reporter.ts +263 -0
  57. package/src/emulator-startup.ts +74 -0
  58. package/src/index.ts +1 -0
  59. package/src/instance.ts +84 -105
  60. package/src/runner.ts +11 -11
  61. package/test/setup.ts +1 -0
  62. package/src/app-monitor.ts +0 -577
@@ -5,6 +5,23 @@ 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;
10
+ const createLogcatProcess = (lines = []) => {
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), {
14
+ nodeChildProcess: Promise.resolve({
15
+ kill: vi.fn(),
16
+ }),
17
+ [Symbol.asyncIterator]: async function* () {
18
+ for (const line of lines) {
19
+ yield line;
20
+ }
21
+ },
22
+ });
23
+ return process;
24
+ };
8
25
  const harnessConfig = {
9
26
  metroPort: DEFAULT_METRO_PORT,
10
27
  };
@@ -19,7 +36,9 @@ describe('Android platform instance', () => {
19
36
  beforeEach(async () => {
20
37
  vi.restoreAllMocks();
21
38
  vi.unstubAllEnvs();
39
+ vi.useRealTimers();
22
40
  vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorAvailable').mockResolvedValue('/tmp/android-sdk');
41
+ vi.spyOn(emulatorStartup, 'warnIfEmulatorAccelerationUnavailable').mockResolvedValue(undefined);
23
42
  });
24
43
  it('reuses a running emulator and does not shut it down on dispose', async () => {
25
44
  const ensureAndroidEmulatorEnvironment = vi
@@ -32,6 +51,7 @@ describe('Android platform instance', () => {
32
51
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
33
52
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
34
53
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
54
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
35
55
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
36
56
  vi.spyOn(sharedPrefs, 'clearHarnessDebugHttpHost').mockResolvedValue(undefined);
37
57
  vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
@@ -68,6 +88,7 @@ describe('Android platform instance', () => {
68
88
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
69
89
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
70
90
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
91
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
71
92
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
72
93
  vi.spyOn(sharedPrefs, 'clearHarnessDebugHttpHost').mockResolvedValue(undefined);
73
94
  vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
@@ -113,6 +134,7 @@ describe('Android platform instance', () => {
113
134
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
114
135
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
115
136
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
137
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
116
138
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
117
139
  await expect(getAndroidEmulatorPlatformInstance({
118
140
  name: 'android',
@@ -145,6 +167,7 @@ describe('Android platform instance', () => {
145
167
  hwDeviceName: 'pixel_8',
146
168
  diskDataPartitionSize: '1G',
147
169
  vmHeapSize: '1G',
170
+ hwCpuNcore: String(getEmulatorCpuCores()),
148
171
  });
149
172
  vi.spyOn(adb, 'startEmulator').mockResolvedValue(undefined);
150
173
  vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
@@ -152,6 +175,7 @@ describe('Android platform instance', () => {
152
175
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
153
176
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
154
177
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
178
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
155
179
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
156
180
  await expect(getAndroidEmulatorPlatformInstance({
157
181
  name: 'android',
@@ -196,6 +220,7 @@ describe('Android platform instance', () => {
196
220
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
197
221
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
198
222
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
223
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
199
224
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
200
225
  await expect(getAndroidEmulatorPlatformInstance({
201
226
  name: 'android',
@@ -238,6 +263,7 @@ describe('Android platform instance', () => {
238
263
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
239
264
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
240
265
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
266
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
241
267
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
242
268
  await expect(getAndroidEmulatorPlatformInstance({
243
269
  name: 'android',
@@ -315,7 +341,8 @@ describe('Android platform instance', () => {
315
341
  activityName: '.MainActivity',
316
342
  }, harnessConfig, init)).rejects.toBeInstanceOf(HarnessEmulatorConfigError);
317
343
  });
318
- it('returns a noop emulator app monitor when native crash detection is disabled', async () => {
344
+ it('creates a real emulator app session when native crash detection is disabled', async () => {
345
+ vi.useFakeTimers();
319
346
  vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
320
347
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
321
348
  vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
@@ -324,7 +351,14 @@ describe('Android platform instance', () => {
324
351
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
325
352
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
326
353
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
354
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
327
355
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
356
+ const stopApp = vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
357
+ const startApp = vi.spyOn(adb, 'startApp').mockResolvedValue(undefined);
358
+ const getAppPid = vi.spyOn(adb, 'getAppPid').mockResolvedValue(4321);
359
+ const startLogcat = vi
360
+ .spyOn(adb, 'startLogcat')
361
+ .mockReturnValue(createLogcatProcess());
328
362
  const instance = await getAndroidEmulatorPlatformInstance({
329
363
  name: 'android',
330
364
  device: {
@@ -341,14 +375,84 @@ describe('Android platform instance', () => {
341
375
  activityName: '.MainActivity',
342
376
  }, harnessConfigWithoutNativeCrashDetection, init);
343
377
  const listener = vi.fn();
344
- const appMonitor = instance.createAppMonitor();
345
- await expect(appMonitor.start()).resolves.toBeUndefined();
346
- await expect(appMonitor.stop()).resolves.toBeUndefined();
347
- await expect(appMonitor.dispose()).resolves.toBeUndefined();
348
- expect(appMonitor.addListener(listener)).toBeUndefined();
349
- expect(appMonitor.removeListener(listener)).toBeUndefined();
378
+ const appSession = await instance.createAppSession();
379
+ appSession.addListener(listener);
380
+ await vi.advanceTimersByTimeAsync(0);
381
+ expect(startLogcat).toHaveBeenCalledWith('emulator-5554', [
382
+ 'logcat',
383
+ '-v',
384
+ 'threadtime',
385
+ '-b',
386
+ 'crash',
387
+ '--uid=10234',
388
+ '-T',
389
+ '01-01 00:00:00.000',
390
+ ], { signal: expect.any(AbortSignal) });
391
+ expect(startLogcat.mock.invocationCallOrder[0]).toBeLessThan(startApp.mock.invocationCallOrder[0]);
392
+ await expect(appSession.getState()).resolves.toEqual({
393
+ status: 'running',
394
+ pid: 4321,
395
+ });
396
+ expect(getAppPid).toHaveBeenCalled();
397
+ expect(listener).not.toHaveBeenCalled();
398
+ expect(appSession.removeListener(listener)).toBeUndefined();
399
+ await expect(appSession.dispose()).resolves.toBeUndefined();
400
+ expect(stopApp).toHaveBeenCalled();
350
401
  });
351
- it('returns a noop physical device app monitor when native crash detection is disabled', async () => {
402
+ it('reports an early Android crash from logcat before a PID poll succeeds', async () => {
403
+ vi.useFakeTimers();
404
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
405
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
406
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
407
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
408
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
409
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
410
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
411
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
412
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
413
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
414
+ vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
415
+ vi.spyOn(adb, 'startApp').mockResolvedValue(undefined);
416
+ vi.spyOn(adb, 'getAppPid').mockResolvedValue(null);
417
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createLogcatProcess([
418
+ '--------- beginning of crash',
419
+ 'Process: com.harnessplayground, PID: 7777',
420
+ 'FATAL EXCEPTION: main',
421
+ ]));
422
+ const instance = await getAndroidEmulatorPlatformInstance({
423
+ name: 'android',
424
+ device: {
425
+ type: 'emulator',
426
+ name: 'Pixel_8_API_35',
427
+ avd: {
428
+ apiLevel: 35,
429
+ profile: 'pixel_8',
430
+ diskSize: '1G',
431
+ heapSize: '1G',
432
+ },
433
+ },
434
+ bundleId: 'com.harnessplayground',
435
+ activityName: '.MainActivity',
436
+ }, harnessConfig, init);
437
+ const listener = vi.fn();
438
+ const appSession = await instance.createAppSession();
439
+ appSession.addListener(listener);
440
+ await vi.advanceTimersByTimeAsync(0);
441
+ expect(appSession.getLogs().map((entry) => entry.line)).toEqual([
442
+ '--------- beginning of crash',
443
+ 'Process: com.harnessplayground, PID: 7777',
444
+ 'FATAL EXCEPTION: main',
445
+ ]);
446
+ await expect(appSession.getState()).resolves.toMatchObject({
447
+ status: 'exited',
448
+ pid: 7777,
449
+ reason: 'observed-exit',
450
+ });
451
+ expect(listener).toHaveBeenCalledWith({ type: 'app_exited' });
452
+ await appSession.dispose();
453
+ });
454
+ it('creates a real physical-device app session when native crash detection is disabled', async () => {
455
+ vi.useFakeTimers();
352
456
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
353
457
  vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
354
458
  manufacturer: 'motorola',
@@ -358,17 +462,14 @@ describe('Android platform instance', () => {
358
462
  vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
359
463
  vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
360
464
  vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
465
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('01-01 00:00:00.000');
361
466
  vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
362
- await expect(getAndroidPhysicalDevicePlatformInstance({
363
- name: 'android-device',
364
- device: {
365
- type: 'physical',
366
- manufacturer: 'motorola',
367
- model: 'moto g72',
368
- },
369
- bundleId: 'com.harnessplayground',
370
- activityName: '.MainActivity',
371
- }, harnessConfigWithoutNativeCrashDetection)).resolves.toBeDefined();
467
+ vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
468
+ vi.spyOn(adb, 'startApp').mockResolvedValue(undefined);
469
+ vi.spyOn(adb, 'getAppPid').mockResolvedValue(8765);
470
+ const startLogcat = vi
471
+ .spyOn(adb, 'startLogcat')
472
+ .mockReturnValue(createLogcatProcess());
372
473
  const instance = await getAndroidPhysicalDevicePlatformInstance({
373
474
  name: 'android-device',
374
475
  device: {
@@ -380,12 +481,26 @@ describe('Android platform instance', () => {
380
481
  activityName: '.MainActivity',
381
482
  }, harnessConfigWithoutNativeCrashDetection);
382
483
  const listener = vi.fn();
383
- const appMonitor = instance.createAppMonitor();
384
- await expect(appMonitor.start()).resolves.toBeUndefined();
385
- await expect(appMonitor.stop()).resolves.toBeUndefined();
386
- await expect(appMonitor.dispose()).resolves.toBeUndefined();
387
- expect(appMonitor.addListener(listener)).toBeUndefined();
388
- expect(appMonitor.removeListener(listener)).toBeUndefined();
484
+ const appSession = await instance.createAppSession();
485
+ appSession.addListener(listener);
486
+ await vi.advanceTimersByTimeAsync(0);
487
+ expect(startLogcat).toHaveBeenCalledWith('012345', [
488
+ 'logcat',
489
+ '-v',
490
+ 'threadtime',
491
+ '-b',
492
+ 'crash',
493
+ '--uid=10234',
494
+ '-T',
495
+ '01-01 00:00:00.000',
496
+ ], { signal: expect.any(AbortSignal) });
497
+ await expect(appSession.getState()).resolves.toEqual({
498
+ status: 'running',
499
+ pid: 8765,
500
+ });
501
+ expect(listener).not.toHaveBeenCalled();
502
+ expect(appSession.removeListener(listener)).toBeUndefined();
503
+ await expect(appSession.dispose()).resolves.toBeUndefined();
389
504
  });
390
505
  it('grants permissions when permissions are enabled for emulator', async () => {
391
506
  vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
package/dist/adb.d.ts CHANGED
@@ -40,10 +40,16 @@ export declare const waitForEmulator: (name: string, signal: AbortSignal) => Pro
40
40
  export declare const waitForEmulatorDisconnect: (adbId: string, signal: AbortSignal) => Promise<void>;
41
41
  export declare const waitForBoot: (name: string, signal: AbortSignal) => Promise<string>;
42
42
  export declare const isAppRunning: (adbId: string, bundleId: string) => Promise<boolean>;
43
+ export declare const getAppPid: (adbId: string, bundleId: string) => Promise<number | null>;
43
44
  export declare const getAppUid: (adbId: string, bundleId: string) => Promise<number>;
44
45
  export declare const setHideErrorDialogs: (adbId: string, hide: boolean) => Promise<void>;
45
46
  export declare const getLogcatTimestamp: (adbId: string) => Promise<string>;
46
- 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;
50
+ export declare const DROPBOX_CRASH_TAGS: readonly ["data_app_crash", "data_app_native_crash"];
51
+ export declare const getDropboxPrint: (adbId: string, tags?: readonly string[]) => Promise<string>;
52
+ export declare const getActivityExitInfo: (adbId: string, bundleId: string) => Promise<string>;
47
53
  export declare const getAvds: () => Promise<string[]>;
48
54
  export type AdbDevice = {
49
55
  id: 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();
@@ -432,6 +440,9 @@ export const waitForBoot = async (name, signal) => {
432
440
  throw signal.reason;
433
441
  };
434
442
  export const isAppRunning = async (adbId, bundleId) => {
443
+ return (await getAppPid(adbId, bundleId)) != null;
444
+ };
445
+ export const getAppPid = async (adbId, bundleId) => {
435
446
  try {
436
447
  const { stdout } = await spawn(getAdbBinaryPath(), [
437
448
  '-s',
@@ -440,11 +451,15 @@ export const isAppRunning = async (adbId, bundleId) => {
440
451
  'pidof',
441
452
  bundleId,
442
453
  ]);
443
- return stdout.trim() !== '';
454
+ const pid = stdout
455
+ .trim()
456
+ .split(/\s+/)
457
+ .find((value) => value !== '');
458
+ return pid ? Number(pid) : null;
444
459
  }
445
460
  catch (error) {
446
461
  if (error instanceof SubprocessError && error.exitCode === 1) {
447
- return false;
462
+ return null;
448
463
  }
449
464
  throw error;
450
465
  }
@@ -490,10 +505,44 @@ export const getLogcatTimestamp = async (adbId) => {
490
505
  ]);
491
506
  return stdout.trim().replace(/^'+|'+$/g, '');
492
507
  };
493
- export const startLogcat = (adbId, args) => spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
508
+ export const startLogcat = (adbId, args, options) => spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
494
509
  stdout: 'pipe',
495
510
  stderr: 'pipe',
511
+ signal: options?.signal,
496
512
  });
513
+ export const DROPBOX_CRASH_TAGS = [
514
+ 'data_app_crash',
515
+ 'data_app_native_crash',
516
+ ];
517
+ export const getDropboxPrint = async (adbId, tags = DROPBOX_CRASH_TAGS) => {
518
+ // Android treats multiple args after --print as one search string, so each
519
+ // tag must be queried separately and merged on the host.
520
+ const outputs = await Promise.all(tags.map(async (tag) => {
521
+ const { stdout } = await spawn(getAdbBinaryPath(), [
522
+ '-s',
523
+ adbId,
524
+ 'shell',
525
+ 'dumpsys',
526
+ 'dropbox',
527
+ '--print',
528
+ tag,
529
+ ]);
530
+ return stdout;
531
+ }));
532
+ return outputs.join('\n');
533
+ };
534
+ export const getActivityExitInfo = async (adbId, bundleId) => {
535
+ const { stdout } = await spawn(getAdbBinaryPath(), [
536
+ '-s',
537
+ adbId,
538
+ 'shell',
539
+ 'dumpsys',
540
+ 'activity',
541
+ 'exit-info',
542
+ bundleId,
543
+ ]);
544
+ return stdout;
545
+ };
497
546
  export const getAvds = async () => {
498
547
  try {
499
548
  const emulatorBinaryPath = await ensureEmulatorInstalled();
@@ -0,0 +1,26 @@
1
+ import { type AppSession, type CrashArtifactWriter } from '@react-native-harness/platforms';
2
+ import { type Subprocess } from '@react-native-harness/tools';
3
+ type CreateAndroidAppSessionOptions = {
4
+ appUid: number;
5
+ bundleId: string;
6
+ startApp: () => Promise<void>;
7
+ stopApp: () => Promise<void>;
8
+ getAppPid: () => Promise<number | null>;
9
+ getLogcatTimestamp: () => Promise<string>;
10
+ startLogcat: (args: readonly string[], options: {
11
+ signal: AbortSignal;
12
+ }) => Subprocess;
13
+ getDropboxOutput?: () => Promise<string>;
14
+ getExitInfo?: () => Promise<string>;
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;
23
+ };
24
+ export declare const createAndroidAppSession: ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, signal, }: CreateAndroidAppSessionOptions) => Promise<AppSession>;
25
+ export {};
26
+ //# sourceMappingURL=app-session.d.ts.map
@@ -0,0 +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;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"}