@react-native-harness/platform-android 1.1.0-rc.2 → 1.1.0-rc.4
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 +117 -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__/targets.test.d.ts +2 -0
- package/dist/__tests__/targets.test.d.ts.map +1 -0
- package/dist/__tests__/targets.test.js +49 -0
- package/dist/adb.d.ts +23 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +259 -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 +31 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +317 -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 +232 -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 +9 -52
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +4 -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 +212 -0
- package/src/__tests__/instance.test.ts +610 -0
- package/src/__tests__/targets.test.ts +53 -0
- package/src/adb.ts +430 -28
- 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 +554 -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 +19 -70
- package/src/targets.ts +18 -8
package/src/adb.ts
CHANGED
|
@@ -1,10 +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
|
+
ensureAndroidEmulatorAvailable,
|
|
9
|
+
ensureAndroidSdkPackages,
|
|
10
|
+
getAdbBinaryPath,
|
|
11
|
+
getAndroidSystemImagePackage,
|
|
12
|
+
getAvdManagerBinaryPath,
|
|
13
|
+
getEmulatorBinaryPath,
|
|
14
|
+
getHostAndroidSystemImageArch,
|
|
15
|
+
getRequiredAndroidSdkPackages,
|
|
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
|
+
await ensureAndroidEmulatorAvailable();
|
|
135
|
+
|
|
136
|
+
const emulatorBinaryPath = getEmulatorBinaryPath();
|
|
137
|
+
await access(emulatorBinaryPath);
|
|
138
|
+
return emulatorBinaryPath;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type CreateAvdOptions = {
|
|
142
|
+
name: string;
|
|
143
|
+
apiLevel: number;
|
|
144
|
+
profile: string;
|
|
145
|
+
diskSize: string;
|
|
146
|
+
heapSize: string;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const getRequiredEmulatorPackages = (apiLevel: number): string[] => {
|
|
150
|
+
return getRequiredAndroidSdkPackages({
|
|
151
|
+
apiLevel,
|
|
152
|
+
includeEmulator: true,
|
|
153
|
+
architecture: getHostAndroidSystemImageArch(),
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const verifyAndroidEmulatorSdk = async (
|
|
158
|
+
apiLevel: number,
|
|
159
|
+
): Promise<void> => {
|
|
160
|
+
await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
|
|
161
|
+
};
|
|
3
162
|
|
|
4
163
|
export const getStartAppArgs = (
|
|
5
164
|
bundleId: string,
|
|
6
165
|
activityName: string,
|
|
7
|
-
options?: AndroidAppLaunchOptions
|
|
166
|
+
options?: AndroidAppLaunchOptions,
|
|
8
167
|
): string[] => {
|
|
9
168
|
const args = [
|
|
10
169
|
'shell',
|
|
@@ -33,7 +192,7 @@ export const getStartAppArgs = (
|
|
|
33
192
|
|
|
34
193
|
if (!Number.isSafeInteger(value)) {
|
|
35
194
|
throw new Error(
|
|
36
|
-
`Android app launch option "${key}" must be a safe integer
|
|
195
|
+
`Android app launch option "${key}" must be a safe integer.`,
|
|
37
196
|
);
|
|
38
197
|
}
|
|
39
198
|
|
|
@@ -45,9 +204,9 @@ export const getStartAppArgs = (
|
|
|
45
204
|
|
|
46
205
|
export const isAppInstalled = async (
|
|
47
206
|
adbId: string,
|
|
48
|
-
bundleId: string
|
|
207
|
+
bundleId: string,
|
|
49
208
|
): Promise<boolean> => {
|
|
50
|
-
const { stdout } = await spawn(
|
|
209
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
51
210
|
'-s',
|
|
52
211
|
adbId,
|
|
53
212
|
'shell',
|
|
@@ -62,9 +221,9 @@ export const isAppInstalled = async (
|
|
|
62
221
|
export const reversePort = async (
|
|
63
222
|
adbId: string,
|
|
64
223
|
port: number,
|
|
65
|
-
hostPort: number = port
|
|
224
|
+
hostPort: number = port,
|
|
66
225
|
): Promise<void> => {
|
|
67
|
-
await spawn(
|
|
226
|
+
await spawn(getAdbBinaryPath(), [
|
|
68
227
|
'-s',
|
|
69
228
|
adbId,
|
|
70
229
|
'reverse',
|
|
@@ -75,22 +234,33 @@ export const reversePort = async (
|
|
|
75
234
|
|
|
76
235
|
export const stopApp = async (
|
|
77
236
|
adbId: string,
|
|
78
|
-
bundleId: string
|
|
237
|
+
bundleId: string,
|
|
79
238
|
): Promise<void> => {
|
|
80
|
-
await spawn(
|
|
239
|
+
await spawn(getAdbBinaryPath(), [
|
|
240
|
+
'-s',
|
|
241
|
+
adbId,
|
|
242
|
+
'shell',
|
|
243
|
+
'am',
|
|
244
|
+
'force-stop',
|
|
245
|
+
bundleId,
|
|
246
|
+
]);
|
|
81
247
|
};
|
|
82
248
|
|
|
83
249
|
export const startApp = async (
|
|
84
250
|
adbId: string,
|
|
85
251
|
bundleId: string,
|
|
86
252
|
activityName: string,
|
|
87
|
-
options?: AndroidAppLaunchOptions
|
|
253
|
+
options?: AndroidAppLaunchOptions,
|
|
88
254
|
): Promise<void> => {
|
|
89
|
-
await spawn(
|
|
255
|
+
await spawn(getAdbBinaryPath(), [
|
|
256
|
+
'-s',
|
|
257
|
+
adbId,
|
|
258
|
+
...getStartAppArgs(bundleId, activityName, options),
|
|
259
|
+
]);
|
|
90
260
|
};
|
|
91
261
|
|
|
92
262
|
export const getDeviceIds = async (): Promise<string[]> => {
|
|
93
|
-
const { stdout } = await spawn(
|
|
263
|
+
const { stdout } = await spawn(getAdbBinaryPath(), ['devices']);
|
|
94
264
|
return stdout
|
|
95
265
|
.split('\n')
|
|
96
266
|
.slice(1) // Skip header
|
|
@@ -99,17 +269,23 @@ export const getDeviceIds = async (): Promise<string[]> => {
|
|
|
99
269
|
};
|
|
100
270
|
|
|
101
271
|
export const getEmulatorName = async (
|
|
102
|
-
adbId: string
|
|
272
|
+
adbId: string,
|
|
103
273
|
): Promise<string | null> => {
|
|
104
|
-
const { stdout } = await spawn(
|
|
274
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
275
|
+
'-s',
|
|
276
|
+
adbId,
|
|
277
|
+
'emu',
|
|
278
|
+
'avd',
|
|
279
|
+
'name',
|
|
280
|
+
]);
|
|
105
281
|
return stdout.split('\n')[0].trim() || null;
|
|
106
282
|
};
|
|
107
283
|
|
|
108
284
|
export const getShellProperty = async (
|
|
109
285
|
adbId: string,
|
|
110
|
-
property: string
|
|
286
|
+
property: string,
|
|
111
287
|
): Promise<string | null> => {
|
|
112
|
-
const { stdout } = await spawn(
|
|
288
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
113
289
|
'-s',
|
|
114
290
|
adbId,
|
|
115
291
|
'shell',
|
|
@@ -119,13 +295,17 @@ export const getShellProperty = async (
|
|
|
119
295
|
return stdout.trim() || null;
|
|
120
296
|
};
|
|
121
297
|
|
|
298
|
+
const isTransientAdbShellFailure = (error: unknown): boolean => {
|
|
299
|
+
return error instanceof SubprocessError && error.exitCode === 1;
|
|
300
|
+
};
|
|
301
|
+
|
|
122
302
|
export type DeviceInfo = {
|
|
123
303
|
manufacturer: string | null;
|
|
124
304
|
model: string | null;
|
|
125
305
|
};
|
|
126
306
|
|
|
127
307
|
export const getDeviceInfo = async (
|
|
128
|
-
adbId: string
|
|
308
|
+
adbId: string,
|
|
129
309
|
): Promise<DeviceInfo | null> => {
|
|
130
310
|
const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer');
|
|
131
311
|
const model = await getShellProperty(adbId, 'ro.product.model');
|
|
@@ -133,20 +313,241 @@ export const getDeviceInfo = async (
|
|
|
133
313
|
};
|
|
134
314
|
|
|
135
315
|
export const isBootCompleted = async (adbId: string): Promise<boolean> => {
|
|
136
|
-
|
|
137
|
-
|
|
316
|
+
try {
|
|
317
|
+
const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
|
|
318
|
+
return bootCompleted === '1';
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (isTransientAdbShellFailure(error)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
138
326
|
};
|
|
139
327
|
|
|
140
328
|
export const stopEmulator = async (adbId: string): Promise<void> => {
|
|
141
|
-
await spawn(
|
|
329
|
+
await spawn(getAdbBinaryPath(), ['-s', adbId, 'emu', 'kill']);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const installApp = async (
|
|
333
|
+
adbId: string,
|
|
334
|
+
appPath: string,
|
|
335
|
+
): Promise<void> => {
|
|
336
|
+
await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export const hasAvd = async (name: string): Promise<boolean> => {
|
|
340
|
+
const avds = await getAvds();
|
|
341
|
+
return avds.includes(name);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export const createAvd = async ({
|
|
345
|
+
name,
|
|
346
|
+
apiLevel,
|
|
347
|
+
profile,
|
|
348
|
+
diskSize,
|
|
349
|
+
heapSize,
|
|
350
|
+
}: CreateAvdOptions): Promise<void> => {
|
|
351
|
+
const systemImagePackage = getAndroidSystemImagePackage(
|
|
352
|
+
apiLevel,
|
|
353
|
+
getHostAndroidSystemImageArch(),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
await verifyAndroidEmulatorSdk(apiLevel);
|
|
357
|
+
await spawn('bash', [
|
|
358
|
+
'-lc',
|
|
359
|
+
`printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}"`,
|
|
360
|
+
]);
|
|
361
|
+
await spawn('bash', [
|
|
362
|
+
'-lc',
|
|
363
|
+
`printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${getAvdConfigPath(
|
|
364
|
+
name,
|
|
365
|
+
)}"`,
|
|
366
|
+
]);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export const deleteAvd = async (name: string): Promise<void> => {
|
|
370
|
+
await rm(
|
|
371
|
+
`${
|
|
372
|
+
process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
|
|
373
|
+
}/${name}.avd`,
|
|
374
|
+
{
|
|
375
|
+
force: true,
|
|
376
|
+
recursive: true,
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
await rm(
|
|
380
|
+
`${
|
|
381
|
+
process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
|
|
382
|
+
}/${name}.ini`,
|
|
383
|
+
{
|
|
384
|
+
force: true,
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const startEmulator = async (
|
|
390
|
+
name: string,
|
|
391
|
+
mode: EmulatorBootMode = 'default-boot',
|
|
392
|
+
): Promise<void> => {
|
|
393
|
+
const emulatorBinaryPath = await ensureEmulatorInstalled();
|
|
394
|
+
const childProcess = emulatorProcess.startDetachedProcess(
|
|
395
|
+
emulatorBinaryPath,
|
|
396
|
+
getEmulatorStartupArgs(name, mode),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
let stdout = '';
|
|
400
|
+
let stderr = '';
|
|
401
|
+
|
|
402
|
+
childProcess.stdout?.setEncoding('utf8');
|
|
403
|
+
childProcess.stderr?.setEncoding('utf8');
|
|
404
|
+
|
|
405
|
+
const onStdout = (chunk: string | Buffer) => {
|
|
406
|
+
stdout = appendBoundedOutput(stdout, chunk.toString());
|
|
407
|
+
};
|
|
408
|
+
const onStderr = (chunk: string | Buffer) => {
|
|
409
|
+
stderr = appendBoundedOutput(stderr, chunk.toString());
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
childProcess.stdout?.on('data', onStdout);
|
|
413
|
+
childProcess.stderr?.on('data', onStderr);
|
|
414
|
+
|
|
415
|
+
const startupAbortController = new AbortController();
|
|
416
|
+
const cleanup = () => {
|
|
417
|
+
startupAbortController.abort();
|
|
418
|
+
childProcess.stdout?.off('data', onStdout);
|
|
419
|
+
childProcess.stderr?.off('data', onStderr);
|
|
420
|
+
childProcess.removeAllListeners('error');
|
|
421
|
+
childProcess.removeAllListeners('close');
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const earlyExit = new Promise<never>((_, reject) => {
|
|
425
|
+
childProcess.once('error', (error) => {
|
|
426
|
+
reject(
|
|
427
|
+
formatEmulatorStartupError({
|
|
428
|
+
name,
|
|
429
|
+
stdout,
|
|
430
|
+
stderr,
|
|
431
|
+
error,
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
childProcess.once('close', (exitCode, signal) => {
|
|
437
|
+
reject(
|
|
438
|
+
formatEmulatorStartupError({
|
|
439
|
+
name,
|
|
440
|
+
stdout,
|
|
441
|
+
stderr,
|
|
442
|
+
exitCode,
|
|
443
|
+
signal,
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const observedBoot = waitForEmulator(name, startupAbortController.signal)
|
|
450
|
+
.then(() => 'booted' as const)
|
|
451
|
+
.catch((error: unknown) => {
|
|
452
|
+
if (startupAbortController.signal.aborted) {
|
|
453
|
+
return 'aborted' as const;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
throw error;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then(
|
|
460
|
+
() => 'timeout' as const,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
await Promise.race([earlyExit, observedBoot, observationTimeout]);
|
|
465
|
+
} finally {
|
|
466
|
+
cleanup();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
childProcess.stdout?.destroy();
|
|
470
|
+
childProcess.stderr?.destroy();
|
|
471
|
+
childProcess.unref();
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export const waitForEmulator = async (
|
|
475
|
+
name: string,
|
|
476
|
+
signal: AbortSignal,
|
|
477
|
+
): Promise<string> => {
|
|
478
|
+
while (!signal.aborted) {
|
|
479
|
+
const adbIds = await getDeviceIds();
|
|
480
|
+
|
|
481
|
+
for (const adbId of adbIds) {
|
|
482
|
+
if (!adbId.startsWith('emulator-')) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const emulatorName = await getEmulatorName(adbId);
|
|
487
|
+
|
|
488
|
+
if (emulatorName === name) {
|
|
489
|
+
return adbId;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
await waitWithSignal(1000, signal);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
throw signal.reason;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export const waitForEmulatorDisconnect = async (
|
|
500
|
+
adbId: string,
|
|
501
|
+
signal: AbortSignal,
|
|
502
|
+
): Promise<void> => {
|
|
503
|
+
while (!signal.aborted) {
|
|
504
|
+
const adbIds = await getDeviceIds();
|
|
505
|
+
|
|
506
|
+
if (!adbIds.includes(adbId)) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
await waitWithSignal(1000, signal);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
throw signal.reason;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
export const waitForBoot = async (
|
|
517
|
+
name: string,
|
|
518
|
+
signal: AbortSignal,
|
|
519
|
+
): Promise<string> => {
|
|
520
|
+
while (!signal.aborted) {
|
|
521
|
+
const adbIds = await getDeviceIds();
|
|
522
|
+
|
|
523
|
+
for (const adbId of adbIds) {
|
|
524
|
+
if (!adbId.startsWith('emulator-')) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const emulatorName = await getEmulatorName(adbId);
|
|
529
|
+
|
|
530
|
+
if (emulatorName !== name) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (await isBootCompleted(adbId)) {
|
|
535
|
+
return adbId;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
await waitWithSignal(1000, signal);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
throw signal.reason;
|
|
142
543
|
};
|
|
143
544
|
|
|
144
545
|
export const isAppRunning = async (
|
|
145
546
|
adbId: string,
|
|
146
|
-
bundleId: string
|
|
547
|
+
bundleId: string,
|
|
147
548
|
): Promise<boolean> => {
|
|
148
549
|
try {
|
|
149
|
-
const { stdout } = await spawn(
|
|
550
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
150
551
|
'-s',
|
|
151
552
|
adbId,
|
|
152
553
|
'shell',
|
|
@@ -165,9 +566,9 @@ export const isAppRunning = async (
|
|
|
165
566
|
|
|
166
567
|
export const getAppUid = async (
|
|
167
568
|
adbId: string,
|
|
168
|
-
bundleId: string
|
|
569
|
+
bundleId: string,
|
|
169
570
|
): Promise<number> => {
|
|
170
|
-
const { stdout } = await spawn(
|
|
571
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
171
572
|
'-s',
|
|
172
573
|
adbId,
|
|
173
574
|
'shell',
|
|
@@ -190,9 +591,9 @@ export const getAppUid = async (
|
|
|
190
591
|
|
|
191
592
|
export const setHideErrorDialogs = async (
|
|
192
593
|
adbId: string,
|
|
193
|
-
hide: boolean
|
|
594
|
+
hide: boolean,
|
|
194
595
|
): Promise<void> => {
|
|
195
|
-
await spawn(
|
|
596
|
+
await spawn(getAdbBinaryPath(), [
|
|
196
597
|
'-s',
|
|
197
598
|
adbId,
|
|
198
599
|
'shell',
|
|
@@ -205,7 +606,7 @@ export const setHideErrorDialogs = async (
|
|
|
205
606
|
};
|
|
206
607
|
|
|
207
608
|
export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
|
|
208
|
-
const { stdout } = await spawn(
|
|
609
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
209
610
|
'-s',
|
|
210
611
|
adbId,
|
|
211
612
|
'shell',
|
|
@@ -218,7 +619,8 @@ export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
|
|
|
218
619
|
|
|
219
620
|
export const getAvds = async (): Promise<string[]> => {
|
|
220
621
|
try {
|
|
221
|
-
const
|
|
622
|
+
const emulatorBinaryPath = await ensureEmulatorInstalled();
|
|
623
|
+
const { stdout } = await spawn(emulatorBinaryPath, ['-list-avds']);
|
|
222
624
|
return stdout
|
|
223
625
|
.split('\n')
|
|
224
626
|
.map((line) => line.trim())
|
|
@@ -235,7 +637,7 @@ export type AdbDevice = {
|
|
|
235
637
|
};
|
|
236
638
|
|
|
237
639
|
export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
|
|
238
|
-
const { stdout } = await spawn(
|
|
640
|
+
const { stdout } = await spawn(getAdbBinaryPath(), ['devices', '-l']);
|
|
239
641
|
const lines = stdout.split('\n').slice(1);
|
|
240
642
|
const devices: AdbDevice[] = [];
|
|
241
643
|
|