@react-native-harness/platform-android 1.1.0-rc.2 → 1.1.0-rc.3

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 (83) hide show
  1. package/README.md +9 -2
  2. package/dist/__tests__/adb.test.js +283 -10
  3. package/dist/__tests__/avd-config.test.d.ts +2 -0
  4. package/dist/__tests__/avd-config.test.d.ts.map +1 -0
  5. package/dist/__tests__/avd-config.test.js +174 -0
  6. package/dist/__tests__/ci-action.test.d.ts +2 -0
  7. package/dist/__tests__/ci-action.test.d.ts.map +1 -0
  8. package/dist/__tests__/ci-action.test.js +46 -0
  9. package/dist/__tests__/emulator-startup.test.d.ts +2 -0
  10. package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
  11. package/dist/__tests__/emulator-startup.test.js +19 -0
  12. package/dist/__tests__/environment.test.d.ts +2 -0
  13. package/dist/__tests__/environment.test.d.ts.map +1 -0
  14. package/dist/__tests__/environment.test.js +51 -0
  15. package/dist/__tests__/instance.test.d.ts +2 -0
  16. package/dist/__tests__/instance.test.d.ts.map +1 -0
  17. package/dist/__tests__/instance.test.js +423 -0
  18. package/dist/adb.d.ts +23 -0
  19. package/dist/adb.d.ts.map +1 -1
  20. package/dist/adb.js +265 -16
  21. package/dist/app-monitor.d.ts.map +1 -1
  22. package/dist/app-monitor.js +27 -7
  23. package/dist/assertions.d.ts +5 -0
  24. package/dist/assertions.d.ts.map +1 -0
  25. package/dist/assertions.js +6 -0
  26. package/dist/avd-config.d.ts +41 -0
  27. package/dist/avd-config.d.ts.map +1 -0
  28. package/dist/avd-config.js +173 -0
  29. package/dist/config.d.ts +77 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +5 -0
  32. package/dist/emulator-startup.d.ts +3 -0
  33. package/dist/emulator-startup.d.ts.map +1 -0
  34. package/dist/emulator-startup.js +17 -0
  35. package/dist/emulator.d.ts +6 -0
  36. package/dist/emulator.d.ts.map +1 -0
  37. package/dist/emulator.js +27 -0
  38. package/dist/environment.d.ts +28 -0
  39. package/dist/environment.d.ts.map +1 -0
  40. package/dist/environment.js +295 -0
  41. package/dist/errors.d.ts +7 -0
  42. package/dist/errors.d.ts.map +1 -0
  43. package/dist/errors.js +14 -0
  44. package/dist/factory.d.ts.map +1 -1
  45. package/dist/factory.js +3 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +3 -0
  49. package/dist/instance.d.ts +6 -0
  50. package/dist/instance.d.ts.map +1 -0
  51. package/dist/instance.js +234 -0
  52. package/dist/reader.d.ts +6 -0
  53. package/dist/reader.d.ts.map +1 -0
  54. package/dist/reader.js +57 -0
  55. package/dist/runner.d.ts +2 -2
  56. package/dist/runner.d.ts.map +1 -1
  57. package/dist/runner.js +12 -52
  58. package/dist/targets.d.ts +1 -1
  59. package/dist/targets.d.ts.map +1 -1
  60. package/dist/targets.js +2 -0
  61. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  62. package/dist/types.d.ts +381 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +107 -0
  65. package/package.json +4 -4
  66. package/src/__tests__/adb.test.ts +419 -15
  67. package/src/__tests__/avd-config.test.ts +206 -0
  68. package/src/__tests__/ci-action.test.ts +81 -0
  69. package/src/__tests__/emulator-startup.test.ts +32 -0
  70. package/src/__tests__/environment.test.ts +87 -0
  71. package/src/__tests__/instance.test.ts +610 -0
  72. package/src/adb.ts +423 -16
  73. package/src/app-monitor.ts +56 -18
  74. package/src/avd-config.ts +290 -0
  75. package/src/config.ts +8 -0
  76. package/src/emulator-startup.ts +28 -0
  77. package/src/environment.ts +510 -0
  78. package/src/errors.ts +19 -0
  79. package/src/factory.ts +4 -0
  80. package/src/index.ts +7 -1
  81. package/src/instance.ts +380 -0
  82. package/src/runner.ts +23 -69
  83. package/src/targets.ts +11 -8
