@react-native-harness/platform-android 1.1.0-rc.1 → 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/__tests__/shared-prefs.test.d.ts +2 -0
- package/dist/__tests__/shared-prefs.test.d.ts.map +1 -0
- package/dist/__tests__/shared-prefs.test.js +87 -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 +29 -8
- 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/environment.d.ts +28 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +295 -0
- package/dist/errors.d.ts +4 -12
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -24
- 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/runner.d.ts +3 -3
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +12 -48
- package/dist/shared-prefs.d.ts +3 -0
- package/dist/shared-prefs.d.ts.map +1 -0
- package/dist/shared-prefs.js +92 -0
- 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/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/__tests__/shared-prefs.test.ts +144 -0
- package/src/adb.ts +423 -16
- package/src/app-monitor.ts +58 -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 +25 -63
- package/src/shared-prefs.ts +205 -0
- package/src/targets.ts +11 -8
- package/tsconfig.json +2 -2
- package/tsconfig.lib.json +2 -2
package/dist/adb.js
CHANGED
|
@@ -1,4 +1,90 @@
|
|
|
1
1
|
import { spawn, SubprocessError } from '@react-native-harness/tools';
|
|
2
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
3
|
+
import { access, rm } from 'node:fs/promises';
|
|
4
|
+
import { ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, getAvdManagerBinaryPath, getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, getSdkManagerBinaryPath, } from './environment.js';
|
|
5
|
+
import { getEmulatorStartupArgs, } from './emulator-startup.js';
|
|
6
|
+
const wait = async (ms) => {
|
|
7
|
+
await new Promise((resolve) => {
|
|
8
|
+
setTimeout(resolve, ms);
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
const waitForAbort = (signal) => {
|
|
12
|
+
if (signal.aborted) {
|
|
13
|
+
return Promise.reject(signal.reason);
|
|
14
|
+
}
|
|
15
|
+
return new Promise((_, reject) => {
|
|
16
|
+
signal.addEventListener('abort', () => {
|
|
17
|
+
reject(signal.reason);
|
|
18
|
+
}, { once: true });
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const waitWithSignal = async (ms, signal) => {
|
|
22
|
+
if (signal.aborted) {
|
|
23
|
+
throw signal.reason;
|
|
24
|
+
}
|
|
25
|
+
await Promise.race([wait(ms), waitForAbort(signal)]);
|
|
26
|
+
};
|
|
27
|
+
const getAvdConfigPath = (name) => `${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd/config.ini`;
|
|
28
|
+
const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
|
|
29
|
+
const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
|
|
30
|
+
export const emulatorProcess = {
|
|
31
|
+
startDetachedProcess: (file, args) => nodeSpawn(file, args, {
|
|
32
|
+
detached: true,
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
const appendBoundedOutput = (output, chunk, limit = EMULATOR_OUTPUT_BUFFER_LIMIT) => {
|
|
37
|
+
const nextOutput = output + chunk;
|
|
38
|
+
if (nextOutput.length <= limit) {
|
|
39
|
+
return nextOutput;
|
|
40
|
+
}
|
|
41
|
+
return nextOutput.slice(-limit);
|
|
42
|
+
};
|
|
43
|
+
const formatEmulatorStartupError = ({ name, stdout, stderr, exitCode, signal, error, }) => {
|
|
44
|
+
const sections = [`Failed to start Android emulator @${name}.`];
|
|
45
|
+
if (typeof exitCode === 'number') {
|
|
46
|
+
sections.push(`Exit code: ${exitCode}`);
|
|
47
|
+
}
|
|
48
|
+
if (signal) {
|
|
49
|
+
sections.push(`Signal: ${signal}`);
|
|
50
|
+
}
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
sections.push(`Cause: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
const trimmedStdout = stdout.trim();
|
|
55
|
+
const trimmedStderr = stderr.trim();
|
|
56
|
+
if (trimmedStdout !== '') {
|
|
57
|
+
sections.push(`stdout:\n${trimmedStdout}`);
|
|
58
|
+
}
|
|
59
|
+
if (trimmedStderr !== '') {
|
|
60
|
+
sections.push(`stderr:\n${trimmedStderr}`);
|
|
61
|
+
}
|
|
62
|
+
return new Error(sections.join('\n\n'), {
|
|
63
|
+
cause: error instanceof Error ? error : undefined,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const ensureEmulatorInstalled = async () => {
|
|
67
|
+
const emulatorBinaryPath = getEmulatorBinaryPath();
|
|
68
|
+
try {
|
|
69
|
+
await access(emulatorBinaryPath);
|
|
70
|
+
return emulatorBinaryPath;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
await spawn(getSdkManagerBinaryPath(), ['emulator']);
|
|
74
|
+
await access(emulatorBinaryPath);
|
|
75
|
+
return emulatorBinaryPath;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
export const getRequiredEmulatorPackages = (apiLevel) => {
|
|
79
|
+
return getRequiredAndroidSdkPackages({
|
|
80
|
+
apiLevel,
|
|
81
|
+
includeEmulator: true,
|
|
82
|
+
architecture: getHostAndroidSystemImageArch(),
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
export const verifyAndroidEmulatorSdk = async (apiLevel) => {
|
|
86
|
+
await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
|
|
87
|
+
};
|
|
2
88
|
export const getStartAppArgs = (bundleId, activityName, options) => {
|
|
3
89
|
const args = [
|
|
4
90
|
'shell',
|
|
@@ -29,7 +115,7 @@ export const getStartAppArgs = (bundleId, activityName, options) => {
|
|
|
29
115
|
return args;
|
|
30
116
|
};
|
|
31
117
|
export const isAppInstalled = async (adbId, bundleId) => {
|
|
32
|
-
const { stdout } = await spawn(
|
|
118
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
33
119
|
'-s',
|
|
34
120
|
adbId,
|
|
35
121
|
'shell',
|
|
@@ -41,7 +127,7 @@ export const isAppInstalled = async (adbId, bundleId) => {
|
|
|
41
127
|
return stdout.trim() !== '';
|
|
42
128
|
};
|
|
43
129
|
export const reversePort = async (adbId, port, hostPort = port) => {
|
|
44
|
-
await spawn(
|
|
130
|
+
await spawn(getAdbBinaryPath(), [
|
|
45
131
|
'-s',
|
|
46
132
|
adbId,
|
|
47
133
|
'reverse',
|
|
@@ -50,13 +136,24 @@ export const reversePort = async (adbId, port, hostPort = port) => {
|
|
|
50
136
|
]);
|
|
51
137
|
};
|
|
52
138
|
export const stopApp = async (adbId, bundleId) => {
|
|
53
|
-
await spawn(
|
|
139
|
+
await spawn(getAdbBinaryPath(), [
|
|
140
|
+
'-s',
|
|
141
|
+
adbId,
|
|
142
|
+
'shell',
|
|
143
|
+
'am',
|
|
144
|
+
'force-stop',
|
|
145
|
+
bundleId,
|
|
146
|
+
]);
|
|
54
147
|
};
|
|
55
148
|
export const startApp = async (adbId, bundleId, activityName, options) => {
|
|
56
|
-
await spawn(
|
|
149
|
+
await spawn(getAdbBinaryPath(), [
|
|
150
|
+
'-s',
|
|
151
|
+
adbId,
|
|
152
|
+
...getStartAppArgs(bundleId, activityName, options),
|
|
153
|
+
]);
|
|
57
154
|
};
|
|
58
155
|
export const getDeviceIds = async () => {
|
|
59
|
-
const { stdout } = await spawn(
|
|
156
|
+
const { stdout } = await spawn(getAdbBinaryPath(), ['devices']);
|
|
60
157
|
return stdout
|
|
61
158
|
.split('\n')
|
|
62
159
|
.slice(1) // Skip header
|
|
@@ -64,11 +161,17 @@ export const getDeviceIds = async () => {
|
|
|
64
161
|
.map((line) => line.split('\t')[0]);
|
|
65
162
|
};
|
|
66
163
|
export const getEmulatorName = async (adbId) => {
|
|
67
|
-
const { stdout } = await spawn(
|
|
164
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
165
|
+
'-s',
|
|
166
|
+
adbId,
|
|
167
|
+
'emu',
|
|
168
|
+
'avd',
|
|
169
|
+
'name',
|
|
170
|
+
]);
|
|
68
171
|
return stdout.split('\n')[0].trim() || null;
|
|
69
172
|
};
|
|
70
173
|
export const getShellProperty = async (adbId, property) => {
|
|
71
|
-
const { stdout } = await spawn(
|
|
174
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
72
175
|
'-s',
|
|
73
176
|
adbId,
|
|
74
177
|
'shell',
|
|
@@ -77,21 +180,166 @@ export const getShellProperty = async (adbId, property) => {
|
|
|
77
180
|
]);
|
|
78
181
|
return stdout.trim() || null;
|
|
79
182
|
};
|
|
183
|
+
const isTransientAdbShellFailure = (error) => {
|
|
184
|
+
return error instanceof SubprocessError && error.exitCode === 1;
|
|
185
|
+
};
|
|
80
186
|
export const getDeviceInfo = async (adbId) => {
|
|
81
187
|
const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer');
|
|
82
188
|
const model = await getShellProperty(adbId, 'ro.product.model');
|
|
83
189
|
return { manufacturer, model };
|
|
84
190
|
};
|
|
85
191
|
export const isBootCompleted = async (adbId) => {
|
|
86
|
-
|
|
87
|
-
|
|
192
|
+
try {
|
|
193
|
+
const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
|
|
194
|
+
return bootCompleted === '1';
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (isTransientAdbShellFailure(error)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
88
202
|
};
|
|
89
203
|
export const stopEmulator = async (adbId) => {
|
|
90
|
-
await spawn(
|
|
204
|
+
await spawn(getAdbBinaryPath(), ['-s', adbId, 'emu', 'kill']);
|
|
205
|
+
};
|
|
206
|
+
export const installApp = async (adbId, appPath) => {
|
|
207
|
+
await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
|
|
208
|
+
};
|
|
209
|
+
export const hasAvd = async (name) => {
|
|
210
|
+
const avds = await getAvds();
|
|
211
|
+
return avds.includes(name);
|
|
212
|
+
};
|
|
213
|
+
export const createAvd = async ({ name, apiLevel, profile, diskSize, heapSize, }) => {
|
|
214
|
+
const systemImagePackage = getAndroidSystemImagePackage(apiLevel, getHostAndroidSystemImageArch());
|
|
215
|
+
await verifyAndroidEmulatorSdk(apiLevel);
|
|
216
|
+
await spawn('bash', [
|
|
217
|
+
'-lc',
|
|
218
|
+
`printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}"`,
|
|
219
|
+
]);
|
|
220
|
+
await spawn('bash', [
|
|
221
|
+
'-lc',
|
|
222
|
+
`printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${getAvdConfigPath(name)}"`,
|
|
223
|
+
]);
|
|
224
|
+
};
|
|
225
|
+
export const deleteAvd = async (name) => {
|
|
226
|
+
await rm(`${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd`, {
|
|
227
|
+
force: true,
|
|
228
|
+
recursive: true,
|
|
229
|
+
});
|
|
230
|
+
await rm(`${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.ini`, {
|
|
231
|
+
force: true,
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
export const startEmulator = async (name, mode = 'default-boot') => {
|
|
235
|
+
const emulatorBinaryPath = await ensureEmulatorInstalled();
|
|
236
|
+
const childProcess = emulatorProcess.startDetachedProcess(emulatorBinaryPath, getEmulatorStartupArgs(name, mode));
|
|
237
|
+
let stdout = '';
|
|
238
|
+
let stderr = '';
|
|
239
|
+
childProcess.stdout?.setEncoding('utf8');
|
|
240
|
+
childProcess.stderr?.setEncoding('utf8');
|
|
241
|
+
const onStdout = (chunk) => {
|
|
242
|
+
stdout = appendBoundedOutput(stdout, chunk.toString());
|
|
243
|
+
};
|
|
244
|
+
const onStderr = (chunk) => {
|
|
245
|
+
stderr = appendBoundedOutput(stderr, chunk.toString());
|
|
246
|
+
};
|
|
247
|
+
childProcess.stdout?.on('data', onStdout);
|
|
248
|
+
childProcess.stderr?.on('data', onStderr);
|
|
249
|
+
const startupAbortController = new AbortController();
|
|
250
|
+
const cleanup = () => {
|
|
251
|
+
startupAbortController.abort();
|
|
252
|
+
childProcess.stdout?.off('data', onStdout);
|
|
253
|
+
childProcess.stderr?.off('data', onStderr);
|
|
254
|
+
childProcess.removeAllListeners('error');
|
|
255
|
+
childProcess.removeAllListeners('close');
|
|
256
|
+
};
|
|
257
|
+
const earlyExit = new Promise((_, reject) => {
|
|
258
|
+
childProcess.once('error', (error) => {
|
|
259
|
+
reject(formatEmulatorStartupError({
|
|
260
|
+
name,
|
|
261
|
+
stdout,
|
|
262
|
+
stderr,
|
|
263
|
+
error,
|
|
264
|
+
}));
|
|
265
|
+
});
|
|
266
|
+
childProcess.once('close', (exitCode, signal) => {
|
|
267
|
+
reject(formatEmulatorStartupError({
|
|
268
|
+
name,
|
|
269
|
+
stdout,
|
|
270
|
+
stderr,
|
|
271
|
+
exitCode,
|
|
272
|
+
signal,
|
|
273
|
+
}));
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
const observedBoot = waitForEmulator(name, startupAbortController.signal)
|
|
277
|
+
.then(() => 'booted')
|
|
278
|
+
.catch((error) => {
|
|
279
|
+
if (startupAbortController.signal.aborted) {
|
|
280
|
+
return 'aborted';
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
283
|
+
});
|
|
284
|
+
const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then(() => 'timeout');
|
|
285
|
+
try {
|
|
286
|
+
await Promise.race([earlyExit, observedBoot, observationTimeout]);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
cleanup();
|
|
290
|
+
}
|
|
291
|
+
childProcess.stdout?.destroy();
|
|
292
|
+
childProcess.stderr?.destroy();
|
|
293
|
+
childProcess.unref();
|
|
294
|
+
};
|
|
295
|
+
export const waitForEmulator = async (name, signal) => {
|
|
296
|
+
while (!signal.aborted) {
|
|
297
|
+
const adbIds = await getDeviceIds();
|
|
298
|
+
for (const adbId of adbIds) {
|
|
299
|
+
if (!adbId.startsWith('emulator-')) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const emulatorName = await getEmulatorName(adbId);
|
|
303
|
+
if (emulatorName === name) {
|
|
304
|
+
return adbId;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
await waitWithSignal(1000, signal);
|
|
308
|
+
}
|
|
309
|
+
throw signal.reason;
|
|
310
|
+
};
|
|
311
|
+
export const waitForEmulatorDisconnect = async (adbId, signal) => {
|
|
312
|
+
while (!signal.aborted) {
|
|
313
|
+
const adbIds = await getDeviceIds();
|
|
314
|
+
if (!adbIds.includes(adbId)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await waitWithSignal(1000, signal);
|
|
318
|
+
}
|
|
319
|
+
throw signal.reason;
|
|
320
|
+
};
|
|
321
|
+
export const waitForBoot = async (name, signal) => {
|
|
322
|
+
while (!signal.aborted) {
|
|
323
|
+
const adbIds = await getDeviceIds();
|
|
324
|
+
for (const adbId of adbIds) {
|
|
325
|
+
if (!adbId.startsWith('emulator-')) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const emulatorName = await getEmulatorName(adbId);
|
|
329
|
+
if (emulatorName !== name) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (await isBootCompleted(adbId)) {
|
|
333
|
+
return adbId;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
await waitWithSignal(1000, signal);
|
|
337
|
+
}
|
|
338
|
+
throw signal.reason;
|
|
91
339
|
};
|
|
92
340
|
export const isAppRunning = async (adbId, bundleId) => {
|
|
93
341
|
try {
|
|
94
|
-
const { stdout } = await spawn(
|
|
342
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
95
343
|
'-s',
|
|
96
344
|
adbId,
|
|
97
345
|
'shell',
|
|
@@ -108,7 +356,7 @@ export const isAppRunning = async (adbId, bundleId) => {
|
|
|
108
356
|
}
|
|
109
357
|
};
|
|
110
358
|
export const getAppUid = async (adbId, bundleId) => {
|
|
111
|
-
const { stdout } = await spawn(
|
|
359
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
112
360
|
'-s',
|
|
113
361
|
adbId,
|
|
114
362
|
'shell',
|
|
@@ -127,7 +375,7 @@ export const getAppUid = async (adbId, bundleId) => {
|
|
|
127
375
|
return Number(match[1]);
|
|
128
376
|
};
|
|
129
377
|
export const setHideErrorDialogs = async (adbId, hide) => {
|
|
130
|
-
await spawn(
|
|
378
|
+
await spawn(getAdbBinaryPath(), [
|
|
131
379
|
'-s',
|
|
132
380
|
adbId,
|
|
133
381
|
'shell',
|
|
@@ -139,7 +387,7 @@ export const setHideErrorDialogs = async (adbId, hide) => {
|
|
|
139
387
|
]);
|
|
140
388
|
};
|
|
141
389
|
export const getLogcatTimestamp = async (adbId) => {
|
|
142
|
-
const { stdout } = await spawn(
|
|
390
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
143
391
|
'-s',
|
|
144
392
|
adbId,
|
|
145
393
|
'shell',
|
|
@@ -150,7 +398,8 @@ export const getLogcatTimestamp = async (adbId) => {
|
|
|
150
398
|
};
|
|
151
399
|
export const getAvds = async () => {
|
|
152
400
|
try {
|
|
153
|
-
const
|
|
401
|
+
const emulatorBinaryPath = await ensureEmulatorInstalled();
|
|
402
|
+
const { stdout } = await spawn(emulatorBinaryPath, ['-list-avds']);
|
|
154
403
|
return stdout
|
|
155
404
|
.split('\n')
|
|
156
405
|
.map((line) => line.trim())
|
|
@@ -161,7 +410,7 @@ export const getAvds = async () => {
|
|
|
161
410
|
}
|
|
162
411
|
};
|
|
163
412
|
export const getConnectedDevices = async () => {
|
|
164
|
-
const { stdout } = await spawn(
|
|
413
|
+
const { stdout } = await spawn(getAdbBinaryPath(), ['devices', '-l']);
|
|
165
414
|
const lines = stdout.split('\n').slice(1);
|
|
166
415
|
const devices = [];
|
|
167
416
|
for (const line of lines) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-monitor.d.ts","sourceRoot":"","sources":["../src/app-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,EAErB,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"app-monitor.d.ts","sourceRoot":"","sources":["../src/app-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,EAErB,MAAM,iCAAiC,CAAC;AAgTzC,QAAA,MAAM,qBAAqB,GACzB,MAAM,MAAM,EACZ,UAAU,MAAM,KACf,eAAe,GAAG,IAwEpB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,mDAKrC;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,KAAG,iBAgLH,CAAC;AAEF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AACjC,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG;IAC3C,eAAe,EAAE,CACf,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC,CAAC"}
|
package/dist/app-monitor.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { escapeRegExp, getEmitter, logger, spawn, SubprocessError } from '@react-native-harness/tools';
|
|
1
|
+
import { escapeRegExp, getEmitter, logger, spawn, SubprocessError, } from '@react-native-harness/tools';
|
|
2
2
|
import * as adb from './adb.js';
|
|
3
3
|
import { androidCrashParser } from './crash-parser.js';
|
|
4
|
-
const
|
|
4
|
+
const androidAppMonitorLogger = logger.child('android-app-monitor');
|
|
5
|
+
const getLogcatArgs = (uid, fromTime) => [
|
|
6
|
+
'logcat',
|
|
7
|
+
'-v',
|
|
8
|
+
'threadtime',
|
|
9
|
+
'-b',
|
|
10
|
+
'crash',
|
|
11
|
+
`--uid=${uid}`,
|
|
12
|
+
'-T',
|
|
13
|
+
fromTime,
|
|
14
|
+
];
|
|
5
15
|
const MAX_RECENT_LOG_LINES = 200;
|
|
6
16
|
const MAX_RECENT_CRASH_ARTIFACTS = 10;
|
|
7
17
|
const CRASH_ARTIFACT_SETTLE_DELAY_MS = 100;
|
|
@@ -28,7 +38,11 @@ const getAndroidLogLineCrashDetails = ({ line, bundleId, pid, }) => {
|
|
|
28
38
|
summary: line.trim(),
|
|
29
39
|
signal: getSignal(line),
|
|
30
40
|
exceptionType: fatalExceptionMatch?.[1]?.trim(),
|
|
31
|
-
processName: processMatch
|
|
41
|
+
processName: processMatch
|
|
42
|
+
? bundleId
|
|
43
|
+
: line.includes(bundleId)
|
|
44
|
+
? bundleId
|
|
45
|
+
: undefined,
|
|
32
46
|
pid: pid ?? (processMatch ? Number(processMatch[1]) : undefined),
|
|
33
47
|
rawLines: [line],
|
|
34
48
|
};
|
|
@@ -115,7 +129,9 @@ const createCrashArtifact = ({ details, recentLogLines, }) => {
|
|
|
115
129
|
triggerLine: details.summary ?? '',
|
|
116
130
|
triggerOccurredAt,
|
|
117
131
|
artifactType: 'logcat',
|
|
118
|
-
rawLines: rawLines.length > 0
|
|
132
|
+
rawLines: rawLines.length > 0
|
|
133
|
+
? rawLines
|
|
134
|
+
: parsedDetails.rawLines ?? details.rawLines,
|
|
119
135
|
};
|
|
120
136
|
};
|
|
121
137
|
const persistCrashArtifact = ({ details, crashArtifactWriter, }) => {
|
|
@@ -150,7 +166,8 @@ const getLatestCrashArtifact = ({ crashArtifacts, recentLogLines, processName, p
|
|
|
150
166
|
: matchingByProcess.length > 0
|
|
151
167
|
? matchingByProcess
|
|
152
168
|
: crashArtifacts;
|
|
153
|
-
const sortedCandidates = [...candidates].sort((left, right) => Math.abs(left.occurredAt - occurredAt) -
|
|
169
|
+
const sortedCandidates = [...candidates].sort((left, right) => Math.abs(left.occurredAt - occurredAt) -
|
|
170
|
+
Math.abs(right.occurredAt - occurredAt));
|
|
154
171
|
const artifact = sortedCandidates[0];
|
|
155
172
|
if (!artifact) {
|
|
156
173
|
return null;
|
|
@@ -234,7 +251,10 @@ export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifact
|
|
|
234
251
|
emitter.emit(event);
|
|
235
252
|
};
|
|
236
253
|
const recordLogLine = (line) => {
|
|
237
|
-
recentLogLines = [
|
|
254
|
+
recentLogLines = [
|
|
255
|
+
...recentLogLines,
|
|
256
|
+
{ line, occurredAt: Date.now() },
|
|
257
|
+
].slice(-MAX_RECENT_LOG_LINES);
|
|
238
258
|
};
|
|
239
259
|
const recordCrashArtifact = (details) => {
|
|
240
260
|
if (!details) {
|
|
@@ -276,7 +296,8 @@ export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifact
|
|
|
276
296
|
emit({ type: 'log', source: 'logs', line });
|
|
277
297
|
const event = createAndroidLogEvent(line, bundleId);
|
|
278
298
|
if (event) {
|
|
279
|
-
if (event.type === 'possible_crash' ||
|
|
299
|
+
if (event.type === 'possible_crash' ||
|
|
300
|
+
event.type === 'app_exited') {
|
|
280
301
|
recordCrashArtifact(event.crashDetails);
|
|
281
302
|
}
|
|
282
303
|
emit(event);
|
|
@@ -285,7 +306,7 @@ export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifact
|
|
|
285
306
|
}
|
|
286
307
|
catch (error) {
|
|
287
308
|
if (!(error instanceof SubprocessError && error.signalName === 'SIGTERM')) {
|
|
288
|
-
|
|
309
|
+
androidAppMonitorLogger.debug('Android logcat monitor stopped', error);
|
|
289
310
|
}
|
|
290
311
|
}
|
|
291
312
|
})();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AndroidSystemImageArch } from './environment.js';
|
|
2
|
+
import type { AndroidEmulator, AndroidEmulatorAVDConfig } from './config.js';
|
|
3
|
+
export type AvdConfig = {
|
|
4
|
+
imageSysdir1?: string;
|
|
5
|
+
abiType?: string;
|
|
6
|
+
hwDeviceName?: string;
|
|
7
|
+
diskDataPartitionSize?: string;
|
|
8
|
+
vmHeapSize?: string;
|
|
9
|
+
};
|
|
10
|
+
export type AvdCompatibilityResult = {
|
|
11
|
+
compatible: true;
|
|
12
|
+
} | {
|
|
13
|
+
compatible: false;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
export declare const getAvdDirectory: (name: string) => string;
|
|
17
|
+
export declare const getAvdConfigPath: (name: string) => string;
|
|
18
|
+
export declare const parseAvdConfig: (contents: string) => AvdConfig;
|
|
19
|
+
export declare const readAvdConfig: (name: string) => Promise<AvdConfig | null>;
|
|
20
|
+
export declare const isAvdCompatible: ({ emulator, avdConfig, hostArch, }: {
|
|
21
|
+
emulator: AndroidEmulator;
|
|
22
|
+
avdConfig: AvdConfig;
|
|
23
|
+
hostArch: AndroidSystemImageArch;
|
|
24
|
+
}) => AvdCompatibilityResult;
|
|
25
|
+
export declare const getNormalizedAvdCacheConfig: ({ emulator, hostArch, }: {
|
|
26
|
+
emulator: AndroidEmulator;
|
|
27
|
+
hostArch: AndroidSystemImageArch;
|
|
28
|
+
}) => {
|
|
29
|
+
name: string;
|
|
30
|
+
apiLevel: number;
|
|
31
|
+
arch: AndroidSystemImageArch;
|
|
32
|
+
profile: string;
|
|
33
|
+
diskSize: string;
|
|
34
|
+
heapSize: string;
|
|
35
|
+
} | null;
|
|
36
|
+
export declare const resolveAvdCachingEnabled: ({ avd, isInteractive, env, }: {
|
|
37
|
+
avd?: AndroidEmulatorAVDConfig;
|
|
38
|
+
isInteractive: boolean;
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
}) => boolean;
|
|
41
|
+
//# sourceMappingURL=avd-config.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
export const getAvdDirectory = (name) => {
|
|
3
|
+
return `${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd`;
|
|
4
|
+
};
|
|
5
|
+
export const getAvdConfigPath = (name) => {
|
|
6
|
+
return `${getAvdDirectory(name)}/config.ini`;
|
|
7
|
+
};
|
|
8
|
+
const normalizeAvdValue = (value) => {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return value.trim();
|
|
13
|
+
};
|
|
14
|
+
const normalizeConfigValue = (value) => {
|
|
15
|
+
return value.trim().toLowerCase();
|
|
16
|
+
};
|
|
17
|
+
const parseSizeInBytes = (value) => {
|
|
18
|
+
if (!value) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
22
|
+
if (/^\d+$/.test(normalizedValue)) {
|
|
23
|
+
return Number(normalizedValue);
|
|
24
|
+
}
|
|
25
|
+
const match = normalizedValue.match(/^(\d+)([kmgt])$/i);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const size = Number(match[1]);
|
|
30
|
+
const unit = match[2]?.toLowerCase();
|
|
31
|
+
const multiplier = unit === 'k'
|
|
32
|
+
? 1024
|
|
33
|
+
: unit === 'm'
|
|
34
|
+
? 1024 ** 2
|
|
35
|
+
: unit === 'g'
|
|
36
|
+
? 1024 ** 3
|
|
37
|
+
: unit === 't'
|
|
38
|
+
? 1024 ** 4
|
|
39
|
+
: null;
|
|
40
|
+
return multiplier == null ? null : size * multiplier;
|
|
41
|
+
};
|
|
42
|
+
const getApiLevelFromImageSysdir = (value) => {
|
|
43
|
+
const match = value?.match(/android-(\d+)/i);
|
|
44
|
+
return match ? Number(match[1]) : null;
|
|
45
|
+
};
|
|
46
|
+
const normalizeProfile = (value) => {
|
|
47
|
+
if (!value) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return value
|
|
51
|
+
.trim()
|
|
52
|
+
.replace(/[\r\n]+/g, ' ')
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
};
|
|
55
|
+
export const parseAvdConfig = (contents) => {
|
|
56
|
+
const config = {};
|
|
57
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
58
|
+
const trimmedLine = line.trim();
|
|
59
|
+
if (trimmedLine === '' || trimmedLine.startsWith('#')) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const separatorIndex = trimmedLine.indexOf('=');
|
|
63
|
+
if (separatorIndex === -1) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const key = trimmedLine.slice(0, separatorIndex).trim();
|
|
67
|
+
const value = trimmedLine.slice(separatorIndex + 1).trim();
|
|
68
|
+
switch (key) {
|
|
69
|
+
case 'image.sysdir.1':
|
|
70
|
+
config.imageSysdir1 = value;
|
|
71
|
+
break;
|
|
72
|
+
case 'abi.type':
|
|
73
|
+
config.abiType = value;
|
|
74
|
+
break;
|
|
75
|
+
case 'hw.device.name':
|
|
76
|
+
config.hwDeviceName = value;
|
|
77
|
+
break;
|
|
78
|
+
case 'disk.dataPartition.size':
|
|
79
|
+
config.diskDataPartitionSize = value;
|
|
80
|
+
break;
|
|
81
|
+
case 'vm.heapSize':
|
|
82
|
+
config.vmHeapSize = value;
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
89
|
+
};
|
|
90
|
+
export const readAvdConfig = async (name) => {
|
|
91
|
+
const configPath = getAvdConfigPath(name);
|
|
92
|
+
try {
|
|
93
|
+
await access(configPath);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return parseAvdConfig(await readFile(configPath, 'utf8'));
|
|
99
|
+
};
|
|
100
|
+
export const isAvdCompatible = ({ emulator, avdConfig, hostArch, }) => {
|
|
101
|
+
const requestedAvdConfig = emulator.avd;
|
|
102
|
+
if (!requestedAvdConfig) {
|
|
103
|
+
return { compatible: false, reason: 'AVD config is required.' };
|
|
104
|
+
}
|
|
105
|
+
if (emulator.name.trim() === '') {
|
|
106
|
+
return { compatible: false, reason: 'AVD name is required.' };
|
|
107
|
+
}
|
|
108
|
+
const apiLevel = getApiLevelFromImageSysdir(avdConfig.imageSysdir1);
|
|
109
|
+
if (apiLevel !== requestedAvdConfig.apiLevel) {
|
|
110
|
+
return {
|
|
111
|
+
compatible: false,
|
|
112
|
+
reason: `API level mismatch: expected ${requestedAvdConfig.apiLevel}, got ${apiLevel ?? 'missing'}.`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (normalizeAvdValue(avdConfig.abiType) !== hostArch) {
|
|
116
|
+
return {
|
|
117
|
+
compatible: false,
|
|
118
|
+
reason: `ABI mismatch: expected ${hostArch}, got ${normalizeAvdValue(avdConfig.abiType) ?? 'missing'}.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (normalizeProfile(avdConfig.hwDeviceName) !==
|
|
122
|
+
normalizeProfile(requestedAvdConfig.profile)) {
|
|
123
|
+
return {
|
|
124
|
+
compatible: false,
|
|
125
|
+
reason: `Profile mismatch: expected ${requestedAvdConfig.profile}, got ${avdConfig.hwDeviceName ?? 'missing'}.`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if ((() => {
|
|
129
|
+
const configuredDiskSizeBytes = parseSizeInBytes(avdConfig.diskDataPartitionSize);
|
|
130
|
+
const requestedDiskSizeBytes = parseSizeInBytes(requestedAvdConfig.diskSize);
|
|
131
|
+
if (configuredDiskSizeBytes != null && requestedDiskSizeBytes != null) {
|
|
132
|
+
return configuredDiskSizeBytes < requestedDiskSizeBytes;
|
|
133
|
+
}
|
|
134
|
+
return (normalizeConfigValue(avdConfig.diskDataPartitionSize ?? '') !==
|
|
135
|
+
normalizeConfigValue(requestedAvdConfig.diskSize));
|
|
136
|
+
})()) {
|
|
137
|
+
return {
|
|
138
|
+
compatible: false,
|
|
139
|
+
reason: `Disk size mismatch: expected ${requestedAvdConfig.diskSize}, got ${avdConfig.diskDataPartitionSize ?? 'missing'}.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (normalizeConfigValue(avdConfig.vmHeapSize ?? '') !==
|
|
143
|
+
normalizeConfigValue(requestedAvdConfig.heapSize)) {
|
|
144
|
+
return {
|
|
145
|
+
compatible: false,
|
|
146
|
+
reason: `Heap size mismatch: expected ${requestedAvdConfig.heapSize}, got ${avdConfig.vmHeapSize ?? 'missing'}.`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { compatible: true };
|
|
150
|
+
};
|
|
151
|
+
export const getNormalizedAvdCacheConfig = ({ emulator, hostArch, }) => {
|
|
152
|
+
const avd = emulator.avd;
|
|
153
|
+
if (!avd) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
name: emulator.name,
|
|
158
|
+
apiLevel: avd.apiLevel,
|
|
159
|
+
arch: hostArch,
|
|
160
|
+
profile: avd.profile.trim().toLowerCase(),
|
|
161
|
+
diskSize: avd.diskSize.trim().toLowerCase(),
|
|
162
|
+
heapSize: avd.heapSize.trim().toLowerCase(),
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
export const resolveAvdCachingEnabled = ({ avd, isInteractive, env = process.env, }) => {
|
|
166
|
+
const override = env.HARNESS_AVD_CACHING;
|
|
167
|
+
const configValue = avd?.snapshot?.enabled;
|
|
168
|
+
const requestedValue = override == null ? configValue : override.toLowerCase() === 'true';
|
|
169
|
+
if (!requestedValue) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return !isInteractive;
|
|
173
|
+
};
|