@react-native-harness/platform-android 1.2.0 → 1.4.0-rc.1
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/__tests__/app-monitor.test.js +180 -102
- package/dist/__tests__/crash-diagnostics.test.d.ts +2 -0
- package/dist/__tests__/crash-diagnostics.test.d.ts.map +1 -0
- package/dist/__tests__/crash-diagnostics.test.js +119 -0
- package/dist/__tests__/crash-reporter.test.d.ts +2 -0
- package/dist/__tests__/crash-reporter.test.d.ts.map +1 -0
- package/dist/__tests__/crash-reporter.test.js +150 -0
- package/dist/__tests__/instance.test.js +133 -24
- package/dist/adb.d.ts +4 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +42 -2
- package/dist/app-session.d.ts +17 -0
- package/dist/app-session.d.ts.map +1 -0
- package/dist/app-session.js +209 -0
- package/dist/crash-diagnostics.d.ts +40 -0
- package/dist/crash-diagnostics.d.ts.map +1 -0
- package/dist/crash-diagnostics.js +256 -0
- package/dist/crash-reporter.d.ts +12 -0
- package/dist/crash-reporter.d.ts.map +1 -0
- package/dist/crash-reporter.js +150 -0
- package/dist/device-state.d.ts +6 -0
- package/dist/device-state.d.ts.map +1 -0
- package/dist/device-state.js +142 -0
- package/dist/instance.d.ts +1 -1
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +32 -59
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/__tests__/crash-diagnostics.test.ts +144 -0
- package/src/__tests__/crash-reporter.test.ts +167 -0
- package/src/__tests__/instance.test.ts +174 -31
- package/src/adb.ts +61 -2
- package/src/app-session.ts +300 -0
- package/src/crash-diagnostics.ts +471 -0
- package/src/crash-reporter.ts +263 -0
- package/src/instance.ts +56 -91
- package/src/runner.ts +7 -3
- package/test/setup.ts +1 -0
- package/src/app-monitor.ts +0 -577
package/src/adb.ts
CHANGED
|
@@ -699,6 +699,13 @@ export const isAppRunning = async (
|
|
|
699
699
|
adbId: string,
|
|
700
700
|
bundleId: string,
|
|
701
701
|
): Promise<boolean> => {
|
|
702
|
+
return (await getAppPid(adbId, bundleId)) != null;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
export const getAppPid = async (
|
|
706
|
+
adbId: string,
|
|
707
|
+
bundleId: string,
|
|
708
|
+
): Promise<number | null> => {
|
|
702
709
|
try {
|
|
703
710
|
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
704
711
|
'-s',
|
|
@@ -707,10 +714,15 @@ export const isAppRunning = async (
|
|
|
707
714
|
'pidof',
|
|
708
715
|
bundleId,
|
|
709
716
|
]);
|
|
710
|
-
|
|
717
|
+
const pid = stdout
|
|
718
|
+
.trim()
|
|
719
|
+
.split(/\s+/)
|
|
720
|
+
.find((value) => value !== '');
|
|
721
|
+
|
|
722
|
+
return pid ? Number(pid) : null;
|
|
711
723
|
} catch (error) {
|
|
712
724
|
if (error instanceof SubprocessError && error.exitCode === 1) {
|
|
713
|
-
return
|
|
725
|
+
return null;
|
|
714
726
|
}
|
|
715
727
|
|
|
716
728
|
throw error;
|
|
@@ -779,6 +791,53 @@ export const startLogcat = (
|
|
|
779
791
|
stderr: 'pipe',
|
|
780
792
|
});
|
|
781
793
|
|
|
794
|
+
export const DROPBOX_CRASH_TAGS = [
|
|
795
|
+
'data_app_crash',
|
|
796
|
+
'data_app_native_crash',
|
|
797
|
+
] as const;
|
|
798
|
+
|
|
799
|
+
export const getDropboxPrint = async (
|
|
800
|
+
adbId: string,
|
|
801
|
+
tags: readonly string[] = DROPBOX_CRASH_TAGS,
|
|
802
|
+
): Promise<string> => {
|
|
803
|
+
// Android treats multiple args after --print as one search string, so each
|
|
804
|
+
// tag must be queried separately and merged on the host.
|
|
805
|
+
const outputs = await Promise.all(
|
|
806
|
+
tags.map(async (tag) => {
|
|
807
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
808
|
+
'-s',
|
|
809
|
+
adbId,
|
|
810
|
+
'shell',
|
|
811
|
+
'dumpsys',
|
|
812
|
+
'dropbox',
|
|
813
|
+
'--print',
|
|
814
|
+
tag,
|
|
815
|
+
]);
|
|
816
|
+
|
|
817
|
+
return stdout;
|
|
818
|
+
}),
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
return outputs.join('\n');
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
export const getActivityExitInfo = async (
|
|
825
|
+
adbId: string,
|
|
826
|
+
bundleId: string,
|
|
827
|
+
): Promise<string> => {
|
|
828
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
829
|
+
'-s',
|
|
830
|
+
adbId,
|
|
831
|
+
'shell',
|
|
832
|
+
'dumpsys',
|
|
833
|
+
'activity',
|
|
834
|
+
'exit-info',
|
|
835
|
+
bundleId,
|
|
836
|
+
]);
|
|
837
|
+
|
|
838
|
+
return stdout;
|
|
839
|
+
};
|
|
840
|
+
|
|
782
841
|
export const getAvds = async (): Promise<string[]> => {
|
|
783
842
|
try {
|
|
784
843
|
const emulatorBinaryPath = await ensureEmulatorInstalled();
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppSessionEmitter,
|
|
3
|
+
createBoundedLogBuffer,
|
|
4
|
+
type AppSession,
|
|
5
|
+
type AppSessionState,
|
|
6
|
+
type CrashArtifactWriter,
|
|
7
|
+
} from '@react-native-harness/platforms';
|
|
8
|
+
import {
|
|
9
|
+
escapeRegExp,
|
|
10
|
+
logger,
|
|
11
|
+
type Subprocess,
|
|
12
|
+
} from '@react-native-harness/tools';
|
|
13
|
+
import { createAndroidCrashReporter } from './crash-reporter.js';
|
|
14
|
+
|
|
15
|
+
const androidAppSessionLogger = logger.child('android-app-session');
|
|
16
|
+
const APP_EXIT_POLL_INTERVAL_MS = 1000;
|
|
17
|
+
|
|
18
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
|
|
20
|
+
const getLogcatArgs = (appUid: number, fromTime: string) =>
|
|
21
|
+
[
|
|
22
|
+
'logcat',
|
|
23
|
+
'-v',
|
|
24
|
+
'threadtime',
|
|
25
|
+
'-b',
|
|
26
|
+
'crash',
|
|
27
|
+
`--uid=${appUid}`,
|
|
28
|
+
'-T',
|
|
29
|
+
fromTime,
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
const getProcessPattern = (bundleId: string) =>
|
|
33
|
+
new RegExp(`Process:\\s*${escapeRegExp(bundleId)},\\s*PID:\\s*(\\d+)`);
|
|
34
|
+
|
|
35
|
+
const getStartProcPattern = (bundleId: string) =>
|
|
36
|
+
new RegExp(`Start proc (\\d+):${escapeRegExp(bundleId)}(?:/|\\s)`);
|
|
37
|
+
|
|
38
|
+
const getProcessDiedPattern = (bundleId: string) =>
|
|
39
|
+
new RegExp(
|
|
40
|
+
`Process\\s+${escapeRegExp(
|
|
41
|
+
bundleId
|
|
42
|
+
)}\\s+\\(pid\\s+(\\d+)\\)\\s+has\\s+died`,
|
|
43
|
+
'i'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const getObservedPid = (line: string, bundleId: string): number | undefined => {
|
|
47
|
+
const match =
|
|
48
|
+
line.match(getProcessPattern(bundleId)) ??
|
|
49
|
+
line.match(getStartProcPattern(bundleId)) ??
|
|
50
|
+
line.match(getProcessDiedPattern(bundleId));
|
|
51
|
+
|
|
52
|
+
return match?.[1] ? Number(match[1]) : undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const isCrashSignal = (line: string, bundleId: string): boolean => {
|
|
56
|
+
return (
|
|
57
|
+
getProcessPattern(bundleId).test(line) ||
|
|
58
|
+
new RegExp(`>>>\\s*${escapeRegExp(bundleId)}\\s*<<<`).test(line) ||
|
|
59
|
+
getProcessDiedPattern(bundleId).test(line) ||
|
|
60
|
+
(line.includes(bundleId) &&
|
|
61
|
+
/fatal|crash|signal 11|signal 6|backtrace/i.test(line))
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const stopSubprocess = async (child: Subprocess) => {
|
|
66
|
+
try {
|
|
67
|
+
(await child.nodeChildProcess).kill();
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore termination failures for already-ended background processes.
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const isExitedState = (
|
|
74
|
+
state: AppSessionState
|
|
75
|
+
): state is Extract<AppSessionState, { status: 'exited' }> =>
|
|
76
|
+
state.status === 'exited';
|
|
77
|
+
|
|
78
|
+
type CreateAndroidAppSessionOptions = {
|
|
79
|
+
appUid: number;
|
|
80
|
+
bundleId: string;
|
|
81
|
+
startApp: () => Promise<void>;
|
|
82
|
+
stopApp: () => Promise<void>;
|
|
83
|
+
getAppPid: () => Promise<number | null>;
|
|
84
|
+
getLogcatTimestamp: () => Promise<string>;
|
|
85
|
+
startLogcat: (args: readonly string[]) => Subprocess;
|
|
86
|
+
getDropboxOutput?: () => Promise<string>;
|
|
87
|
+
getExitInfo?: () => Promise<string>;
|
|
88
|
+
crashArtifactWriter?: CrashArtifactWriter;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const createAndroidAppSession = async ({
|
|
92
|
+
appUid,
|
|
93
|
+
bundleId,
|
|
94
|
+
startApp,
|
|
95
|
+
stopApp,
|
|
96
|
+
getAppPid,
|
|
97
|
+
getLogcatTimestamp,
|
|
98
|
+
startLogcat,
|
|
99
|
+
getDropboxOutput,
|
|
100
|
+
getExitInfo,
|
|
101
|
+
crashArtifactWriter,
|
|
102
|
+
}: CreateAndroidAppSessionOptions): Promise<AppSession> => {
|
|
103
|
+
const emitter = createAppSessionEmitter();
|
|
104
|
+
const logBuffer = createBoundedLogBuffer();
|
|
105
|
+
let state: AppSessionState = { status: 'running' };
|
|
106
|
+
let disposed = false;
|
|
107
|
+
let stopPolling = false;
|
|
108
|
+
let hasObservedProcess = false;
|
|
109
|
+
let exitNotification: ReturnType<typeof setTimeout> | null = null;
|
|
110
|
+
let pollDelayTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
111
|
+
let resolvePollDelay: (() => void) | null = null;
|
|
112
|
+
|
|
113
|
+
const getCurrentPid = () => ('pid' in state ? state.pid : undefined);
|
|
114
|
+
|
|
115
|
+
const setRunning = (pid?: number) => {
|
|
116
|
+
if (disposed || state.status === 'disposed' || state.status === 'exited') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (pid !== undefined) {
|
|
121
|
+
hasObservedProcess = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
state =
|
|
125
|
+
pid === undefined ? { status: 'running' } : { status: 'running', pid };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const scheduleExitNotification = () => {
|
|
129
|
+
if (exitNotification) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
exitNotification = setTimeout(() => {
|
|
134
|
+
exitNotification = null;
|
|
135
|
+
|
|
136
|
+
if (!disposed && state.status === 'exited') {
|
|
137
|
+
emitter.emit({ type: 'app_exited' });
|
|
138
|
+
}
|
|
139
|
+
}, 0);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const waitForNextPoll = () =>
|
|
143
|
+
new Promise<void>((resolve) => {
|
|
144
|
+
resolvePollDelay = () => {
|
|
145
|
+
resolvePollDelay = null;
|
|
146
|
+
pollDelayTimeout = null;
|
|
147
|
+
resolve();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
pollDelayTimeout = setTimeout(() => {
|
|
151
|
+
resolvePollDelay?.();
|
|
152
|
+
}, APP_EXIT_POLL_INTERVAL_MS);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const cancelPendingPollDelay = () => {
|
|
156
|
+
if (pollDelayTimeout) {
|
|
157
|
+
clearTimeout(pollDelayTimeout);
|
|
158
|
+
pollDelayTimeout = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
resolvePollDelay?.();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const setExited = (
|
|
165
|
+
reason: 'observed-exit' | 'process-gone',
|
|
166
|
+
pid?: number
|
|
167
|
+
) => {
|
|
168
|
+
if (disposed || state.status === 'disposed' || state.status === 'exited') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
state = {
|
|
173
|
+
status: 'exited',
|
|
174
|
+
occurredAt: Date.now(),
|
|
175
|
+
pid: pid ?? getCurrentPid(),
|
|
176
|
+
reason,
|
|
177
|
+
};
|
|
178
|
+
scheduleExitNotification();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const setExitedPid = (pid: number) => {
|
|
182
|
+
if (state.status !== 'exited' || state.pid !== undefined) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
state = {
|
|
187
|
+
status: 'exited',
|
|
188
|
+
occurredAt: state.occurredAt,
|
|
189
|
+
reason: state.reason,
|
|
190
|
+
pid,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const logcatTimestamp = await getLogcatTimestamp();
|
|
195
|
+
const sessionStartedAt = Date.now();
|
|
196
|
+
const logcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp));
|
|
197
|
+
const crashReporter = createAndroidCrashReporter({
|
|
198
|
+
bundleId,
|
|
199
|
+
crashArtifactWriter,
|
|
200
|
+
getLogs: () => logBuffer.getLogs(),
|
|
201
|
+
getDropboxOutput,
|
|
202
|
+
getExitInfo,
|
|
203
|
+
minOccurredAt: sessionStartedAt,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const logTask = (async () => {
|
|
207
|
+
try {
|
|
208
|
+
for await (const rawLine of logcatProcess) {
|
|
209
|
+
const line = String(rawLine);
|
|
210
|
+
|
|
211
|
+
if (!disposed) {
|
|
212
|
+
logBuffer.push(line);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const observedPid = getObservedPid(line, bundleId);
|
|
216
|
+
|
|
217
|
+
if (observedPid !== undefined) {
|
|
218
|
+
if (state.status === 'running') {
|
|
219
|
+
setRunning(observedPid);
|
|
220
|
+
} else {
|
|
221
|
+
setExitedPid(observedPid);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (state.status === 'running' && isCrashSignal(line, bundleId)) {
|
|
226
|
+
setExited('observed-exit', observedPid);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (!disposed) {
|
|
231
|
+
androidAppSessionLogger.debug('Android logcat stream stopped', error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await startApp();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
disposed = true;
|
|
240
|
+
stopPolling = true;
|
|
241
|
+
emitter.clear();
|
|
242
|
+
await stopSubprocess(logcatProcess);
|
|
243
|
+
await Promise.allSettled([logTask]);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const pollTask = (async () => {
|
|
248
|
+
await sleep(0);
|
|
249
|
+
|
|
250
|
+
while (!stopPolling) {
|
|
251
|
+
if (isExitedState(state)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const pid = await getAppPid();
|
|
257
|
+
|
|
258
|
+
if (pid != null) {
|
|
259
|
+
setRunning(pid);
|
|
260
|
+
} else if (hasObservedProcess) {
|
|
261
|
+
setExited('process-gone');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
androidAppSessionLogger.debug('Android app session poll failed', error);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await waitForNextPoll();
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
dispose: async () => {
|
|
274
|
+
if (disposed) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
disposed = true;
|
|
279
|
+
stopPolling = true;
|
|
280
|
+
state = { status: 'disposed', occurredAt: Date.now() };
|
|
281
|
+
|
|
282
|
+
if (exitNotification) {
|
|
283
|
+
clearTimeout(exitNotification);
|
|
284
|
+
exitNotification = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
cancelPendingPollDelay();
|
|
288
|
+
|
|
289
|
+
emitter.clear();
|
|
290
|
+
await stopSubprocess(logcatProcess);
|
|
291
|
+
await stopApp();
|
|
292
|
+
await Promise.allSettled([logTask, pollTask]);
|
|
293
|
+
},
|
|
294
|
+
getState: async () => state,
|
|
295
|
+
getLogs: () => logBuffer.getLogs(),
|
|
296
|
+
getCrashDetails: crashReporter.getCrashDetails,
|
|
297
|
+
addListener: emitter.addListener,
|
|
298
|
+
removeListener: emitter.removeListener,
|
|
299
|
+
};
|
|
300
|
+
};
|