package/src/adb.ts CHANGED
@@ -1,5 +1,169 @@
1
1
  import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
2
2
  import { spawn, SubprocessError } from '@react-native-harness/tools';
3
+ import { spawn as nodeSpawn } from 'node:child_process';
4
+ import type { ChildProcessByStdio } from 'node:child_process';
5
+ import { access, rm } from 'node:fs/promises';
6
+ import type { Readable } from 'node:stream';
7
+ import {
8
+ ensureAndroidSdkPackages,
9
+ getAdbBinaryPath,
10
+ getAndroidSystemImagePackage,
11
+ getAvdManagerBinaryPath,
12
+ getEmulatorBinaryPath,
13
+ getHostAndroidSystemImageArch,
14
+ getRequiredAndroidSdkPackages,
15
+ getSdkManagerBinaryPath,
16
+ } from './environment.js';
17
+ import {
18
+ getEmulatorStartupArgs,
19
+ type EmulatorBootMode,
20
+ } from './emulator-startup.js';
21
+
22
+ const wait = async (ms: number): Promise<void> => {
23
+ await new Promise((resolve) => {
24
+ setTimeout(resolve, ms);
25
+ });
26
+ };
27
+
28
+ const waitForAbort = (signal: AbortSignal): Promise<never> => {
29
+ if (signal.aborted) {
30
+ return Promise.reject(signal.reason);
31
+ }
32
+
33
+ return new Promise((_, reject) => {
34
+ signal.addEventListener(
35
+ 'abort',
36
+ () => {
37
+ reject(signal.reason);
38
+ },
39
+ { once: true }
40
+ );
41
+ });
42
+ };
43
+
44
+ const waitWithSignal = async (
45
+ ms: number,
46
+ signal: AbortSignal
47
+ ): Promise<void> => {
48
+ if (signal.aborted) {
49
+ throw signal.reason;
50
+ }
51
+
52
+ await Promise.race([wait(ms), waitForAbort(signal)]);
53
+ };
54
+
55
+ const getAvdConfigPath = (name: string): string =>
56
+ `${
57
+ process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
58
+ }/${name}.avd/config.ini`;
59
+
60
+ const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
61
+ const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
62
+
63
+ export const emulatorProcess = {
64
+ startDetachedProcess: (
65
+ file: string,
66
+ args: readonly string[]
67
+ ): ChildProcessByStdio<null, Readable, Readable> =>
68
+ nodeSpawn(file, args, {
69
+ detached: true,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ }),
72
+ };
73
+
74
+ const appendBoundedOutput = (
75
+ output: string,
76
+ chunk: string,
77
+ limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT
78
+ ): string => {
79
+ const nextOutput = output + chunk;
80
+
81
+ if (nextOutput.length <= limit) {
82
+ return nextOutput;
83
+ }
84
+
85
+ return nextOutput.slice(-limit);
86
+ };
87
+
88
+ const formatEmulatorStartupError = ({
89
+ name,
90
+ stdout,
91
+ stderr,
92
+ exitCode,
93
+ signal,
94
+ error,
95
+ }: {
96
+ name: string;
97
+ stdout: string;
98
+ stderr: string;
99
+ exitCode?: number | null;
100
+ signal?: NodeJS.Signals | null;
101
+ error?: unknown;
102
+ }): Error => {
103
+ const sections = [`Failed to start Android emulator @${name}.`];
104
+
105
+ if (typeof exitCode === 'number') {
106
+ sections.push(`Exit code: ${exitCode}`);
107
+ }
108
+
109
+ if (signal) {
110
+ sections.push(`Signal: ${signal}`);
111
+ }
112
+
113
+ if (error instanceof Error) {
114
+ sections.push(`Cause: ${error.message}`);
115
+ }
116
+
117
+ const trimmedStdout = stdout.trim();
118
+ const trimmedStderr = stderr.trim();
119
+
120
+ if (trimmedStdout !== '') {
121
+ sections.push(`stdout:\n${trimmedStdout}`);
122
+ }
123
+
124
+ if (trimmedStderr !== '') {
125
+ sections.push(`stderr:\n${trimmedStderr}`);
126
+ }
127
+
128
+ return new Error(sections.join('\n\n'), {
129
+ cause: error instanceof Error ? error : undefined,
130
+ });
131
+ };
132
+
133
+ const ensureEmulatorInstalled = async (): Promise<string> => {
134
+ const emulatorBinaryPath = getEmulatorBinaryPath();
135
+
136
+ try {
137
+ await access(emulatorBinaryPath);
138
+ return emulatorBinaryPath;
139
+ } catch {
140
+ await spawn(getSdkManagerBinaryPath(), ['emulator']);
141
+ await access(emulatorBinaryPath);
142
+ return emulatorBinaryPath;
143
+ }
144
+ };
145
+
146
+ export type CreateAvdOptions = {
147
+ name: string;
148
+ apiLevel: number;
149
+ profile: string;
150
+ diskSize: string;
151
+ heapSize: string;
152
+ };
153
+
154
+ export const getRequiredEmulatorPackages = (apiLevel: number): string[] => {
155
+ return getRequiredAndroidSdkPackages({
156
+ apiLevel,
157
+ includeEmulator: true,
158
+ architecture: getHostAndroidSystemImageArch(),
159
+ });
160
+ };
161
+
162
+ export const verifyAndroidEmulatorSdk = async (
163
+ apiLevel: number
164
+ ): Promise<void> => {
165
+ await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
166
+ };
3
167
 
