@react-native-harness/platform-android 1.4.1 → 1.5.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 +2 -4
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +2 -3
- package/dist/app-session.d.ts +3 -12
- package/dist/app-session.d.ts.map +1 -1
- package/dist/app-session.js +23 -33
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +2 -4
- package/package.json +4 -4
- package/src/adb.ts +5 -6
- package/src/app-session.ts +14 -31
- package/src/instance.ts +2 -4
- 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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
|
|
2
|
-
import { type
|
|
2
|
+
import { type OwnedProcess } from '@react-native-harness/tools';
|
|
3
3
|
import type { ChildProcessByStdio } from 'node:child_process';
|
|
4
4
|
import type { Readable } from 'node:stream';
|
|
5
5
|
import { type EmulatorBootMode } from './emulator-startup.js';
|
|
@@ -44,9 +44,7 @@ export declare const getAppPid: (adbId: string, bundleId: string) => Promise<num
|
|
|
44
44
|
export declare const getAppUid: (adbId: string, bundleId: string) => Promise<number>;
|
|
45
45
|
export declare const setHideErrorDialogs: (adbId: string, hide: boolean) => Promise<void>;
|
|
46
46
|
export declare const getLogcatTimestamp: (adbId: string) => Promise<string>;
|
|
47
|
-
export declare const startLogcat: (adbId: string, args: readonly string[]
|
|
48
|
-
signal?: AbortSignal;
|
|
49
|
-
}) => Subprocess;
|
|
47
|
+
export declare const startLogcat: (adbId: string, args: readonly string[]) => OwnedProcess;
|
|
50
48
|
export declare const DROPBOX_CRASH_TAGS: readonly ["data_app_crash", "data_app_native_crash"];
|
|
51
49
|
export declare const getDropboxPrint: (adbId: string, tags?: readonly string[]) => Promise<string>;
|
|
52
50
|
export declare const getActivityExitInfo: (adbId: string, bundleId: string) => Promise<string>;
|
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,
|
|
1
|
+
{"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
|
package/dist/adb.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { delay, logger, spawn, SubprocessError, waitForAbort, } from '@react-native-harness/tools';
|
|
1
|
+
import { delay, logger, spawn, spawnOwnedProcess, SubprocessError, waitForAbort, } from '@react-native-harness/tools';
|
|
2
2
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
3
3
|
import { access, rm } from 'node:fs/promises';
|
|
4
4
|
import { getAvdConfigPath, getAvdDirectory, readAvdConfig, } from './avd-config.js';
|
|
@@ -505,10 +505,9 @@ export const getLogcatTimestamp = async (adbId) => {
|
|
|
505
505
|
]);
|
|
506
506
|
return stdout.trim().replace(/^'+|'+$/g, '');
|
|
507
507
|
};
|
|
508
|
-
export const startLogcat = (adbId, args
|
|
508
|
+
export const startLogcat = (adbId, args) => spawnOwnedProcess(getAdbBinaryPath(), ['-s', adbId, ...args], {
|
|
509
509
|
stdout: 'pipe',
|
|
510
510
|
stderr: 'pipe',
|
|
511
|
-
signal: options?.signal,
|
|
512
511
|
});
|
|
513
512
|
export const DROPBOX_CRASH_TAGS = [
|
|
514
513
|
'data_app_crash',
|
package/dist/app-session.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AppSession, type CrashArtifactWriter } from '@react-native-harness/platforms';
|
|
2
|
-
import { type
|
|
2
|
+
import { type OwnedProcess } from '@react-native-harness/tools';
|
|
3
3
|
type CreateAndroidAppSessionOptions = {
|
|
4
4
|
appUid: number;
|
|
5
5
|
bundleId: string;
|
|
@@ -7,20 +7,11 @@ type CreateAndroidAppSessionOptions = {
|
|
|
7
7
|
stopApp: () => Promise<void>;
|
|
8
8
|
getAppPid: () => Promise<number | null>;
|
|
9
9
|
getLogcatTimestamp: () => Promise<string>;
|
|
10
|
-
startLogcat: (args: readonly string[]
|
|
11
|
-
signal: AbortSignal;
|
|
12
|
-
}) => Subprocess;
|
|
10
|
+
startLogcat: (args: readonly string[]) => OwnedProcess;
|
|
13
11
|
getDropboxOutput?: () => Promise<string>;
|
|
14
12
|
getExitInfo?: () => Promise<string>;
|
|
15
13
|
crashArtifactWriter?: CrashArtifactWriter;
|
|
16
|
-
/**
|
|
17
|
-
* Session-lifetime abort signal (see HarnessPlatformInitOptions.signal).
|
|
18
|
-
* Combined with the app session's own logcatAbortController so that
|
|
19
|
-
* aborting the session stops the logcat stream even if dispose() hasn't
|
|
20
|
-
* been called yet (e.g. SIGTERM during teardown of other resources).
|
|
21
|
-
*/
|
|
22
|
-
signal?: AbortSignal;
|
|
23
14
|
};
|
|
24
|
-
export declare const createAndroidAppSession: ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter,
|
|
15
|
+
export declare const createAndroidAppSession: ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, }: CreateAndroidAppSessionOptions) => Promise<AppSession>;
|
|
25
16
|
export {};
|
|
26
17
|
//# sourceMappingURL=app-session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-session.d.ts","sourceRoot":"","sources":["../src/app-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,
|
|
1
|
+
{"version":3,"file":"app-session.d.ts","sourceRoot":"","sources":["../src/app-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,6BAA6B,CAAC;AA0DrC,KAAK,8BAA8B,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,kBAAkB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,WAAW,EAAE,CACX,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,YAAY,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAU,0IAW3C,8BAA8B,KAAG,OAAO,CAAC,UAAU,CA2MrD,CAAC"}
|
package/dist/app-session.js
CHANGED
|
@@ -31,7 +31,7 @@ const isCrashSignal = (line, bundleId) => {
|
|
|
31
31
|
/fatal|crash|signal 11|signal 6|backtrace/i.test(line)));
|
|
32
32
|
};
|
|
33
33
|
const isExitedState = (state) => state.status === 'exited';
|
|
34
|
-
export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter,
|
|
34
|
+
export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, }) => {
|
|
35
35
|
const emitter = createAppSessionEmitter();
|
|
36
36
|
const logBuffer = createBoundedLogBuffer();
|
|
37
37
|
let state = { status: 'running' };
|
|
@@ -105,20 +105,8 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
|
|
|
105
105
|
};
|
|
106
106
|
const logcatTimestamp = await getLogcatTimestamp();
|
|
107
107
|
const sessionStartedAt = Date.now();
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
? AbortSignal.any([signal, logcatAbortController.signal])
|
|
111
|
-
: logcatAbortController.signal;
|
|
112
|
-
const logcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp), {
|
|
113
|
-
signal: logcatSignal,
|
|
114
|
-
});
|
|
115
|
-
// Aborting is nano-spawn's own cancellation path, so the resulting
|
|
116
|
-
// SubprocessError is settled the same way regardless of which of the
|
|
117
|
-
// merged stdout/stderr iterators observes it first. Without this, killing
|
|
118
|
-
// the underlying process directly can make both iterators reject at once,
|
|
119
|
-
// and nano-spawn's internal `Promise.race` only reports one of them,
|
|
120
|
-
// leaving the other an unhandled rejection.
|
|
121
|
-
logcatProcess.catch(() => undefined);
|
|
108
|
+
const ownedLogcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp));
|
|
109
|
+
const logcatProcess = ownedLogcatProcess.subprocess;
|
|
122
110
|
const crashReporter = createAndroidCrashReporter({
|
|
123
111
|
bundleId,
|
|
124
112
|
crashArtifactWriter,
|
|
@@ -161,7 +149,7 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
|
|
|
161
149
|
disposed = true;
|
|
162
150
|
stopPolling = true;
|
|
163
151
|
emitter.clear();
|
|
164
|
-
|
|
152
|
+
await ownedLogcatProcess.dispose();
|
|
165
153
|
await Promise.allSettled([logTask]);
|
|
166
154
|
throw error;
|
|
167
155
|
}
|
|
@@ -187,24 +175,26 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
|
|
|
187
175
|
await waitForNextPoll();
|
|
188
176
|
}
|
|
189
177
|
})();
|
|
178
|
+
let disposePromise;
|
|
179
|
+
const dispose = () => (disposePromise ??= (async () => {
|
|
180
|
+
if (disposed) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
disposed = true;
|
|
184
|
+
stopPolling = true;
|
|
185
|
+
state = { status: 'disposed', occurredAt: Date.now() };
|
|
186
|
+
if (exitNotification) {
|
|
187
|
+
clearTimeout(exitNotification);
|
|
188
|
+
exitNotification = null;
|
|
189
|
+
}
|
|
190
|
+
cancelPendingPollDelay();
|
|
191
|
+
emitter.clear();
|
|
192
|
+
await ownedLogcatProcess.dispose();
|
|
193
|
+
await stopApp();
|
|
194
|
+
await Promise.allSettled([logTask, pollTask]);
|
|
195
|
+
})());
|
|
190
196
|
return {
|
|
191
|
-
dispose
|
|
192
|
-
if (disposed) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
disposed = true;
|
|
196
|
-
stopPolling = true;
|
|
197
|
-
state = { status: 'disposed', occurredAt: Date.now() };
|
|
198
|
-
if (exitNotification) {
|
|
199
|
-
clearTimeout(exitNotification);
|
|
200
|
-
exitNotification = null;
|
|
201
|
-
}
|
|
202
|
-
cancelPendingPollDelay();
|
|
203
|
-
emitter.clear();
|
|
204
|
-
logcatAbortController.abort();
|
|
205
|
-
await stopApp();
|
|
206
|
-
await Promise.allSettled([logTask, pollTask]);
|
|
207
|
-
},
|
|
197
|
+
dispose,
|
|
208
198
|
getState: async () => state,
|
|
209
199
|
getLogs: () => logBuffer.getLogs(),
|
|
210
200
|
getCrashDetails: crashReporter.getCrashDetails,
|
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;AAyKrB,eAAO,MAAM,kCAAkC,GAC7C,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,MAAM,0BAA0B,KAC/B,OAAO,CAAC,qBAAqB,
|
|
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;AAyKrB,eAAO,MAAM,kCAAkC,GAC7C,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,MAAM,0BAA0B,KAC/B,OAAO,CAAC,qBAAqB,CAmI/B,CAAC;AAEF,eAAO,MAAM,wCAAwC,GACnD,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,EAC5B,OAAO,0BAA0B,KAChC,OAAO,CAAC,qBAAqB,CA8D/B,CAAC"}
|
package/dist/instance.js
CHANGED
|
@@ -172,11 +172,10 @@ export const getAndroidEmulatorPlatformInstance = async (config, harnessConfig,
|
|
|
172
172
|
stopApp: () => adb.stopApp(adbId, config.bundleId),
|
|
173
173
|
getAppPid: () => adb.getAppPid(adbId, config.bundleId),
|
|
174
174
|
getLogcatTimestamp: () => adb.getLogcatTimestamp(adbId),
|
|
175
|
-
startLogcat: (args
|
|
175
|
+
startLogcat: (args) => adb.startLogcat(adbId, args),
|
|
176
176
|
getDropboxOutput: () => adb.getDropboxPrint(adbId),
|
|
177
177
|
getExitInfo: () => adb.getActivityExitInfo(adbId, config.bundleId),
|
|
178
178
|
crashArtifactWriter: init.crashArtifactWriter,
|
|
179
|
-
signal: init.signal,
|
|
180
179
|
});
|
|
181
180
|
},
|
|
182
181
|
dispose: async () => {
|
|
@@ -218,11 +217,10 @@ export const getAndroidPhysicalDevicePlatformInstance = async (config, harnessCo
|
|
|
218
217
|
stopApp: () => adb.stopApp(adbId, config.bundleId),
|
|
219
218
|
getAppPid: () => adb.getAppPid(adbId, config.bundleId),
|
|
220
219
|
getLogcatTimestamp: () => adb.getLogcatTimestamp(adbId),
|
|
221
|
-
startLogcat: (args
|
|
220
|
+
startLogcat: (args) => adb.startLogcat(adbId, args),
|
|
222
221
|
getDropboxOutput: () => adb.getDropboxPrint(adbId),
|
|
223
222
|
getExitInfo: () => adb.getActivityExitInfo(adbId, config.bundleId),
|
|
224
223
|
crashArtifactWriter: init?.crashArtifactWriter,
|
|
225
|
-
signal: init?.signal,
|
|
226
224
|
});
|
|
227
225
|
},
|
|
228
226
|
dispose: async () => {
|
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.5.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/
|
|
24
|
-
"@react-native-harness/
|
|
22
|
+
"@react-native-harness/platforms": "1.5.0",
|
|
23
|
+
"@react-native-harness/tools": "1.5.0",
|
|
24
|
+
"@react-native-harness/config": "1.5.0"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT"
|
|
27
27
|
}
|
package/src/adb.ts
CHANGED
|
@@ -3,9 +3,10 @@ import {
|
|
|
3
3
|
delay,
|
|
4
4
|
logger,
|
|
5
5
|
spawn,
|
|
6
|
+
spawnOwnedProcess,
|
|
6
7
|
SubprocessError,
|
|
7
8
|
waitForAbort,
|
|
8
|
-
type
|
|
9
|
+
type OwnedProcess,
|
|
9
10
|
} from '@react-native-harness/tools';
|
|
10
11
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
11
12
|
import type { ChildProcessByStdio } from 'node:child_process';
|
|
@@ -789,13 +790,11 @@ export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
|
|
|
789
790
|
|
|
790
791
|
export const startLogcat = (
|
|
791
792
|
adbId: string,
|
|
792
|
-
args: readonly string[]
|
|
793
|
-
|
|
794
|
-
)
|
|
795
|
-
spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
|
|
793
|
+
args: readonly string[]
|
|
794
|
+
): OwnedProcess =>
|
|
795
|
+
spawnOwnedProcess(getAdbBinaryPath(), ['-s', adbId, ...args], {
|
|
796
796
|
stdout: 'pipe',
|
|
797
797
|
stderr: 'pipe',
|
|
798
|
-
signal: options?.signal,
|
|
799
798
|
});
|
|
800
799
|
|
|
801
800
|
export const DROPBOX_CRASH_TAGS = [
|
package/src/app-session.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
escapeRegExp,
|
|
10
10
|
logger,
|
|
11
|
-
type
|
|
11
|
+
type OwnedProcess,
|
|
12
12
|
} from '@react-native-harness/tools';
|
|
13
13
|
import { createAndroidCrashReporter } from './crash-reporter.js';
|
|
14
14
|
|
|
@@ -75,19 +75,11 @@ type CreateAndroidAppSessionOptions = {
|
|
|
75
75
|
getAppPid: () => Promise<number | null>;
|
|
76
76
|
getLogcatTimestamp: () => Promise<string>;
|
|
77
77
|
startLogcat: (
|
|
78
|
-
args: readonly string[]
|
|
79
|
-
|
|
80
|
-
) => Subprocess;
|
|
78
|
+
args: readonly string[]
|
|
79
|
+
) => OwnedProcess;
|
|
81
80
|
getDropboxOutput?: () => Promise<string>;
|
|
82
81
|
getExitInfo?: () => Promise<string>;
|
|
83
82
|
crashArtifactWriter?: CrashArtifactWriter;
|
|
84
|
-
/**
|
|
85
|
-
* Session-lifetime abort signal (see HarnessPlatformInitOptions.signal).
|
|
86
|
-
* Combined with the app session's own logcatAbortController so that
|
|
87
|
-
* aborting the session stops the logcat stream even if dispose() hasn't
|
|
88
|
-
* been called yet (e.g. SIGTERM during teardown of other resources).
|
|
89
|
-
*/
|
|
90
|
-
signal?: AbortSignal;
|
|
91
83
|
};
|
|
92
84
|
|
|
93
85
|
export const createAndroidAppSession = async ({
|
|
@@ -101,7 +93,6 @@ export const createAndroidAppSession = async ({
|
|
|
101
93
|
getDropboxOutput,
|
|
102
94
|
getExitInfo,
|
|
103
95
|
crashArtifactWriter,
|
|
104
|
-
signal,
|
|
105
96
|
}: CreateAndroidAppSessionOptions): Promise<AppSession> => {
|
|
106
97
|
const emitter = createAppSessionEmitter();
|
|
107
98
|
const logBuffer = createBoundedLogBuffer();
|
|
@@ -196,20 +187,8 @@ export const createAndroidAppSession = async ({
|
|
|
196
187
|
|
|
197
188
|
const logcatTimestamp = await getLogcatTimestamp();
|
|
198
189
|
const sessionStartedAt = Date.now();
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
? AbortSignal.any([signal, logcatAbortController.signal])
|
|
202
|
-
: logcatAbortController.signal;
|
|
203
|
-
const logcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp), {
|
|
204
|
-
signal: logcatSignal,
|
|
205
|
-
});
|
|
206
|
-
// Aborting is nano-spawn's own cancellation path, so the resulting
|
|
207
|
-
// SubprocessError is settled the same way regardless of which of the
|
|
208
|
-
// merged stdout/stderr iterators observes it first. Without this, killing
|
|
209
|
-
// the underlying process directly can make both iterators reject at once,
|
|
210
|
-
// and nano-spawn's internal `Promise.race` only reports one of them,
|
|
211
|
-
// leaving the other an unhandled rejection.
|
|
212
|
-
logcatProcess.catch(() => undefined);
|
|
190
|
+
const ownedLogcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp));
|
|
191
|
+
const logcatProcess = ownedLogcatProcess.subprocess;
|
|
213
192
|
const crashReporter = createAndroidCrashReporter({
|
|
214
193
|
bundleId,
|
|
215
194
|
crashArtifactWriter,
|
|
@@ -255,7 +234,7 @@ export const createAndroidAppSession = async ({
|
|
|
255
234
|
disposed = true;
|
|
256
235
|
stopPolling = true;
|
|
257
236
|
emitter.clear();
|
|
258
|
-
|
|
237
|
+
await ownedLogcatProcess.dispose();
|
|
259
238
|
await Promise.allSettled([logTask]);
|
|
260
239
|
throw error;
|
|
261
240
|
}
|
|
@@ -285,8 +264,9 @@ export const createAndroidAppSession = async ({
|
|
|
285
264
|
}
|
|
286
265
|
})();
|
|
287
266
|
|
|
288
|
-
|
|
289
|
-
|
|
267
|
+
let disposePromise: Promise<void> | undefined;
|
|
268
|
+
const dispose = () =>
|
|
269
|
+
(disposePromise ??= (async () => {
|
|
290
270
|
if (disposed) {
|
|
291
271
|
return;
|
|
292
272
|
}
|
|
@@ -303,10 +283,13 @@ export const createAndroidAppSession = async ({
|
|
|
303
283
|
cancelPendingPollDelay();
|
|
304
284
|
|
|
305
285
|
emitter.clear();
|
|
306
|
-
|
|
286
|
+
await ownedLogcatProcess.dispose();
|
|
307
287
|
await stopApp();
|
|
308
288
|
await Promise.allSettled([logTask, pollTask]);
|
|
309
|
-
}
|
|
289
|
+
})());
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
dispose,
|
|
310
293
|
getState: async () => state,
|
|
311
294
|
getLogs: () => logBuffer.getLogs(),
|
|
312
295
|
getCrashDetails: crashReporter.getCrashDetails,
|
package/src/instance.ts
CHANGED
|
@@ -297,11 +297,10 @@ export const getAndroidEmulatorPlatformInstance = async (
|
|
|
297
297
|
stopApp: () => adb.stopApp(adbId, config.bundleId),
|
|
298
298
|
getAppPid: () => adb.getAppPid(adbId, config.bundleId),
|
|
299
299
|
getLogcatTimestamp: () => adb.getLogcatTimestamp(adbId),
|
|
300
|
-
startLogcat: (args
|
|
300
|
+
startLogcat: (args) => adb.startLogcat(adbId, args),
|
|
301
301
|
getDropboxOutput: () => adb.getDropboxPrint(adbId),
|
|
302
302
|
getExitInfo: () => adb.getActivityExitInfo(adbId, config.bundleId),
|
|
303
303
|
crashArtifactWriter: init.crashArtifactWriter,
|
|
304
|
-
signal: init.signal,
|
|
305
304
|
});
|
|
306
305
|
},
|
|
307
306
|
dispose: async () => {
|
|
@@ -371,11 +370,10 @@ export const getAndroidPhysicalDevicePlatformInstance = async (
|
|
|
371
370
|
stopApp: () => adb.stopApp(adbId, config.bundleId),
|
|
372
371
|
getAppPid: () => adb.getAppPid(adbId, config.bundleId),
|
|
373
372
|
getLogcatTimestamp: () => adb.getLogcatTimestamp(adbId),
|
|
374
|
-
startLogcat: (args
|
|
373
|
+
startLogcat: (args) => adb.startLogcat(adbId, args),
|
|
375
374
|
getDropboxOutput: () => adb.getDropboxPrint(adbId),
|
|
376
375
|
getExitInfo: () => adb.getActivityExitInfo(adbId, config.bundleId),
|
|
377
376
|
crashArtifactWriter: init?.crashArtifactWriter,
|
|
378
|
-
signal: init?.signal,
|
|
379
377
|
});
|
|
380
378
|
},
|
|
381
379
|
dispose: async () => {
|
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
|
-
};
|