@react-native-harness/platform-android 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +31 -20
- package/dist/avd-config.d.ts +2 -0
- package/dist/avd-config.d.ts.map +1 -1
- package/dist/avd-config.js +10 -2
- package/dist/config.d.ts +10 -10
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +21 -28
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +10 -2
- package/package.json +4 -4
- package/src/adb.ts +46 -38
- package/src/avd-config.ts +14 -4
- package/src/environment.ts +49 -39
- package/src/instance.ts +14 -2
- package/dist/app-monitor.d.ts +0 -13
- package/dist/app-monitor.d.ts.map +0 -1
- package/dist/app-monitor.js +0 -376
- package/dist/device-state.d.ts +0 -6
- package/dist/device-state.d.ts.map +0 -1
- package/dist/device-state.js +0 -142
package/dist/adb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,EAOL,KAAK,YAAY,EAClB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
|
package/dist/adb.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { delay, logger, spawn, spawnOwnedProcess, SubprocessError, waitForAbort, } from '@react-native-harness/tools';
|
|
2
2
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
3
|
-
import { access, rm } from 'node:fs/promises';
|
|
4
|
-
import { getAvdConfigPath, getAvdDirectory, readAvdConfig, } from './avd-config.js';
|
|
3
|
+
import { access, appendFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { getAvdConfigPath, getAvdDirectory, getAvdIniPath, readAvdConfig, } from './avd-config.js';
|
|
5
5
|
import { ensureAndroidEmulatorAvailable, ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, getAvdManagerBinaryPath, getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from './environment.js';
|
|
6
6
|
import { getEmulatorCpuCores, getEmulatorStartupArgs, } from './emulator-startup.js';
|
|
7
7
|
import { AdbAppNotInstalledError, AdbPermissionGrantError, } from './adb-errors.js';
|
|
@@ -102,17 +102,16 @@ const ensureAvdConfigExists = async (name) => {
|
|
|
102
102
|
}
|
|
103
103
|
return configPath;
|
|
104
104
|
};
|
|
105
|
-
const getAvdIniPath = (name) => {
|
|
106
|
-
const avdHome = process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`;
|
|
107
|
-
return `${avdHome}/${name}.ini`;
|
|
108
|
-
};
|
|
109
105
|
const ensureAvdIniExists = async ({ name, apiLevel, }) => {
|
|
110
106
|
const iniPath = getAvdIniPath(name);
|
|
111
107
|
const avdDirectory = getAvdDirectory(name);
|
|
112
|
-
await
|
|
113
|
-
'-
|
|
114
|
-
`
|
|
115
|
-
|
|
108
|
+
await writeFile(iniPath, [
|
|
109
|
+
'avd.ini.encoding=UTF-8',
|
|
110
|
+
`path=${avdDirectory}`,
|
|
111
|
+
`path.rel=avd/${name}.avd`,
|
|
112
|
+
`target=android-${apiLevel}`,
|
|
113
|
+
'',
|
|
114
|
+
].join('\n'));
|
|
116
115
|
return iniPath;
|
|
117
116
|
};
|
|
118
117
|
export const getStartAppArgs = (bundleId, activityName, options) => {
|
|
@@ -302,10 +301,20 @@ export const createAvd = async ({ name, apiLevel, profile, diskSize, heapSize, }
|
|
|
302
301
|
const systemImagePackage = getAndroidSystemImagePackage(apiLevel, getHostAndroidSystemImageArch());
|
|
303
302
|
await verifyAndroidEmulatorSdk(apiLevel);
|
|
304
303
|
await ensureAvdProfileAvailable(profile);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
304
|
+
// avdmanager asks whether to create a custom hardware profile; answer "no".
|
|
305
|
+
await spawn(getAvdManagerBinaryPath(), [
|
|
306
|
+
'create',
|
|
307
|
+
'avd',
|
|
308
|
+
'--force',
|
|
309
|
+
'--name',
|
|
310
|
+
name,
|
|
311
|
+
'--package',
|
|
312
|
+
systemImagePackage,
|
|
313
|
+
'--device',
|
|
314
|
+
profile,
|
|
315
|
+
'-p',
|
|
316
|
+
getAvdDirectory(name),
|
|
317
|
+
], { stdin: { string: 'no\n' } });
|
|
309
318
|
await ensureAvdIniExists({ name, apiLevel });
|
|
310
319
|
const configPath = await ensureAvdConfigExists(name);
|
|
311
320
|
// hw.cpu.ncore is baked into config.ini (rather than passed as a `-cores`
|
|
@@ -313,17 +322,19 @@ export const createAvd = async ({ name, apiLevel, profile, diskSize, heapSize, }
|
|
|
313
322
|
// snapshots require an identical hardware config to load, so the vCPU
|
|
314
323
|
// count must be fixed at AVD-creation time and stay consistent across
|
|
315
324
|
// boots.
|
|
316
|
-
await
|
|
317
|
-
|
|
318
|
-
`
|
|
319
|
-
|
|
325
|
+
await appendFile(configPath, [
|
|
326
|
+
`disk.dataPartition.size=${diskSize}`,
|
|
327
|
+
`vm.heapSize=${heapSize}`,
|
|
328
|
+
`hw.cpu.ncore=${getEmulatorCpuCores()}`,
|
|
329
|
+
'',
|
|
330
|
+
].join('\n'));
|
|
320
331
|
};
|
|
321
332
|
export const deleteAvd = async (name) => {
|
|
322
|
-
await rm(
|
|
333
|
+
await rm(getAvdDirectory(name), {
|
|
323
334
|
force: true,
|
|
324
335
|
recursive: true,
|
|
325
336
|
});
|
|
326
|
-
await rm(
|
|
337
|
+
await rm(getAvdIniPath(name), {
|
|
327
338
|
force: true,
|
|
328
339
|
});
|
|
329
340
|
};
|
package/dist/avd-config.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ export type AvdCompatibilityResult = {
|
|
|
14
14
|
compatible: false;
|
|
15
15
|
reason: string;
|
|
16
16
|
};
|
|
17
|
+
export declare const getAvdHome: () => string;
|
|
17
18
|
export declare const getAvdDirectory: (name: string) => string;
|
|
19
|
+
export declare const getAvdIniPath: (name: string) => string;
|
|
18
20
|
export declare const getAvdConfigPath: (name: string) => string;
|
|
19
21
|
export declare const parseAvdConfig: (contents: string) => AvdConfig;
|
|
20
22
|
export declare const readAvdConfig: (name: string) => Promise<AvdConfig | null>;
|
package/dist/avd-config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avd-config.d.ts","sourceRoot":"","sources":["../src/avd-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"avd-config.d.ts","sourceRoot":"","sources":["../src/avd-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAG7E,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;IACpB,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,UAAU,QAAO,MAI7B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAE9C,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAE5C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAE/C,CAAC;AAkEF,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,KAAG,SA4CjD,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,sBA+FH,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"}
|
package/dist/avd-config.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { getEmulatorCpuCores } from './emulator-startup.js';
|
|
5
|
+
export const getAvdHome = () => {
|
|
6
|
+
return (process.env.ANDROID_AVD_HOME ?? path.join(os.homedir(), '.android', 'avd'));
|
|
7
|
+
};
|
|
3
8
|
export const getAvdDirectory = (name) => {
|
|
4
|
-
return
|
|
9
|
+
return path.join(getAvdHome(), `${name}.avd`);
|
|
10
|
+
};
|
|
11
|
+
export const getAvdIniPath = (name) => {
|
|
12
|
+
return path.join(getAvdHome(), `${name}.ini`);
|
|
5
13
|
};
|
|
6
14
|
export const getAvdConfigPath = (name) => {
|
|
7
|
-
return
|
|
15
|
+
return path.join(getAvdDirectory(name), 'config.ini');
|
|
8
16
|
};
|
|
9
17
|
const normalizeAvdValue = (value) => {
|
|
10
18
|
if (!value) {
|
package/dist/config.d.ts
CHANGED
|
@@ -68,8 +68,8 @@ export declare const AndroidEmulatorSchema: z.ZodObject<{
|
|
|
68
68
|
} | undefined;
|
|
69
69
|
}>>;
|
|
70
70
|
}, "strip", z.ZodTypeAny, {
|
|
71
|
-
name: string;
|
|
72
71
|
type: "emulator";
|
|
72
|
+
name: string;
|
|
73
73
|
avd?: {
|
|
74
74
|
apiLevel: number;
|
|
75
75
|
profile: string;
|
|
@@ -80,8 +80,8 @@ export declare const AndroidEmulatorSchema: z.ZodObject<{
|
|
|
80
80
|
} | undefined;
|
|
81
81
|
} | undefined;
|
|
82
82
|
}, {
|
|
83
|
-
name: string;
|
|
84
83
|
type: "emulator";
|
|
84
|
+
name: string;
|
|
85
85
|
avd?: {
|
|
86
86
|
apiLevel: number;
|
|
87
87
|
profile: string;
|
|
@@ -138,8 +138,8 @@ export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
|
|
|
138
138
|
} | undefined;
|
|
139
139
|
}>>;
|
|
140
140
|
}, "strip", z.ZodTypeAny, {
|
|
141
|
-
name: string;
|
|
142
141
|
type: "emulator";
|
|
142
|
+
name: string;
|
|
143
143
|
avd?: {
|
|
144
144
|
apiLevel: number;
|
|
145
145
|
profile: string;
|
|
@@ -150,8 +150,8 @@ export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
|
|
|
150
150
|
} | undefined;
|
|
151
151
|
} | undefined;
|
|
152
152
|
}, {
|
|
153
|
-
name: string;
|
|
154
153
|
type: "emulator";
|
|
154
|
+
name: string;
|
|
155
155
|
avd?: {
|
|
156
156
|
apiLevel: number;
|
|
157
157
|
profile: string;
|
|
@@ -209,8 +209,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
209
209
|
} | undefined;
|
|
210
210
|
}>>;
|
|
211
211
|
}, "strip", z.ZodTypeAny, {
|
|
212
|
-
name: string;
|
|
213
212
|
type: "emulator";
|
|
213
|
+
name: string;
|
|
214
214
|
avd?: {
|
|
215
215
|
apiLevel: number;
|
|
216
216
|
profile: string;
|
|
@@ -221,8 +221,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
221
221
|
} | undefined;
|
|
222
222
|
} | undefined;
|
|
223
223
|
}, {
|
|
224
|
-
name: string;
|
|
225
224
|
type: "emulator";
|
|
225
|
+
name: string;
|
|
226
226
|
avd?: {
|
|
227
227
|
apiLevel: number;
|
|
228
228
|
profile: string;
|
|
@@ -255,11 +255,10 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
255
255
|
extras?: Record<string, string | number | boolean> | undefined;
|
|
256
256
|
}>>;
|
|
257
257
|
}, "strip", z.ZodTypeAny, {
|
|
258
|
-
bundleId: string;
|
|
259
258
|
name: string;
|
|
260
259
|
device: {
|
|
261
|
-
name: string;
|
|
262
260
|
type: "emulator";
|
|
261
|
+
name: string;
|
|
263
262
|
avd?: {
|
|
264
263
|
apiLevel: number;
|
|
265
264
|
profile: string;
|
|
@@ -274,16 +273,16 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
274
273
|
manufacturer: string;
|
|
275
274
|
model: string;
|
|
276
275
|
};
|
|
276
|
+
bundleId: string;
|
|
277
277
|
activityName: string;
|
|
278
278
|
appLaunchOptions?: {
|
|
279
279
|
extras?: Record<string, string | number | boolean> | undefined;
|
|
280
280
|
} | undefined;
|
|
281
281
|
}, {
|
|
282
|
-
bundleId: string;
|
|
283
282
|
name: string;
|
|
284
283
|
device: {
|
|
285
|
-
name: string;
|
|
286
284
|
type: "emulator";
|
|
285
|
+
name: string;
|
|
287
286
|
avd?: {
|
|
288
287
|
apiLevel: number;
|
|
289
288
|
profile: string;
|
|
@@ -298,6 +297,7 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
298
297
|
manufacturer: string;
|
|
299
298
|
model: string;
|
|
300
299
|
};
|
|
300
|
+
bundleId: string;
|
|
301
301
|
activityName?: string | undefined;
|
|
302
302
|
appLaunchOptions?: {
|
|
303
303
|
extras?: Record<string, string | number | boolean> | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../src/environment.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAAC;AAE5E,KAAK,qBAAqB,GAAG;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAQF,eAAO,MAAM,4BAA4B,GAAI,+BAG1C,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAM,KAAG,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../src/environment.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAAC;AAE5E,KAAK,qBAAqB,GAAG;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAQF,eAAO,MAAM,4BAA4B,GAAI,+BAG1C,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAM,KAAG,MAAM,GAAG,IAkBrD,CAAC;AAqQF,eAAO,MAAM,iBAAiB,GAC5B,MAAK,MAAM,CAAC,UAAwB,EACpC,UAAS,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAM,KAC/C,MAAM,GAAG,IAIX,CAAC;AAiBF,eAAO,MAAM,6BAA6B,GACxC,eAAc,MAAqB,KAClC,sBAUF,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,UAAU,MAAM,KAAG,MAE5D,CAAC;AAEF,eAAO,MAAM,4BAA4B,GACvC,UAAU,MAAM,EAChB,eAAc,sBAAwD,KACrE,MAEF,CAAC;AAEF,eAAO,MAAM,6BAA6B,GAAI,+CAI3C;IACD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,sBAAsB,CAAC;CAClC,KAAG,MAAM,EAed,CAAC;AAoBF,eAAO,MAAM,wBAAwB,GACnC,UAAU,SAAS,MAAM,EAAE,EAC3B,oCAIG,qBAA0B,KAC5B,OAAO,CAAC,MAAM,CA8BhB,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,UAAS,qBAA0B,KAClC,OAAO,CAAC,MAAM,CAEhB,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,UAAS,qBAA0B,KAClC,OAAO,CAAC,MAAM,CAEhB,CAAC;AAEF,eAAO,MAAM,qCAAqC,GAChD,UAAU,MAAM,EAChB,eAAc,sBAAwD,EACtE,UAAS,qBAA0B,KAClC,OAAO,CAAC,MAAM,CAQhB,CAAC;AAEF,eAAO,MAAM,iCAAiC,QAAa,OAAO,CAAC,MAAM,CAIxE,CAAC;AAEF,eAAO,MAAM,sCAAsC,QACvC,OAAO,CAAC,MAAM,CAIvB,CAAC;AAEJ,eAAO,MAAM,gCAAgC,GAC3C,UAAU,MAAM,KACf,OAAO,CAAC,MAAM,CAOhB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,MAAK,MAAM,CAAC,UAAwB,KACnC,MAAM,CAAC,UA4BT,CAAC;AAEF,eAAO,MAAM,2BAA2B,QAAO,IAE9C,CAAC;AAUF,eAAO,MAAM,gBAAgB,GAC3B,UAAS,MAAoC,KAC5C,MACwE,CAAC;AAE5E,eAAO,MAAM,qBAAqB,GAChC,UAAS,MAAoC,KAC5C,MACuE,CAAC;AAE3E,eAAO,MAAM,uBAAuB,GAClC,UAAS,MAAoC,KAC5C,MAMA,CAAC;AAEJ,eAAO,MAAM,uBAAuB,GAClC,UAAS,MAAoC,KAC5C,MAMA,CAAC"}
|
package/dist/environment.js
CHANGED
|
@@ -19,6 +19,9 @@ export const getDefaultUnixAndroidSdkRoot = ({ platform = process.platform, home
|
|
|
19
19
|
if (platform === 'linux') {
|
|
20
20
|
return path.join(homeDirectory, 'Android', 'Sdk');
|
|
21
21
|
}
|
|
22
|
+
if (platform === 'win32') {
|
|
23
|
+
return path.join(process.env.LOCALAPPDATA ?? path.join(homeDirectory, 'AppData', 'Local'), 'Android', 'Sdk');
|
|
24
|
+
}
|
|
22
25
|
return null;
|
|
23
26
|
};
|
|
24
27
|
const canBootstrapAndroidSdk = (platform = process.platform) => {
|
|
@@ -33,9 +36,8 @@ const pathExists = async (filePath) => {
|
|
|
33
36
|
return false;
|
|
34
37
|
}
|
|
35
38
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
39
|
+
// Enough "y" answers for every license prompt sdkmanager may show.
|
|
40
|
+
const SDK_MANAGER_CONSENT_ANSWERS = 100;
|
|
39
41
|
const downloadText = async (url) => {
|
|
40
42
|
return new Promise((resolve, reject) => {
|
|
41
43
|
const request = https.get(url, (response) => {
|
|
@@ -131,12 +133,10 @@ const ensureAndroidCommandLineTools = async (sdkRoot, platform = process.platfor
|
|
|
131
133
|
await rm(temporaryDirectory, { force: true, recursive: true });
|
|
132
134
|
}
|
|
133
135
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await spawn(
|
|
137
|
-
'
|
|
138
|
-
`yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell(sdkRoot)} --licenses >/dev/null`,
|
|
139
|
-
], {
|
|
136
|
+
// Answers "y" to every prompt, like piping `yes` into sdkmanager.
|
|
137
|
+
const runSdkManagerWithConsent = async (sdkRoot, args) => {
|
|
138
|
+
await spawn(getSdkManagerBinaryPath(sdkRoot), [`--sdk_root=${sdkRoot}`, ...args], {
|
|
139
|
+
stdin: { string: 'y\n'.repeat(SDK_MANAGER_CONSENT_ANSWERS) },
|
|
140
140
|
env: getAndroidProcessEnv({
|
|
141
141
|
...process.env,
|
|
142
142
|
ANDROID_HOME: sdkRoot,
|
|
@@ -144,6 +144,9 @@ const acceptAndroidLicenses = async (sdkRoot) => {
|
|
|
144
144
|
}),
|
|
145
145
|
});
|
|
146
146
|
};
|
|
147
|
+
const acceptAndroidLicenses = async (sdkRoot) => {
|
|
148
|
+
await runSdkManagerWithConsent(sdkRoot, ['--licenses']);
|
|
149
|
+
};
|
|
147
150
|
const getPackageVerificationPath = (sdkRoot, packageName) => {
|
|
148
151
|
if (packageName === 'platform-tools') {
|
|
149
152
|
return getAdbBinaryPath(sdkRoot);
|
|
@@ -176,22 +179,9 @@ const installAndroidSdkPackages = async (sdkRoot, packages) => {
|
|
|
176
179
|
if (packages.length === 0) {
|
|
177
180
|
return;
|
|
178
181
|
}
|
|
179
|
-
const sdkManagerBinaryPath = getSdkManagerBinaryPath(sdkRoot);
|
|
180
|
-
const packageArgs = packages
|
|
181
|
-
.map((packageName) => quoteShell(packageName))
|
|
182
|
-
.join(' ');
|
|
183
182
|
androidEnvironmentLogger.info('Installing missing Android SDK packages: %s', packages.join(', '));
|
|
184
183
|
await acceptAndroidLicenses(sdkRoot);
|
|
185
|
-
await
|
|
186
|
-
'-lc',
|
|
187
|
-
`yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell(sdkRoot)} ${packageArgs}`,
|
|
188
|
-
], {
|
|
189
|
-
env: getAndroidProcessEnv({
|
|
190
|
-
...process.env,
|
|
191
|
-
ANDROID_HOME: sdkRoot,
|
|
192
|
-
ANDROID_SDK_ROOT: sdkRoot,
|
|
193
|
-
}),
|
|
194
|
-
});
|
|
184
|
+
await runSdkManagerWithConsent(sdkRoot, packages);
|
|
195
185
|
};
|
|
196
186
|
export const getAndroidSdkRoot = (env = process.env, options = {}) => {
|
|
197
187
|
return (getConfiguredAndroidSdkRoot(env) ?? getDefaultUnixAndroidSdkRoot(options));
|
|
@@ -304,14 +294,17 @@ export const getAndroidProcessEnv = (env = process.env) => {
|
|
|
304
294
|
...env,
|
|
305
295
|
ANDROID_HOME: sdkRoot,
|
|
306
296
|
ANDROID_SDK_ROOT: sdkRoot,
|
|
307
|
-
ANDROID_AVD_HOME: path.join(os.homedir(), '.android', 'avd'),
|
|
297
|
+
ANDROID_AVD_HOME: env.ANDROID_AVD_HOME || path.join(os.homedir(), '.android', 'avd'),
|
|
308
298
|
PATH: pathEntries.join(path.delimiter),
|
|
309
299
|
};
|
|
310
300
|
};
|
|
311
301
|
export const initializeAndroidProcessEnv = () => {
|
|
312
302
|
Object.assign(process.env, getAndroidProcessEnv());
|
|
313
303
|
};
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
export const
|
|
304
|
+
// On Windows, SDK binaries ship as `.exe` and command-line tools as `.bat`.
|
|
305
|
+
// Spawning handles either form, but existence checks need the real file name.
|
|
306
|
+
const getSdkExecutableName = (name, windowsExtension, platform = process.platform) => (platform === 'win32' ? `${name}${windowsExtension}` : name);
|
|
307
|
+
export const getAdbBinaryPath = (sdkRoot = getRequiredAndroidSdkRoot()) => path.join(sdkRoot, 'platform-tools', getSdkExecutableName('adb', '.exe'));
|
|
308
|
+
export const getEmulatorBinaryPath = (sdkRoot = getRequiredAndroidSdkRoot()) => path.join(sdkRoot, 'emulator', getSdkExecutableName('emulator', '.exe'));
|
|
309
|
+
export const getSdkManagerBinaryPath = (sdkRoot = getRequiredAndroidSdkRoot()) => path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS, 'bin', getSdkExecutableName('sdkmanager', '.bat'));
|
|
310
|
+
export const getAvdManagerBinaryPath = (sdkRoot = getRequiredAndroidSdkRoot()) => path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS, 'bin', getSdkExecutableName('avdmanager', '.bat'));
|
package/dist/instance.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,EACL,qBAAqB,EAGtB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,EACL,qBAAqB,EAGtB,MAAM,aAAa,CAAC;AAmLrB,eAAO,MAAM,kCAAkC,GAC7C,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,MAAM,0BAA0B,KAC/B,OAAO,CAAC,qBAAqB,CAoI/B,CAAC;AAEF,eAAO,MAAM,wCAAwC,GACnD,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,OAAO,0BAA0B,KAChC,OAAO,CAAC,qBAAqB,CA+D/B,CAAC"}
|
package/dist/instance.js
CHANGED
|
@@ -33,11 +33,17 @@ const getOptionalHarnessAppPath = () => {
|
|
|
33
33
|
}
|
|
34
34
|
return appPath;
|
|
35
35
|
};
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// ADB reconnects can remove device-side tunnels between app launches.
|
|
37
|
+
const restorePortForwarding = async (adbId, metroPort) => {
|
|
38
38
|
await Promise.all([
|
|
39
39
|
adbModule.reversePort(adbId, metroPort),
|
|
40
40
|
adbModule.reversePort(adbId, 8080),
|
|
41
|
+
]);
|
|
42
|
+
};
|
|
43
|
+
const configureAndroidRuntime = async (adbId, config, harnessConfig) => {
|
|
44
|
+
const metroPort = harnessConfig.metroPort;
|
|
45
|
+
await Promise.all([
|
|
46
|
+
restorePortForwarding(adbId, metroPort),
|
|
41
47
|
adbModule.setHideErrorDialogs(adbId, true),
|
|
42
48
|
applyHarnessDebugHttpHost(adbId, config.bundleId, `localhost:${metroPort}`),
|
|
43
49
|
]);
|
|
@@ -163,6 +169,7 @@ export const getAndroidEmulatorPlatformInstance = async (config, harnessConfig,
|
|
|
163
169
|
return {
|
|
164
170
|
createAppSession: async (options) => {
|
|
165
171
|
await adb.stopApp(adbId, config.bundleId);
|
|
172
|
+
await restorePortForwarding(adbId, harnessConfig.metroPort);
|
|
166
173
|
const launchOptions = options ??
|
|
167
174
|
config.appLaunchOptions;
|
|
168
175
|
return await createAndroidAppSession({
|
|
@@ -208,6 +215,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async (config, harnessCo
|
|
|
208
215
|
return {
|
|
209
216
|
createAppSession: async (options) => {
|
|
210
217
|
await adb.stopApp(adbId, config.bundleId);
|
|
218
|
+
await restorePortForwarding(adbId, harnessConfig.metroPort);
|
|
211
219
|
const launchOptions = options ??
|
|
212
220
|
config.appLaunchOptions;
|
|
213
221
|
return await createAndroidAppSession({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/platform-android",
|
|
3
3
|
"description": "Android platform for React Native Harness",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"vite": "^7.2.2",
|
|
20
20
|
"zod": "^3.25.67",
|
|
21
21
|
"tslib": "^2.3.0",
|
|
22
|
-
"@react-native-harness/
|
|
23
|
-
"@react-native-harness/platforms": "1.
|
|
24
|
-
"@react-native-harness/
|
|
22
|
+
"@react-native-harness/config": "1.7.0",
|
|
23
|
+
"@react-native-harness/platforms": "1.7.0",
|
|
24
|
+
"@react-native-harness/tools": "1.7.0"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT"
|
|
27
27
|
}
|
package/src/adb.ts
CHANGED
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
} from '@react-native-harness/tools';
|
|
11
11
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
12
12
|
import type { ChildProcessByStdio } from 'node:child_process';
|
|
13
|
-
import { access, rm } from 'node:fs/promises';
|
|
13
|
+
import { access, appendFile, rm, writeFile } from 'node:fs/promises';
|
|
14
14
|
import type { Readable } from 'node:stream';
|
|
15
15
|
import {
|
|
16
16
|
getAvdConfigPath,
|
|
17
17
|
getAvdDirectory,
|
|
18
|
+
getAvdIniPath,
|
|
18
19
|
readAvdConfig,
|
|
19
20
|
} from './avd-config.js';
|
|
20
21
|
import {
|
|
@@ -201,12 +202,6 @@ const ensureAvdConfigExists = async (name: string): Promise<string> => {
|
|
|
201
202
|
return configPath;
|
|
202
203
|
};
|
|
203
204
|
|
|
204
|
-
const getAvdIniPath = (name: string): string => {
|
|
205
|
-
const avdHome =
|
|
206
|
-
process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`;
|
|
207
|
-
return `${avdHome}/${name}.ini`;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
205
|
const ensureAvdIniExists = async ({
|
|
211
206
|
name,
|
|
212
207
|
apiLevel,
|
|
@@ -217,10 +212,16 @@ const ensureAvdIniExists = async ({
|
|
|
217
212
|
const iniPath = getAvdIniPath(name);
|
|
218
213
|
|
|
219
214
|
const avdDirectory = getAvdDirectory(name);
|
|
220
|
-
await
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
215
|
+
await writeFile(
|
|
216
|
+
iniPath,
|
|
217
|
+
[
|
|
218
|
+
'avd.ini.encoding=UTF-8',
|
|
219
|
+
`path=${avdDirectory}`,
|
|
220
|
+
`path.rel=avd/${name}.avd`,
|
|
221
|
+
`target=android-${apiLevel}`,
|
|
222
|
+
'',
|
|
223
|
+
].join('\n')
|
|
224
|
+
);
|
|
224
225
|
|
|
225
226
|
return iniPath;
|
|
226
227
|
};
|
|
@@ -500,12 +501,24 @@ export const createAvd = async ({
|
|
|
500
501
|
|
|
501
502
|
await verifyAndroidEmulatorSdk(apiLevel);
|
|
502
503
|
await ensureAvdProfileAvailable(profile);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
504
|
+
// avdmanager asks whether to create a custom hardware profile; answer "no".
|
|
505
|
+
await spawn(
|
|
506
|
+
getAvdManagerBinaryPath(),
|
|
507
|
+
[
|
|
508
|
+
'create',
|
|
509
|
+
'avd',
|
|
510
|
+
'--force',
|
|
511
|
+
'--name',
|
|
512
|
+
name,
|
|
513
|
+
'--package',
|
|
514
|
+
systemImagePackage,
|
|
515
|
+
'--device',
|
|
516
|
+
profile,
|
|
517
|
+
'-p',
|
|
518
|
+
getAvdDirectory(name),
|
|
519
|
+
],
|
|
520
|
+
{ stdin: { string: 'no\n' } }
|
|
521
|
+
);
|
|
509
522
|
await ensureAvdIniExists({ name, apiLevel });
|
|
510
523
|
const configPath = await ensureAvdConfigExists(name);
|
|
511
524
|
// hw.cpu.ncore is baked into config.ini (rather than passed as a `-cores`
|
|
@@ -513,30 +526,25 @@ export const createAvd = async ({
|
|
|
513
526
|
// snapshots require an identical hardware config to load, so the vCPU
|
|
514
527
|
// count must be fixed at AVD-creation time and stay consistent across
|
|
515
528
|
// boots.
|
|
516
|
-
await
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
529
|
+
await appendFile(
|
|
530
|
+
configPath,
|
|
531
|
+
[
|
|
532
|
+
`disk.dataPartition.size=${diskSize}`,
|
|
533
|
+
`vm.heapSize=${heapSize}`,
|
|
534
|
+
`hw.cpu.ncore=${getEmulatorCpuCores()}`,
|
|
535
|
+
'',
|
|
536
|
+
].join('\n')
|
|
537
|
+
);
|
|
520
538
|
};
|
|
521
539
|
|
|
522
540
|
export const deleteAvd = async (name: string): Promise<void> => {
|
|
523
|
-
await rm(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
);
|
|
532
|
-
await rm(
|
|
533
|
-
`${
|
|
534
|
-
process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
|
|
535
|
-
}/${name}.ini`,
|
|
536
|
-
{
|
|
537
|
-
force: true,
|
|
538
|
-
}
|
|
539
|
-
);
|
|
541
|
+
await rm(getAvdDirectory(name), {
|
|
542
|
+
force: true,
|
|
543
|
+
recursive: true,
|
|
544
|
+
});
|
|
545
|
+
await rm(getAvdIniPath(name), {
|
|
546
|
+
force: true,
|
|
547
|
+
});
|
|
540
548
|
};
|
|
541
549
|
|
|
542
550
|
export const startEmulator = async (
|
package/src/avd-config.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import type { AndroidSystemImageArch } from './environment.js';
|
|
3
5
|
import type { AndroidEmulator, AndroidEmulatorAVDConfig } from './config.js';
|
|
4
6
|
import { getEmulatorCpuCores } from './emulator-startup.js';
|
|
@@ -16,14 +18,22 @@ export type AvdCompatibilityResult =
|
|
|
16
18
|
| { compatible: true }
|
|
17
19
|
| { compatible: false; reason: string };
|
|
18
20
|
|
|
21
|
+
export const getAvdHome = (): string => {
|
|
22
|
+
return (
|
|
23
|
+
process.env.ANDROID_AVD_HOME ?? path.join(os.homedir(), '.android', 'avd')
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
19
27
|
export const getAvdDirectory = (name: string): string => {
|
|
20
|
-
return `${
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
return path.join(getAvdHome(), `${name}.avd`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getAvdIniPath = (name: string): string => {
|
|
32
|
+
return path.join(getAvdHome(), `${name}.ini`);
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
export const getAvdConfigPath = (name: string): string => {
|
|
26
|
-
return
|
|
36
|
+
return path.join(getAvdDirectory(name), 'config.ini');
|
|
27
37
|
};
|
|
28
38
|
|
|
29
39
|
const normalizeAvdValue = (value: string | undefined): string | undefined => {
|
package/src/environment.ts
CHANGED
|
@@ -38,6 +38,14 @@ export const getDefaultUnixAndroidSdkRoot = ({
|
|
|
38
38
|
return path.join(homeDirectory, 'Android', 'Sdk');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
if (platform === 'win32') {
|
|
42
|
+
return path.join(
|
|
43
|
+
process.env.LOCALAPPDATA ?? path.join(homeDirectory, 'AppData', 'Local'),
|
|
44
|
+
'Android',
|
|
45
|
+
'Sdk',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
return null;
|
|
42
50
|
};
|
|
43
51
|
|
|
@@ -56,9 +64,8 @@ const pathExists = async (filePath: string): Promise<boolean> => {
|
|
|
56
64
|
}
|
|
57
65
|
};
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
67
|
+
// Enough "y" answers for every license prompt sdkmanager may show.
|
|
68
|
+
const SDK_MANAGER_CONSENT_ANSWERS = 100;
|
|
62
69
|
|
|
63
70
|
const downloadText = async (url: string): Promise<string> => {
|
|
64
71
|
return new Promise((resolve, reject) => {
|
|
@@ -217,18 +224,16 @@ const ensureAndroidCommandLineTools = async (
|
|
|
217
224
|
}
|
|
218
225
|
};
|
|
219
226
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
// Answers "y" to every prompt, like piping `yes` into sdkmanager.
|
|
228
|
+
const runSdkManagerWithConsent = async (
|
|
229
|
+
sdkRoot: string,
|
|
230
|
+
args: readonly string[],
|
|
231
|
+
): Promise<void> => {
|
|
223
232
|
await spawn(
|
|
224
|
-
|
|
225
|
-
[
|
|
226
|
-
'-lc',
|
|
227
|
-
`yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell(
|
|
228
|
-
sdkRoot,
|
|
229
|
-
)} --licenses >/dev/null`,
|
|
230
|
-
],
|
|
233
|
+
getSdkManagerBinaryPath(sdkRoot),
|
|
234
|
+
[`--sdk_root=${sdkRoot}`, ...args],
|
|
231
235
|
{
|
|
236
|
+
stdin: { string: 'y\n'.repeat(SDK_MANAGER_CONSENT_ANSWERS) },
|
|
232
237
|
env: getAndroidProcessEnv({
|
|
233
238
|
...process.env,
|
|
234
239
|
ANDROID_HOME: sdkRoot,
|
|
@@ -238,6 +243,10 @@ const acceptAndroidLicenses = async (sdkRoot: string): Promise<void> => {
|
|
|
238
243
|
);
|
|
239
244
|
};
|
|
240
245
|
|
|
246
|
+
const acceptAndroidLicenses = async (sdkRoot: string): Promise<void> => {
|
|
247
|
+
await runSdkManagerWithConsent(sdkRoot, ['--licenses']);
|
|
248
|
+
};
|
|
249
|
+
|
|
241
250
|
const getPackageVerificationPath = (
|
|
242
251
|
sdkRoot: string,
|
|
243
252
|
packageName: string,
|
|
@@ -290,33 +299,13 @@ const installAndroidSdkPackages = async (
|
|
|
290
299
|
return;
|
|
291
300
|
}
|
|
292
301
|
|
|
293
|
-
const sdkManagerBinaryPath = getSdkManagerBinaryPath(sdkRoot);
|
|
294
|
-
const packageArgs = packages
|
|
295
|
-
.map((packageName) => quoteShell(packageName))
|
|
296
|
-
.join(' ');
|
|
297
|
-
|
|
298
302
|
androidEnvironmentLogger.info(
|
|
299
303
|
'Installing missing Android SDK packages: %s',
|
|
300
304
|
packages.join(', '),
|
|
301
305
|
);
|
|
302
306
|
|
|
303
307
|
await acceptAndroidLicenses(sdkRoot);
|
|
304
|
-
await
|
|
305
|
-
'bash',
|
|
306
|
-
[
|
|
307
|
-
'-lc',
|
|
308
|
-
`yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell(
|
|
309
|
-
sdkRoot,
|
|
310
|
-
)} ${packageArgs}`,
|
|
311
|
-
],
|
|
312
|
-
{
|
|
313
|
-
env: getAndroidProcessEnv({
|
|
314
|
-
...process.env,
|
|
315
|
-
ANDROID_HOME: sdkRoot,
|
|
316
|
-
ANDROID_SDK_ROOT: sdkRoot,
|
|
317
|
-
}),
|
|
318
|
-
},
|
|
319
|
-
);
|
|
308
|
+
await runSdkManagerWithConsent(sdkRoot, packages);
|
|
320
309
|
};
|
|
321
310
|
|
|
322
311
|
export const getAndroidSdkRoot = (
|
|
@@ -526,7 +515,8 @@ export const getAndroidProcessEnv = (
|
|
|
526
515
|
...env,
|
|
527
516
|
ANDROID_HOME: sdkRoot,
|
|
528
517
|
ANDROID_SDK_ROOT: sdkRoot,
|
|
529
|
-
ANDROID_AVD_HOME:
|
|
518
|
+
ANDROID_AVD_HOME:
|
|
519
|
+
env.ANDROID_AVD_HOME || path.join(os.homedir(), '.android', 'avd'),
|
|
530
520
|
PATH: pathEntries.join(path.delimiter),
|
|
531
521
|
};
|
|
532
522
|
};
|
|
@@ -535,20 +525,40 @@ export const initializeAndroidProcessEnv = (): void => {
|
|
|
535
525
|
Object.assign(process.env, getAndroidProcessEnv());
|
|
536
526
|
};
|
|
537
527
|
|
|
528
|
+
// On Windows, SDK binaries ship as `.exe` and command-line tools as `.bat`.
|
|
529
|
+
// Spawning handles either form, but existence checks need the real file name.
|
|
530
|
+
const getSdkExecutableName = (
|
|
531
|
+
name: string,
|
|
532
|
+
windowsExtension: '.exe' | '.bat',
|
|
533
|
+
platform: NodeJS.Platform = process.platform,
|
|
534
|
+
): string => (platform === 'win32' ? `${name}${windowsExtension}` : name);
|
|
535
|
+
|
|
538
536
|
export const getAdbBinaryPath = (
|
|
539
537
|
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
540
|
-
): string =>
|
|
538
|
+
): string =>
|
|
539
|
+
path.join(sdkRoot, 'platform-tools', getSdkExecutableName('adb', '.exe'));
|
|
541
540
|
|
|
542
541
|
export const getEmulatorBinaryPath = (
|
|
543
542
|
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
544
|
-
): string =>
|
|
543
|
+
): string =>
|
|
544
|
+
path.join(sdkRoot, 'emulator', getSdkExecutableName('emulator', '.exe'));
|
|
545
545
|
|
|
546
546
|
export const getSdkManagerBinaryPath = (
|
|
547
547
|
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
548
548
|
): string =>
|
|
549
|
-
path.join(
|
|
549
|
+
path.join(
|
|
550
|
+
sdkRoot,
|
|
551
|
+
...CMDLINE_TOOLS_PATH_SEGMENTS,
|
|
552
|
+
'bin',
|
|
553
|
+
getSdkExecutableName('sdkmanager', '.bat'),
|
|
554
|
+
);
|
|
550
555
|
|
|
551
556
|
export const getAvdManagerBinaryPath = (
|
|
552
557
|
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
553
558
|
): string =>
|
|
554
|
-
path.join(
|
|
559
|
+
path.join(
|
|
560
|
+
sdkRoot,
|
|
561
|
+
...CMDLINE_TOOLS_PATH_SEGMENTS,
|
|
562
|
+
'bin',
|
|
563
|
+
getSdkExecutableName('avdmanager', '.bat'),
|
|
564
|
+
);
|
package/src/instance.ts
CHANGED
|
@@ -64,6 +64,17 @@ const getOptionalHarnessAppPath = (): string | undefined => {
|
|
|
64
64
|
return appPath;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
// ADB reconnects can remove device-side tunnels between app launches.
|
|
68
|
+
const restorePortForwarding = async (
|
|
69
|
+
adbId: string,
|
|
70
|
+
metroPort: number
|
|
71
|
+
): Promise<void> => {
|
|
72
|
+
await Promise.all([
|
|
73
|
+
adbModule.reversePort(adbId, metroPort),
|
|
74
|
+
adbModule.reversePort(adbId, 8080),
|
|
75
|
+
]);
|
|
76
|
+
};
|
|
77
|
+
|
|
67
78
|
const configureAndroidRuntime = async (
|
|
68
79
|
adbId: string,
|
|
69
80
|
config: AndroidPlatformConfig,
|
|
@@ -72,8 +83,7 @@ const configureAndroidRuntime = async (
|
|
|
72
83
|
const metroPort = harnessConfig.metroPort;
|
|
73
84
|
|
|
74
85
|
await Promise.all([
|
|
75
|
-
|
|
76
|
-
adbModule.reversePort(adbId, 8080),
|
|
86
|
+
restorePortForwarding(adbId, metroPort),
|
|
77
87
|
adbModule.setHideErrorDialogs(adbId, true),
|
|
78
88
|
applyHarnessDebugHttpHost(adbId, config.bundleId, `localhost:${metroPort}`),
|
|
79
89
|
]);
|
|
@@ -280,6 +290,7 @@ export const getAndroidEmulatorPlatformInstance = async (
|
|
|
280
290
|
return {
|
|
281
291
|
createAppSession: async (options) => {
|
|
282
292
|
await adb.stopApp(adbId, config.bundleId);
|
|
293
|
+
await restorePortForwarding(adbId, harnessConfig.metroPort);
|
|
283
294
|
const launchOptions =
|
|
284
295
|
(options as typeof config.appLaunchOptions | undefined) ??
|
|
285
296
|
config.appLaunchOptions;
|
|
@@ -353,6 +364,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async (
|
|
|
353
364
|
return {
|
|
354
365
|
createAppSession: async (options) => {
|
|
355
366
|
await adb.stopApp(adbId, config.bundleId);
|
|
367
|
+
await restorePortForwarding(adbId, harnessConfig.metroPort);
|
|
356
368
|
const launchOptions =
|
|
357
369
|
(options as typeof config.appLaunchOptions | undefined) ??
|
|
358
370
|
config.appLaunchOptions;
|
package/dist/app-monitor.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type AppMonitor, type AppCrashDetails, type CrashArtifactWriter, type CrashDetailsLookupOptions, type AppMonitorEvent } from '@react-native-harness/platforms';
|
|
2
|
-
declare const createAndroidLogEvent: (line: string, bundleId: string) => AppMonitorEvent | null;
|
|
3
|
-
export declare const createAndroidAppMonitor: ({ adbId, bundleId, appUid, crashArtifactWriter, }: {
|
|
4
|
-
adbId: string;
|
|
5
|
-
bundleId: string;
|
|
6
|
-
appUid: number;
|
|
7
|
-
crashArtifactWriter?: CrashArtifactWriter;
|
|
8
|
-
}) => AndroidAppMonitor;
|
|
9
|
-
export { createAndroidLogEvent };
|
|
10
|
-
export type AndroidAppMonitor = AppMonitor & {
|
|
11
|
-
getCrashDetails: (options: CrashDetailsLookupOptions) => Promise<AppCrashDetails | null>;
|
|
12
|
-
};
|
|
13
|
-
//# sourceMappingURL=app-monitor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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;AA+SzC,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,iBA4KH,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
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
import { escapeRegExp, getEmitter, logger, SubprocessError, } from '@react-native-harness/tools';
|
|
2
|
-
import * as adb from './adb.js';
|
|
3
|
-
import { androidCrashParser } from './crash-parser.js';
|
|
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
|
-
];
|
|
15
|
-
const MAX_RECENT_LOG_LINES = 200;
|
|
16
|
-
const MAX_RECENT_CRASH_ARTIFACTS = 10;
|
|
17
|
-
const CRASH_ARTIFACT_SETTLE_DELAY_MS = 100;
|
|
18
|
-
const startProcPattern = (bundleId) => new RegExp(`Start proc (\\d+):${escapeRegExp(bundleId)}(?:/|\\s)`);
|
|
19
|
-
const processPattern = (bundleId) => new RegExp(`Process:\\s*${escapeRegExp(bundleId)},\\s*PID:\\s*(\\d+)`);
|
|
20
|
-
const nativeCrashPattern = (bundleId) => new RegExp(`>>>\\s*${escapeRegExp(bundleId)}\\s*<<<`);
|
|
21
|
-
const processDiedPattern = (bundleId) => new RegExp(`Process\\s+${escapeRegExp(bundleId)}\\s+\\(pid\\s+(\\d+)\\)\\s+has\\s+died`, 'i');
|
|
22
|
-
const getSignal = (line) => {
|
|
23
|
-
const namedSignalMatch = line.match(/\b(SIG[A-Z0-9]+)\b/);
|
|
24
|
-
if (namedSignalMatch) {
|
|
25
|
-
return namedSignalMatch[1];
|
|
26
|
-
}
|
|
27
|
-
const signalNumberMatch = line.match(/signal\s+(\d+)/i);
|
|
28
|
-
if (signalNumberMatch) {
|
|
29
|
-
return `signal ${signalNumberMatch[1]}`;
|
|
30
|
-
}
|
|
31
|
-
return undefined;
|
|
32
|
-
};
|
|
33
|
-
const getAndroidLogLineCrashDetails = ({ line, bundleId, pid, }) => {
|
|
34
|
-
const fatalExceptionMatch = line.match(/FATAL EXCEPTION:\s*(.+)$/i);
|
|
35
|
-
const processMatch = line.match(processPattern(bundleId));
|
|
36
|
-
return {
|
|
37
|
-
source: 'logs',
|
|
38
|
-
summary: line.trim(),
|
|
39
|
-
signal: getSignal(line),
|
|
40
|
-
exceptionType: fatalExceptionMatch?.[1]?.trim(),
|
|
41
|
-
processName: processMatch
|
|
42
|
-
? bundleId
|
|
43
|
-
: line.includes(bundleId)
|
|
44
|
-
? bundleId
|
|
45
|
-
: undefined,
|
|
46
|
-
pid: pid ?? (processMatch ? Number(processMatch[1]) : undefined),
|
|
47
|
-
rawLines: [line],
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
const CRASH_BLOCK_HEADER = '--------- beginning of crash';
|
|
51
|
-
const getLatestCrashBlock = (recentLogLines) => {
|
|
52
|
-
const lines = recentLogLines.map(({ line }) => line);
|
|
53
|
-
let latestCrashHeaderIndex = -1;
|
|
54
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
55
|
-
if (/FATAL EXCEPTION:|Process:\s+.+,\s+PID:/i.test(lines[index])) {
|
|
56
|
-
latestCrashHeaderIndex = index;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const blockStartIndex = Math.max(lines.lastIndexOf(CRASH_BLOCK_HEADER), latestCrashHeaderIndex);
|
|
61
|
-
if (blockStartIndex === -1) {
|
|
62
|
-
return lines;
|
|
63
|
-
}
|
|
64
|
-
return lines.slice(blockStartIndex);
|
|
65
|
-
};
|
|
66
|
-
const getCrashBlockForArtifact = ({ artifact, recentLogLines, }) => {
|
|
67
|
-
const targetIndex = recentLogLines.findIndex(({ line, occurredAt }) => line === artifact.triggerLine &&
|
|
68
|
-
(artifact.triggerOccurredAt === undefined ||
|
|
69
|
-
occurredAt === artifact.triggerOccurredAt));
|
|
70
|
-
if (targetIndex === -1) {
|
|
71
|
-
return artifact.rawLines ?? [];
|
|
72
|
-
}
|
|
73
|
-
let blockStartIndex = targetIndex;
|
|
74
|
-
for (let index = targetIndex; index >= 0; index -= 1) {
|
|
75
|
-
const { line } = recentLogLines[index];
|
|
76
|
-
if (line === CRASH_BLOCK_HEADER) {
|
|
77
|
-
blockStartIndex = index;
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
let blockEndIndex = recentLogLines.length;
|
|
82
|
-
for (let index = targetIndex + 1; index < recentLogLines.length; index += 1) {
|
|
83
|
-
if (recentLogLines[index].line === CRASH_BLOCK_HEADER) {
|
|
84
|
-
blockEndIndex = index;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return recentLogLines
|
|
89
|
-
.slice(blockStartIndex, blockEndIndex)
|
|
90
|
-
.map(({ line }) => line);
|
|
91
|
-
};
|
|
92
|
-
const hydrateCrashArtifact = ({ artifact, recentLogLines, }) => {
|
|
93
|
-
const rawLines = getCrashBlockForArtifact({ artifact, recentLogLines });
|
|
94
|
-
if (rawLines.length === 0) {
|
|
95
|
-
return artifact;
|
|
96
|
-
}
|
|
97
|
-
const parsedDetails = androidCrashParser.parse({
|
|
98
|
-
contents: rawLines.join('\n'),
|
|
99
|
-
bundleId: artifact.processName ?? '',
|
|
100
|
-
pid: artifact.pid,
|
|
101
|
-
});
|
|
102
|
-
return {
|
|
103
|
-
...artifact,
|
|
104
|
-
...parsedDetails,
|
|
105
|
-
artifactType: artifact.artifactType,
|
|
106
|
-
artifactPath: artifact.artifactPath,
|
|
107
|
-
rawLines,
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
const createCrashArtifact = ({ details, recentLogLines, }) => {
|
|
111
|
-
const occurredAt = Date.now();
|
|
112
|
-
const rawLines = getLatestCrashBlock(recentLogLines);
|
|
113
|
-
const triggerOccurredAt = [...recentLogLines]
|
|
114
|
-
.reverse()
|
|
115
|
-
.find(({ line }) => line === details.summary)?.occurredAt;
|
|
116
|
-
const contents = rawLines.length > 0
|
|
117
|
-
? rawLines.join('\n')
|
|
118
|
-
: (details.rawLines ?? []).join('\n');
|
|
119
|
-
const parsedDetails = details.processName !== undefined
|
|
120
|
-
? androidCrashParser.parse({
|
|
121
|
-
contents,
|
|
122
|
-
bundleId: details.processName,
|
|
123
|
-
pid: details.pid,
|
|
124
|
-
})
|
|
125
|
-
: details;
|
|
126
|
-
return {
|
|
127
|
-
...parsedDetails,
|
|
128
|
-
occurredAt,
|
|
129
|
-
triggerLine: details.summary ?? '',
|
|
130
|
-
triggerOccurredAt,
|
|
131
|
-
artifactType: 'logcat',
|
|
132
|
-
rawLines: rawLines.length > 0
|
|
133
|
-
? rawLines
|
|
134
|
-
: parsedDetails.rawLines ?? details.rawLines,
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
const persistCrashArtifact = ({ details, crashArtifactWriter, }) => {
|
|
138
|
-
if (!crashArtifactWriter || details.artifactType !== 'logcat') {
|
|
139
|
-
return details;
|
|
140
|
-
}
|
|
141
|
-
const artifactBody = details.rawLines?.join('\n');
|
|
142
|
-
if (!artifactBody) {
|
|
143
|
-
return details;
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
...details,
|
|
147
|
-
artifactPath: crashArtifactWriter.persistArtifact({
|
|
148
|
-
artifactKind: details.artifactType,
|
|
149
|
-
source: {
|
|
150
|
-
kind: 'text',
|
|
151
|
-
fileName: 'logcat.txt',
|
|
152
|
-
text: `${artifactBody}\n`,
|
|
153
|
-
},
|
|
154
|
-
}),
|
|
155
|
-
};
|
|
156
|
-
};
|
|
157
|
-
const getLatestCrashArtifact = ({ crashArtifacts, recentLogLines, processName, pid, occurredAt, }) => {
|
|
158
|
-
const matchingByPid = pid
|
|
159
|
-
? crashArtifacts.filter((artifact) => artifact.pid === pid)
|
|
160
|
-
: [];
|
|
161
|
-
const matchingByProcess = processName
|
|
162
|
-
? crashArtifacts.filter((artifact) => artifact.processName === processName)
|
|
163
|
-
: [];
|
|
164
|
-
const candidates = matchingByPid.length > 0
|
|
165
|
-
? matchingByPid
|
|
166
|
-
: matchingByProcess.length > 0
|
|
167
|
-
? matchingByProcess
|
|
168
|
-
: crashArtifacts;
|
|
169
|
-
const sortedCandidates = [...candidates].sort((left, right) => Math.abs(left.occurredAt - occurredAt) -
|
|
170
|
-
Math.abs(right.occurredAt - occurredAt));
|
|
171
|
-
const artifact = sortedCandidates[0];
|
|
172
|
-
if (!artifact) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
return hydrateCrashArtifact({
|
|
176
|
-
artifact,
|
|
177
|
-
recentLogLines,
|
|
178
|
-
});
|
|
179
|
-
};
|
|
180
|
-
const createAndroidLogEvent = (line, bundleId) => {
|
|
181
|
-
const startMatch = line.match(startProcPattern(bundleId));
|
|
182
|
-
if (startMatch) {
|
|
183
|
-
return {
|
|
184
|
-
type: 'app_started',
|
|
185
|
-
pid: Number(startMatch[1]),
|
|
186
|
-
source: 'logs',
|
|
187
|
-
line,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
const processMatch = line.match(processPattern(bundleId));
|
|
191
|
-
if (processMatch) {
|
|
192
|
-
return {
|
|
193
|
-
type: 'possible_crash',
|
|
194
|
-
pid: Number(processMatch[1]),
|
|
195
|
-
source: 'logs',
|
|
196
|
-
line,
|
|
197
|
-
crashDetails: getAndroidLogLineCrashDetails({
|
|
198
|
-
line,
|
|
199
|
-
bundleId,
|
|
200
|
-
pid: Number(processMatch[1]),
|
|
201
|
-
}),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
if (nativeCrashPattern(bundleId).test(line)) {
|
|
205
|
-
return {
|
|
206
|
-
type: 'possible_crash',
|
|
207
|
-
source: 'logs',
|
|
208
|
-
line,
|
|
209
|
-
crashDetails: getAndroidLogLineCrashDetails({
|
|
210
|
-
line,
|
|
211
|
-
bundleId,
|
|
212
|
-
}),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
const diedMatch = line.match(processDiedPattern(bundleId));
|
|
216
|
-
if (diedMatch) {
|
|
217
|
-
return {
|
|
218
|
-
type: 'app_exited',
|
|
219
|
-
pid: Number(diedMatch[1]),
|
|
220
|
-
source: 'logs',
|
|
221
|
-
line,
|
|
222
|
-
crashDetails: getAndroidLogLineCrashDetails({
|
|
223
|
-
line,
|
|
224
|
-
bundleId,
|
|
225
|
-
pid: Number(diedMatch[1]),
|
|
226
|
-
}),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (line.includes(bundleId) &&
|
|
230
|
-
/fatal|crash|signal 11|signal 6|backtrace/i.test(line)) {
|
|
231
|
-
return {
|
|
232
|
-
type: 'possible_crash',
|
|
233
|
-
source: 'logs',
|
|
234
|
-
line,
|
|
235
|
-
crashDetails: getAndroidLogLineCrashDetails({
|
|
236
|
-
line,
|
|
237
|
-
bundleId,
|
|
238
|
-
}),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
};
|
|
243
|
-
export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifactWriter, }) => {
|
|
244
|
-
const emitter = getEmitter();
|
|
245
|
-
let isStarted = false;
|
|
246
|
-
let logcatProcess = null;
|
|
247
|
-
let logTask = null;
|
|
248
|
-
let recentLogLines = [];
|
|
249
|
-
let recentCrashArtifacts = [];
|
|
250
|
-
const emit = (event) => {
|
|
251
|
-
emitter.emit(event);
|
|
252
|
-
};
|
|
253
|
-
const recordLogLine = (line) => {
|
|
254
|
-
recentLogLines = [
|
|
255
|
-
...recentLogLines,
|
|
256
|
-
{ line, occurredAt: Date.now() },
|
|
257
|
-
].slice(-MAX_RECENT_LOG_LINES);
|
|
258
|
-
};
|
|
259
|
-
const recordCrashArtifact = (details) => {
|
|
260
|
-
if (!details) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
recentCrashArtifacts = [
|
|
264
|
-
...recentCrashArtifacts,
|
|
265
|
-
createCrashArtifact({
|
|
266
|
-
details,
|
|
267
|
-
recentLogLines,
|
|
268
|
-
}),
|
|
269
|
-
].slice(-MAX_RECENT_CRASH_ARTIFACTS);
|
|
270
|
-
};
|
|
271
|
-
const stopProcess = async (child) => {
|
|
272
|
-
if (!child) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
(await child.nodeChildProcess).kill();
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
// Ignore termination failures for background monitors.
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
const startLogcat = async () => {
|
|
283
|
-
const logcatTimestamp = await adb.getLogcatTimestamp(adbId);
|
|
284
|
-
logcatProcess = adb.startLogcat(adbId, getLogcatArgs(appUid, logcatTimestamp));
|
|
285
|
-
const currentProcess = logcatProcess;
|
|
286
|
-
if (!currentProcess) {
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
logTask = (async () => {
|
|
290
|
-
try {
|
|
291
|
-
for await (const line of currentProcess) {
|
|
292
|
-
recordLogLine(line);
|
|
293
|
-
emit({ type: 'log', source: 'logs', line });
|
|
294
|
-
const event = createAndroidLogEvent(line, bundleId);
|
|
295
|
-
if (event) {
|
|
296
|
-
if (event.type === 'possible_crash' ||
|
|
297
|
-
event.type === 'app_exited') {
|
|
298
|
-
recordCrashArtifact(event.crashDetails);
|
|
299
|
-
}
|
|
300
|
-
emit(event);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
catch (error) {
|
|
305
|
-
if (!(error instanceof SubprocessError && error.signalName === 'SIGTERM')) {
|
|
306
|
-
androidAppMonitorLogger.debug('Android logcat monitor stopped', error);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
})();
|
|
310
|
-
};
|
|
311
|
-
const start = async () => {
|
|
312
|
-
if (isStarted) {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
try {
|
|
316
|
-
await startLogcat();
|
|
317
|
-
isStarted = true;
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
const currentProcess = logcatProcess;
|
|
321
|
-
const currentTask = logTask;
|
|
322
|
-
logcatProcess = null;
|
|
323
|
-
logTask = null;
|
|
324
|
-
await stopProcess(currentProcess);
|
|
325
|
-
await currentTask;
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
const stop = async () => {
|
|
330
|
-
if (!isStarted) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
isStarted = false;
|
|
334
|
-
const currentProcess = logcatProcess;
|
|
335
|
-
const currentTask = logTask;
|
|
336
|
-
logcatProcess = null;
|
|
337
|
-
logTask = null;
|
|
338
|
-
await stopProcess(currentProcess);
|
|
339
|
-
await currentTask;
|
|
340
|
-
};
|
|
341
|
-
const dispose = async () => {
|
|
342
|
-
await stop();
|
|
343
|
-
emitter.clearAllListeners();
|
|
344
|
-
recentLogLines = [];
|
|
345
|
-
recentCrashArtifacts = [];
|
|
346
|
-
};
|
|
347
|
-
const addListener = (listener) => {
|
|
348
|
-
emitter.addListener(listener);
|
|
349
|
-
};
|
|
350
|
-
const removeListener = (listener) => {
|
|
351
|
-
emitter.removeListener(listener);
|
|
352
|
-
};
|
|
353
|
-
return {
|
|
354
|
-
start,
|
|
355
|
-
stop,
|
|
356
|
-
dispose,
|
|
357
|
-
addListener,
|
|
358
|
-
removeListener,
|
|
359
|
-
getCrashDetails: async (options) => {
|
|
360
|
-
await new Promise((resolve) => setTimeout(resolve, CRASH_ARTIFACT_SETTLE_DELAY_MS));
|
|
361
|
-
const details = getLatestCrashArtifact({
|
|
362
|
-
crashArtifacts: recentCrashArtifacts,
|
|
363
|
-
recentLogLines,
|
|
364
|
-
...options,
|
|
365
|
-
});
|
|
366
|
-
if (!details) {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
return persistCrashArtifact({
|
|
370
|
-
details,
|
|
371
|
-
crashArtifactWriter,
|
|
372
|
-
});
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
};
|
|
376
|
-
export { createAndroidLogEvent };
|
package/dist/device-state.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"device-state.d.ts","sourceRoot":"","sources":["../src/device-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EAEtB,MAAM,iCAAiC,CAAC;AA0HzC,eAAO,MAAM,wBAAwB,GAAI,SAAS;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAG,qBAqFH,CAAC"}
|
package/dist/device-state.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import * as adb from './adb.js';
|
|
2
|
-
const ANDROID_PERMISSION_ALIASES = {
|
|
3
|
-
bluetooth: [
|
|
4
|
-
'android.permission.BLUETOOTH_CONNECT',
|
|
5
|
-
'android.permission.BLUETOOTH_SCAN',
|
|
6
|
-
],
|
|
7
|
-
calendar: [
|
|
8
|
-
'android.permission.READ_CALENDAR',
|
|
9
|
-
'android.permission.WRITE_CALENDAR',
|
|
10
|
-
],
|
|
11
|
-
camera: ['android.permission.CAMERA'],
|
|
12
|
-
contacts: [
|
|
13
|
-
'android.permission.READ_CONTACTS',
|
|
14
|
-
'android.permission.WRITE_CONTACTS',
|
|
15
|
-
],
|
|
16
|
-
location: [
|
|
17
|
-
'android.permission.ACCESS_FINE_LOCATION',
|
|
18
|
-
'android.permission.ACCESS_COARSE_LOCATION',
|
|
19
|
-
],
|
|
20
|
-
mediaLibrary: [
|
|
21
|
-
'android.permission.READ_MEDIA_IMAGES',
|
|
22
|
-
'android.permission.READ_MEDIA_VIDEO',
|
|
23
|
-
'android.permission.READ_MEDIA_AUDIO',
|
|
24
|
-
],
|
|
25
|
-
microphone: ['android.permission.RECORD_AUDIO'],
|
|
26
|
-
notifications: ['android.permission.POST_NOTIFICATIONS'],
|
|
27
|
-
phone: [
|
|
28
|
-
'android.permission.CALL_PHONE',
|
|
29
|
-
'android.permission.ANSWER_PHONE_CALLS',
|
|
30
|
-
],
|
|
31
|
-
sms: [
|
|
32
|
-
'android.permission.READ_SMS',
|
|
33
|
-
'android.permission.RECEIVE_SMS',
|
|
34
|
-
'android.permission.SEND_SMS',
|
|
35
|
-
],
|
|
36
|
-
storage: [
|
|
37
|
-
'android.permission.READ_EXTERNAL_STORAGE',
|
|
38
|
-
'android.permission.WRITE_EXTERNAL_STORAGE',
|
|
39
|
-
],
|
|
40
|
-
};
|
|
41
|
-
const normalizePermissionName = (name) => {
|
|
42
|
-
return name.trim();
|
|
43
|
-
};
|
|
44
|
-
const resolvePermissions = async (options) => {
|
|
45
|
-
if (options.command.kind === 'permission-all') {
|
|
46
|
-
return adb.getDeclaredDangerousPermissions(options.adbId, options.bundleId);
|
|
47
|
-
}
|
|
48
|
-
const normalized = normalizePermissionName(options.command.permission);
|
|
49
|
-
const aliases = ANDROID_PERMISSION_ALIASES[normalized];
|
|
50
|
-
if (aliases) {
|
|
51
|
-
return aliases;
|
|
52
|
-
}
|
|
53
|
-
return [normalized];
|
|
54
|
-
};
|
|
55
|
-
const snapshotPermissions = async (options) => {
|
|
56
|
-
const granted = await Promise.all(options.permissions.map(async (permission) => {
|
|
57
|
-
const isGranted = await adb.isPermissionGranted(options.adbId, options.bundleId, permission);
|
|
58
|
-
return isGranted ? permission : null;
|
|
59
|
-
}));
|
|
60
|
-
return granted.filter((permission) => permission != null);
|
|
61
|
-
};
|
|
62
|
-
const applyPermissionSnapshot = async (options) => {
|
|
63
|
-
const grantedSet = new Set(options.snapshot.previousGranted);
|
|
64
|
-
const toGrant = options.snapshot.permissions.filter((permission) => grantedSet.has(permission));
|
|
65
|
-
const toRevoke = options.snapshot.permissions.filter((permission) => !grantedSet.has(permission));
|
|
66
|
-
if (toGrant.length > 0) {
|
|
67
|
-
await adb.setPermissions(options.adbId, options.bundleId, toGrant, 'grant', {
|
|
68
|
-
bestEffort: true,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
if (toRevoke.length > 0) {
|
|
72
|
-
await adb.setPermissions(options.adbId, options.bundleId, toRevoke, 'reset', {
|
|
73
|
-
bestEffort: true,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
export const createAndroidDeviceState = (options) => {
|
|
78
|
-
const outstandingMutations = new Map();
|
|
79
|
-
let mutationCounter = 0;
|
|
80
|
-
const rememberMutation = (snapshot) => {
|
|
81
|
-
const id = `android-permissions-${++mutationCounter}`;
|
|
82
|
-
const mutation = {
|
|
83
|
-
id,
|
|
84
|
-
snapshot,
|
|
85
|
-
revert: async () => {
|
|
86
|
-
await controller.permissions.revert(id);
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
outstandingMutations.set(id, mutation);
|
|
90
|
-
return mutation;
|
|
91
|
-
};
|
|
92
|
-
const controller = {
|
|
93
|
-
permissions: {
|
|
94
|
-
apply: async (command) => {
|
|
95
|
-
const permissions = await resolvePermissions({
|
|
96
|
-
adbId: options.adbId,
|
|
97
|
-
bundleId: options.bundleId,
|
|
98
|
-
command,
|
|
99
|
-
});
|
|
100
|
-
if (permissions.length === 0) {
|
|
101
|
-
return { mutation: null };
|
|
102
|
-
}
|
|
103
|
-
const previousGranted = await snapshotPermissions({
|
|
104
|
-
adbId: options.adbId,
|
|
105
|
-
bundleId: options.bundleId,
|
|
106
|
-
permissions,
|
|
107
|
-
});
|
|
108
|
-
await adb.setPermissions(options.adbId, options.bundleId, permissions, command.decision, { bestEffort: command.kind === 'permission-all' });
|
|
109
|
-
const mutation = rememberMutation({
|
|
110
|
-
kind: 'permission-set',
|
|
111
|
-
permissions,
|
|
112
|
-
previousGranted,
|
|
113
|
-
});
|
|
114
|
-
return { mutation };
|
|
115
|
-
},
|
|
116
|
-
revert: async (mutationId) => {
|
|
117
|
-
const mutation = outstandingMutations.get(mutationId);
|
|
118
|
-
if (!mutation) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
outstandingMutations.delete(mutationId);
|
|
122
|
-
await applyPermissionSnapshot({
|
|
123
|
-
adbId: options.adbId,
|
|
124
|
-
bundleId: options.bundleId,
|
|
125
|
-
snapshot: mutation.snapshot,
|
|
126
|
-
});
|
|
127
|
-
},
|
|
128
|
-
resetOutstanding: async () => {
|
|
129
|
-
const mutations = [...outstandingMutations.values()].reverse();
|
|
130
|
-
outstandingMutations.clear();
|
|
131
|
-
for (const mutation of mutations) {
|
|
132
|
-
await applyPermissionSnapshot({
|
|
133
|
-
adbId: options.adbId,
|
|
134
|
-
bundleId: options.bundleId,
|
|
135
|
-
snapshot: mutation.snapshot,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
return controller;
|
|
142
|
-
};
|