4
168
  export const getStartAppArgs = (
5
169
  bundleId: string,
@@ -47,7 +211,7 @@ export const isAppInstalled = async (
47
211
  adbId: string,
48
212
  bundleId: string
49
213
  ): Promise<boolean> => {
50
- const { stdout } = await spawn('adb', [
214
+ const { stdout } = await spawn(getAdbBinaryPath(), [
51
215
  '-s',
52
216
  adbId,
53
217
  'shell',
@@ -64,7 +228,7 @@ export const reversePort = async (
64
228
  port: number,
65
229
  hostPort: number = port
66
230
  ): Promise<void> => {
67
- await spawn('adb', [
231
+ await spawn(getAdbBinaryPath(), [
68
232
  '-s',
69
233
  adbId,
70
234
  'reverse',
@@ -77,7 +241,14 @@ export const stopApp = async (
77
241
  adbId: string,
78
242
  bundleId: string
79
243
  ): Promise<void> => {
80
- await spawn('adb', ['-s', adbId, 'shell', 'am', 'force-stop', bundleId]);
244
+ await spawn(getAdbBinaryPath(), [
245
+ '-s',
246
+ adbId,
247
+ 'shell',
248
+ 'am',
249
+ 'force-stop',
250
+ bundleId,
251
+ ]);
81
252
  };
82
253
 
83
254
  export const startApp = async (
@@ -86,11 +257,15 @@ export const startApp = async (
86
257
  activityName: string,
87
258
  options?: AndroidAppLaunchOptions
88
259
  ): Promise<void> => {
89
- await spawn('adb', ['-s', adbId, ...getStartAppArgs(bundleId, activityName, options)]);
260
+ await spawn(getAdbBinaryPath(), [
261
+ '-s',
262
+ adbId,
263
+ ...getStartAppArgs(bundleId, activityName, options),
264
+ ]);
90
265
  };
91
266
 
92
267
  export const getDeviceIds = async (): Promise<string[]> => {
93
- const { stdout } = await spawn('adb', ['devices']);
268
+ const { stdout } = await spawn(getAdbBinaryPath(), ['devices']);
94
269
  return stdout
95
270
  .split('\n')
96
271
  .slice(1) // Skip header
@@ -101,7 +276,13 @@ export const getDeviceIds = async (): Promise<string[]> => {
101
276
  export const getEmulatorName = async (
102
277
  adbId: string
103
278
  ): Promise<string | null> => {
104
- const { stdout } = await spawn('adb', ['-s', adbId, 'emu', 'avd', 'name']);
279
+ const { stdout } = await spawn(getAdbBinaryPath(), [
280
+ '-s',
281
+ adbId,
282
+ 'emu',
283
+ 'avd',
284
+ 'name',
285
+ ]);
105
286
  return stdout.split('\n')[0].trim() || null;
106
287
  };
107
288
 
@@ -109,7 +290,7 @@ export const getShellProperty = async (
109
290
  adbId: string,
110
291
  property: string
111
292
  ): Promise<string | null> => {
112
- const { stdout } = await spawn('adb', [
293
+ const { stdout } = await spawn(getAdbBinaryPath(), [
113
294
  '-s',
114
295
  adbId,
115
296
  'shell',
@@ -119,6 +300,10 @@ export const getShellProperty = async (
119
300
  return stdout.trim() || null;
120
301
  };
121
302
 
303
+ const isTransientAdbShellFailure = (error: unknown): boolean => {
304
+ return error instanceof SubprocessError && error.exitCode === 1;
305
+ };
306
+
122
307
  export type DeviceInfo = {
123
308
  manufacturer: string | null;
124
309
  model: string | null;
@@ -133,12 +318,233 @@ export const getDeviceInfo = async (
133
318
  };
134
319
 
135
320
  export const isBootCompleted = async (adbId: string): Promise<boolean> => {
136
- const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
137
- return bootCompleted === '1';
321
+ try {
322
+ const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
323
+ return bootCompleted === '1';
324
+ } catch (error) {
325
+ if (isTransientAdbShellFailure(error)) {
326
+ return false;
327
+ }
328
+
329
+ throw error;
330
+ }
138
331
  };
139
332
 
140
333
  export const stopEmulator = async (adbId: string): Promise<void> => {
141
- await spawn('adb', ['-s', adbId, 'emu', 'kill']);
334
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'emu', 'kill']);
335
+ };
336
+
337
+ export const installApp = async (
338
+ adbId: string,
339
+ appPath: string
340
+ ): Promise<void> => {
341
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
342
+ };
343
+
344
+ export const hasAvd = async (name: string): Promise<boolean> => {
345
+ const avds = await getAvds();
346
+ return avds.includes(name);
347
+ };
348
+
349
+ export const createAvd = async ({
350
+ name,
351
+ apiLevel,
352
+ profile,
353
+ diskSize,
354
+ heapSize,
355
+ }: CreateAvdOptions): Promise<void> => {
356
+ const systemImagePackage = getAndroidSystemImagePackage(
357
+ apiLevel,
358
+ getHostAndroidSystemImageArch()
359
+ );
360
+
361
+ await verifyAndroidEmulatorSdk(apiLevel);
362
+ await spawn('bash', [
363
+ '-lc',
364
+ `printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}"`,
365
+ ]);
366
+ await spawn('bash', [
367
+ '-lc',
368
+ `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${getAvdConfigPath(
369
+ name
370
+ )}"`,
371
+ ]);
372
+ };
373
+
374
+ export const deleteAvd = async (name: string): Promise<void> => {
375
+ await rm(
376
+ `${
377
+ process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
378
+ }/${name}.avd`,
379
+ {
380
+ force: true,
381
+ recursive: true,
382
+ }
383
+ );
384
+ await rm(
385
+ `${
386
+ process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
387
+ }/${name}.ini`,
388
+ {
389
+ force: true,
390
+ }
391
+ );
392
+ };
393
+
394
+ export const startEmulator = async (
395
+ name: string,
396
+ mode: EmulatorBootMode = 'default-boot'
397
+ ): Promise<void> => {
398
+ const emulatorBinaryPath = await ensureEmulatorInstalled();
399
+ const childProcess = emulatorProcess.startDetachedProcess(
400
+ emulatorBinaryPath,
401
+ getEmulatorStartupArgs(name, mode)
402
+ );
403
+
404
+ let stdout = '';
405
+ let stderr = '';
406
+
407
+ childProcess.stdout?.setEncoding('utf8');
408
+ childProcess.stderr?.setEncoding('utf8');
409
+
410
+ const onStdout = (chunk: string | Buffer) => {
411
+ stdout = appendBoundedOutput(stdout, chunk.toString());
412
+ };
413
+ const onStderr = (chunk: string | Buffer) => {
414
+ stderr = appendBoundedOutput(stderr, chunk.toString());
415
+ };
416
+
417
+ childProcess.stdout?.on('data', onStdout);
418
+ childProcess.stderr?.on('data', onStderr);
419
+
420
+ const startupAbortController = new AbortController();
421
+ const cleanup = () => {
422
+ startupAbortController.abort();
423
+ childProcess.stdout?.off('data', onStdout);
424
+ childProcess.stderr?.off('data', onStderr);
425
+ childProcess.removeAllListeners('error');
426
+ childProcess.removeAllListeners('close');
427
+ };
428
+
429
+ const earlyExit = new Promise<never>((_, reject) => {
430
+ childProcess.once('error', (error) => {
431
+ reject(
432
+ formatEmulatorStartupError({
433
+ name,
434
+ stdout,
435
+ stderr,
436
+ error,
437
+ })
438
+ );
439
+ });
440
+
441
+ childProcess.once('close', (exitCode, signal) => {
442
+ reject(
443
+ formatEmulatorStartupError({
444
+ name,
445
+ stdout,
446
+ stderr,
447
+ exitCode,
448
+ signal,
449
+ })
450
+ );
451
+ });
452
+ });
453
+
454
+ const observedBoot = waitForEmulator(name, startupAbortController.signal)
455
+ .then(() => 'booted' as const)
456
+ .catch((error: unknown) => {
457
+ if (startupAbortController.signal.aborted) {
458
+ return 'aborted' as const;
459
+ }
460
+
461
+ throw error;
462
+ });
463
+
464
+ const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then(
465
+ () => 'timeout' as const
466
+ );
467
+
468
+ try {
469
+ await Promise.race([earlyExit, observedBoot, observationTimeout]);
470
+ } finally {
471
+ cleanup();
472
+ }
473
+
474
+ childProcess.stdout?.destroy();
475
+ childProcess.stderr?.destroy();
476
+ childProcess.unref();
477
+ };
478
+
479
+ export const waitForEmulator = async (
480
+ name: string,
481
+ signal: AbortSignal
482
+ ): Promise<string> => {
483
+ while (!signal.aborted) {
484
+ const adbIds = await getDeviceIds();
485
+
486
+ for (const adbId of adbIds) {
487
+ if (!adbId.startsWith('emulator-')) {
488
+ continue;
489
+ }
490
+
491
+ const emulatorName = await getEmulatorName(adbId);
492
+
493
+ if (emulatorName === name) {
494
+ return adbId;
495
+ }
496
+ }
497
+
498
+ await waitWithSignal(1000, signal);
499
+ }
500
+
501
+ throw signal.reason;
502
+ };
503
+
504
+ export const waitForEmulatorDisconnect = async (
505
+ adbId: string,
506
+ signal: AbortSignal
507
+ ): Promise<void> => {
508
+ while (!signal.aborted) {
509
+ const adbIds = await getDeviceIds();
510
+
511
+ if (!adbIds.includes(adbId)) {
512
+ return;
513
+ }
514
+
515
+ await waitWithSignal(1000, signal);
516
+ }
517
+
518
+ throw signal.reason;
519
+ };
520
+
521
+ export const waitForBoot = async (
522
+ name: string,
523
+ signal: AbortSignal
524
+ ): Promise<string> => {
525
+ while (!signal.aborted) {
526
+ const adbIds = await getDeviceIds();
527
+
528
+ for (const adbId of adbIds) {
529
+ if (!adbId.startsWith('emulator-')) {
530
+ continue;
531
+ }
532
+
533
+ const emulatorName = await getEmulatorName(adbId);
534
+
535
+ if (emulatorName !== name) {
536
+ continue;
537
+ }
538
+
539
+ if (await isBootCompleted(adbId)) {
540
+ return adbId;
541
+ }
542
+ }
543
+
544
+ await waitWithSignal(1000, signal);
545
+ }
546
+
547
+ throw signal.reason;
142
548
  };
143
549
 
144
550
  export const isAppRunning = async (
@@ -146,7 +552,7 @@ export const isAppRunning = async (
146
552
  bundleId: string
147
553
  ): Promise<boolean> => {
148
554
  try {
149
- const { stdout } = await spawn('adb', [
555
+ const { stdout } = await spawn(getAdbBinaryPath(), [
150
556
  '-s',
151
557
  adbId,
152
558
  'shell',
@@ -167,7 +573,7 @@ export const getAppUid = async (
167
573
  adbId: string,
168
574
  bundleId: string
169
575
  ): Promise<number> => {
170
- const { stdout } = await spawn('adb', [
576
+ const { stdout } = await spawn(getAdbBinaryPath(), [
171
577
  '-s',
172
578
  adbId,
173
579
  'shell',
@@ -192,7 +598,7 @@ export const setHideErrorDialogs = async (
192
598
  adbId: string,
193
599
  hide: boolean
194
600
  ): Promise<void> => {
195
- await spawn('adb', [
601
+ await spawn(getAdbBinaryPath(), [
196
602
  '-s',
197
603
  adbId,
198
604
  'shell',
@@ -205,7 +611,7 @@ export const setHideErrorDialogs = async (
205
611
  };
206
612
 
207
613
  export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
208
- const { stdout } = await spawn('adb', [
614
+ const { stdout } = await spawn(getAdbBinaryPath(), [
209
615
  '-s',
210
616
  adbId,
211
617
  'shell',
@@ -218,7 +624,8 @@ export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
218
624
 
219
625
  export const getAvds = async (): Promise<string[]> => {
220
626
  try {
221
- const { stdout } = await spawn('emulator', ['-list-avds']);
627
+ const emulatorBinaryPath = await ensureEmulatorInstalled();
628
+ const { stdout } = await spawn(emulatorBinaryPath, ['-list-avds']);
222
629
  return stdout
223
630
  .split('\n')
224
631
  .map((line) => line.trim())
@@ -235,7 +642,7 @@ export type AdbDevice = {
235
642
  };
236
643
 
237
644
  export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
238
- const { stdout } = await spawn('adb', ['devices', '-l']);
645
+ const { stdout } = await spawn(getAdbBinaryPath(), ['devices', '-l']);
239
646
  const lines = stdout.split('\n').slice(1);
240
647
  const devices: AdbDevice[] = [];
241
648
 
@@ -6,14 +6,30 @@ import {
6
6
  type AppMonitorEvent,
7
7
  type AppMonitorListener,
8
8
  } from '@react-native-harness/platforms';
9
- import { escapeRegExp, getEmitter, logger, spawn, SubprocessError, type Subprocess } from '@react-native-harness/tools';
9
+ import {
10
+ escapeRegExp,
11
+ getEmitter,
12
+ logger,
13
+ spawn,
14
+ SubprocessError,
15
+ type Subprocess,
16
+ } from '@react-native-harness/tools';
10
17
  import * as adb from './adb.js';
11
18
  import { androidCrashParser } from './crash-parser.js';
12
19
 
13
20
  const androidAppMonitorLogger = logger.child('android-app-monitor');
14
21
 
15
22
  const getLogcatArgs = (uid: number, fromTime: string) =>
16
- ['logcat', '-v', 'threadtime', '-b', 'crash', `--uid=${uid}`, '-T', fromTime] as const;
23
+ [
24
+ 'logcat',
25
+ '-v',
26
+ 'threadtime',
27
+ '-b',
28
+ 'crash',
29
+ `--uid=${uid}`,
30
+ '-T',
31
+ fromTime,
32
+ ] as const;
17
33
  const MAX_RECENT_LOG_LINES = 200;
18
34
  const MAX_RECENT_CRASH_ARTIFACTS = 10;
19
35
  const CRASH_ARTIFACT_SETTLE_DELAY_MS = 100;
@@ -29,7 +45,9 @@ const nativeCrashPattern = (bundleId: string) =>
29
45
 
30
46
  const processDiedPattern = (bundleId: string) =>
31
47
  new RegExp(
32
- `Process\\s+${escapeRegExp(bundleId)}\\s+\\(pid\\s+(\\d+)\\)\\s+has\\s+died`,
48
+ `Process\\s+${escapeRegExp(
49
+ bundleId
50
+ )}\\s+\\(pid\\s+(\\d+)\\)\\s+has\\s+died`,
33
51
  'i'
34
52
  );
35
53
 
@@ -66,7 +84,11 @@ const getAndroidLogLineCrashDetails = ({
66
84
  summary: line.trim(),
67
85
  signal: getSignal(line),
68
86
  exceptionType: fatalExceptionMatch?.[1]?.trim(),
69
- processName: processMatch ? bundleId : line.includes(bundleId) ? bundleId : undefined,
87
+ processName: processMatch
88
+ ? bundleId
89
+ : line.includes(bundleId)
90
+ ? bundleId
91
+ : undefined,
70
92
  pid: pid ?? (processMatch ? Number(processMatch[1]) : undefined),
71
93
  rawLines: [line],
72
94
  };
@@ -211,7 +233,9 @@ const createCrashArtifact = ({
211
233
  triggerOccurredAt,
212
234
  artifactType: 'logcat',
213
235
  rawLines:
214
- rawLines.length > 0 ? rawLines : parsedDetails.rawLines ?? details.rawLines,
236
+ rawLines.length > 0
237
+ ? rawLines
238
+ : parsedDetails.rawLines ?? details.rawLines,
215
239
  };
216
240
  };
217
241
 
@@ -265,11 +289,12 @@ const getLatestCrashArtifact = ({
265
289
  matchingByPid.length > 0
266
290
  ? matchingByPid
267
291
  : matchingByProcess.length > 0
268
- ? matchingByProcess
269
- : crashArtifacts;
292
+ ? matchingByProcess
293
+ : crashArtifacts;
270
294
  const sortedCandidates = [...candidates].sort(
271
295
  (left, right) =>
272
- Math.abs(left.occurredAt - occurredAt) - Math.abs(right.occurredAt - occurredAt)
296
+ Math.abs(left.occurredAt - occurredAt) -
297
+ Math.abs(right.occurredAt - occurredAt)
273
298
  );
274
299
 
275
300
  const artifact = sortedCandidates[0];
@@ -385,9 +410,10 @@ export const createAndroidAppMonitor = ({
385
410
  };
386
411
 
387
412
  const recordLogLine = (line: string) => {
388
- recentLogLines = [...recentLogLines, { line, occurredAt: Date.now() }].slice(
389
- -MAX_RECENT_LOG_LINES
390
- );
413
+ recentLogLines = [
414
+ ...recentLogLines,
415
+ { line, occurredAt: Date.now() },
416
+ ].slice(-MAX_RECENT_LOG_LINES);
391
417
  };
392
418
 
393
419
  const recordCrashArtifact = (details?: AppCrashDetails) => {
@@ -419,10 +445,14 @@ export const createAndroidAppMonitor = ({
419
445
  const startLogcat = async () => {
420
446
  const logcatTimestamp = await adb.getLogcatTimestamp(adbId);
421
447
 
422
- logcatProcess = spawn('adb', ['-s', adbId, ...getLogcatArgs(appUid, logcatTimestamp)], {
423
- stdout: 'pipe',
424
- stderr: 'pipe',
425
- });
448
+ logcatProcess = spawn(
449
+ 'adb',
450
+ ['-s', adbId, ...getLogcatArgs(appUid, logcatTimestamp)],
451
+ {
452
+ stdout: 'pipe',
453
+ stderr: 'pipe',
454
+ }
455
+ );
426
456
 
427
457
  const currentProcess = logcatProcess;
428
458
 
@@ -439,15 +469,23 @@ export const createAndroidAppMonitor = ({
439
469
  const event = createAndroidLogEvent(line, bundleId);
440
470
 
441
471
  if (event) {
442
- if (event.type === 'possible_crash' || event.type === 'app_exited') {
472
+ if (
473
+ event.type === 'possible_crash' ||
474
+ event.type === 'app_exited'
475
+ ) {
443
476
  recordCrashArtifact(event.crashDetails);
444
477
  }
445
478
  emit(event);
446
479
  }
447
480
  }
448
481
  } catch (error) {
449
- if (!(error instanceof SubprocessError && error.signalName === 'SIGTERM')) {
450
- androidAppMonitorLogger.debug('Android logcat monitor stopped', error);
482
+ if (
483
+ !(error instanceof SubprocessError && error.signalName === 'SIGTERM')
484
+ ) {
485
+ androidAppMonitorLogger.debug(
486
+ 'Android logcat monitor stopped',
487
+ error
488
+ );
451
489
  }
452
490
  }
453
491
  })();