@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.
- package/README.md +9 -2
- package/dist/__tests__/adb.test.js +283 -10
- package/dist/__tests__/avd-config.test.d.ts +2 -0
- package/dist/__tests__/avd-config.test.d.ts.map +1 -0
- package/dist/__tests__/avd-config.test.js +174 -0
- package/dist/__tests__/ci-action.test.d.ts +2 -0
- package/dist/__tests__/ci-action.test.d.ts.map +1 -0
- package/dist/__tests__/ci-action.test.js +46 -0
- package/dist/__tests__/emulator-startup.test.d.ts +2 -0
- package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
- package/dist/__tests__/emulator-startup.test.js +19 -0
- package/dist/__tests__/environment.test.d.ts +2 -0
- package/dist/__tests__/environment.test.d.ts.map +1 -0
- package/dist/__tests__/environment.test.js +51 -0
- package/dist/__tests__/instance.test.d.ts +2 -0
- package/dist/__tests__/instance.test.d.ts.map +1 -0
- package/dist/__tests__/instance.test.js +423 -0
- package/dist/adb.d.ts +23 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +265 -16
- package/dist/app-monitor.d.ts.map +1 -1
- package/dist/app-monitor.js +27 -7
- package/dist/assertions.d.ts +5 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +6 -0
- package/dist/avd-config.d.ts +41 -0
- package/dist/avd-config.d.ts.map +1 -0
- package/dist/avd-config.js +173 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/emulator-startup.d.ts +3 -0
- package/dist/emulator-startup.d.ts.map +1 -0
- package/dist/emulator-startup.js +17 -0
- package/dist/emulator.d.ts +6 -0
- package/dist/emulator.d.ts.map +1 -0
- package/dist/emulator.js +27 -0
- package/dist/environment.d.ts +28 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +295 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/instance.d.ts +6 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +234 -0
- package/dist/reader.d.ts +6 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +57 -0
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +12 -52
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +2 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +381 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +107 -0
- package/package.json +4 -4
- package/src/__tests__/adb.test.ts +419 -15
- package/src/__tests__/avd-config.test.ts +206 -0
- package/src/__tests__/ci-action.test.ts +81 -0
- package/src/__tests__/emulator-startup.test.ts +32 -0
- package/src/__tests__/environment.test.ts +87 -0
- package/src/__tests__/instance.test.ts +610 -0
- package/src/adb.ts +423 -16
- package/src/app-monitor.ts +56 -18
- package/src/avd-config.ts +290 -0
- package/src/config.ts +8 -0
- package/src/emulator-startup.ts +28 -0
- package/src/environment.ts +510 -0
- package/src/errors.ts +19 -0
- package/src/factory.ts +4 -0
- package/src/index.ts +7 -1
- package/src/instance.ts +380 -0
- package/src/runner.ts +23 -69
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
137
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
645
|
+
const { stdout } = await spawn(getAdbBinaryPath(), ['devices', '-l']);
|
|
239
646
|
const lines = stdout.split('\n').slice(1);
|
|
240
647
|
const devices: AdbDevice[] = [];
|
|
241
648
|
|
package/src/app-monitor.ts
CHANGED
|
@@ -6,14 +6,30 @@ import {
|
|
|
6
6
|
type AppMonitorEvent,
|
|
7
7
|
type AppMonitorListener,
|
|
8
8
|
} from '@react-native-harness/platforms';
|
|
9
|
-
import {
|
|
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
|
-
[
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
292
|
+
? matchingByProcess
|
|
293
|
+
: crashArtifacts;
|
|
270
294
|
const sortedCandidates = [...candidates].sort(
|
|
271
295
|
(left, right) =>
|
|
272
|
-
Math.abs(left.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 = [
|
|
389
|
-
|
|
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(
|
|
423
|
-
|
|
424
|
-
|
|
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 (
|
|
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 (
|
|
450
|
-
|
|
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
|
})();
|