Fix crash reporter

Summary: Changelog: Make crash reporter pick up and parse .ips crash reports

Reviewed By: LukeDefeo

Differential Revision: D38355089

fbshipit-source-id: dc1b46257ea12c2e67d8a9baab7f35a905be2204
This commit is contained in:
Andrey Goncharov
2022-08-02 10:08:35 -07:00
committed by Facebook GitHub Bot
parent a998ecb956
commit f132d95bee
3 changed files with 106 additions and 26 deletions

View File

@@ -8,15 +8,41 @@
*/
import {
parseIosCrash,
parsePath,
parseIosCrashLegacy,
parsePathLegacy,
shouldShowiOSCrashNotification,
} from '../iOSCrashUtils';
const modernCrashReportPartial = `{"app_name":"Sample","timestamp":"2022-08-02 16:00:06.00 +0100","app_version":"1.0","slice_uuid":"6f0b8b0f-45d8-3a37-9f85-f1b07ccc121e","build_version":"1","platform":7,"bundleID":"com.facebook.flipper","share_with_app_devs":0,"is_first_party":0,"bug_type":"309","os_version":"macOS 12.4 (21F79)","incident_id":"DEE97E5A-409F-472F-96BC-E6B2D3FBB84B","name":"Sample"}
{
"uptime" : 560000,
"procLaunch" : "2022-08-02 16:00:04.3267 +0100",
"procRole" : "Foreground",
"version" : 2,
"userID" : 501,
"deployVersion" : 210,
"modelCode" : "MacPro7,1",
"procStartAbsTime" : 568274646258426,
"coalitionID" : 260443,
"osVersion" : {
"train" : "macOS 12.4",
"build" : "21F79",
"releaseType" : "User"
},
"captureTime" : "2022-08-02 16:00:06.4551 +0100",
"incident" : "DEE97E5A-409F-472F-96BC-E6B2D3FBB84B",
"bug_type" : "309",
"pid" : 59618,
"procExitAbsTime" : 568276773623039,
"cpuType" : "X86-64",
"procName" : "Sample",
"procPath" : "\/Users\/USER\/Library\/Developer\/CoreSimulator\/Devices\/543083BF-313B-422B-A817-377078C830AF\/data\/Containers\/Bundle\/Application\/22053C61-10A5-49AE-9B09-00B0B9EAC36B\/Sample.app\/Sample"
}`;
test('test the parsing of the date and crash info for the log which matches the predefined regex', () => {
const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa Date/Time: 2019-03-21 12:07:00.861 +0000 \n Blaa balaaa';
const crash = parseIosCrash(log);
const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('SIGSEGV');
expect(crash.name).toEqual('SIGSEGV');
@@ -26,7 +52,7 @@ test('test the parsing of the date and crash info for the log which matches the
test('test the parsing of the reason for crash when log matches the crash regex, but there is no mention of date', () => {
const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa';
const crash = parseIosCrash(log);
const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('SIGSEGV');
expect(crash.name).toEqual('SIGSEGV');
@@ -34,7 +60,7 @@ test('test the parsing of the reason for crash when log matches the crash regex,
test('test the parsing of the crash log when log does not match the predefined regex but is alphanumeric', () => {
const log = 'Blaa Blaaa \n Blaa Blaaa \n Blaa Blaaa';
const crash = parseIosCrash(log);
const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown');
expect(crash.name).toEqual('Unknown');
@@ -43,14 +69,14 @@ test('test the parsing of the crash log when log does not match the predefined r
test('test the parsing of the reason for crash when log does not match the predefined regex contains unicode character', () => {
const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: 🍕🐬 \n Blaa Blaa \n Blaa Blaa';
const crash = parseIosCrash(log);
const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown');
expect(crash.name).toEqual('Unknown');
});
test('test the parsing of the reason for crash when log is empty', () => {
const log = '';
const crash = parseIosCrash(log);
const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown');
expect(crash.name).toEqual('Unknown');
@@ -59,7 +85,7 @@ test('test the parsing of the reason for crash when log is empty', () => {
test('test the parsing of the Android crash log with os being iOS', () => {
const log =
'FATAL EXCEPTION: main\nProcess: com.facebook.flipper.sample, PID: 27026\njava.lang.IndexOutOfBoundsException: Index: 190, Size: 0\n\tat java.util.ArrayList.get(ArrayList.java:437)\n\tat com.facebook.flipper.sample.RootComponentSpec.hitGetRequest(RootComponentSpec.java:72)\n\tat com.facebook.flipper.sample.RootComponent.hitGetRequest(RootComponent.java:46)\n';
const crash = parseIosCrash(log);
const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown');
expect(crash.name).toEqual('Unknown');
@@ -68,32 +94,32 @@ test('test the parsing of the Android crash log with os being iOS', () => {
test('test parsing of path when inputs are correct', () => {
const content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/AppName.app/AppName \n Blaa Blaa \n Blaa Blaa';
const id = parsePath(content);
const id = parsePathLegacy(content);
expect(id).toEqual('path/to/simulator/TH1S-15DEV1CE-1D/AppName.app/AppName');
});
test('test parsing of path when path has special characters in it', () => {
let content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa';
let id = parsePath(content);
let id = parsePathLegacy(content);
expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App Name.app/App Name',
);
content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/App_Name.app/App_Name \n Blaa Blaa \n Blaa Blaa';
id = parsePath(content);
id = parsePathLegacy(content);
expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App_Name.app/App_Name',
);
content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/App%20Name.app/App%20Name \n Blaa Blaa \n Blaa Blaa';
id = parsePath(content);
id = parsePathLegacy(content);
expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App%20Name.app/App%20Name',
);
});
test('test parsing of path when a regex is not present', () => {
const content = 'Blaa Blaaa \n Blaa Blaaa \n Blaa Blaa \n Blaa Blaa';
const id = parsePath(content);
const id = parsePathLegacy(content);
expect(id).toEqual(null);
});
@@ -103,6 +129,7 @@ test('test shouldShowCrashNotification function for all correct inputs', () => {
const shouldShowNotification = shouldShowiOSCrashNotification(
'TH1S-15DEV1CE-1D',
content,
true,
);
expect(shouldShowNotification).toEqual(true);
});
@@ -112,6 +139,7 @@ test('test shouldShowiOSCrashNotification function for all correct inputs but in
const shouldShowNotification = shouldShowiOSCrashNotification(
'TH1S-15DEV1CE-1D',
content,
true,
);
expect(shouldShowNotification).toEqual(false);
});
@@ -121,6 +149,7 @@ test('test shouldShowiOSCrashNotification function for undefined device', () =>
const shouldShowNotification = shouldShowiOSCrashNotification(
null as any,
content,
true,
);
expect(shouldShowNotification).toEqual(false);
});
@@ -136,12 +165,29 @@ test('only crashes from the correct device are picked up', () => {
Responsible: SimulatorTrampoline [1246]
User ID: 501`;
expect(shouldShowiOSCrashNotification(serial, crash)).toBe(true);
expect(shouldShowiOSCrashNotification(serial, crash, true)).toBe(true);
// wrong serial
expect(
shouldShowiOSCrashNotification(
'XC9482A2-26A4-404F-A179-A9FB60B077F6',
crash,
true,
),
).toBe(false);
});
test('modern crashes from the correct device are picked up', () => {
const serial = '543083BF-313B-422B-A817-377078C830AF';
expect(
shouldShowiOSCrashNotification(serial, modernCrashReportPartial, false),
).toBe(true);
// wrong serial
expect(
shouldShowiOSCrashNotification(
'XC9482A2-26A4-404F-A179-A9FB60B077F6',
modernCrashReportPartial,
false,
),
).toBe(false);
});

View File

@@ -14,7 +14,7 @@ import os from 'os';
import path from 'path';
import {ServerDevice} from '../ServerDevice';
export function parseIosCrash(content: string) {
export function parseIosCrashLegacy(content: string) {
const regex = /Exception Type: *\w*/;
const arr = regex.exec(content);
const exceptionString = arr ? arr[0] : '';
@@ -42,11 +42,28 @@ export function parseIosCrash(content: string) {
return crash;
}
export function parseIosCrashModern(content: string) {
const lines = content.split('\n');
// Skip the first line of the .ips file as it is useless
const reportBodyString = lines.slice(1).join('\n');
const reportBody = JSON.parse(reportBodyString);
const exception = `${reportBody.exception.type} (${reportBody.exception.signal})`;
const crash: CrashLog = {
callstack: content,
name: exception,
reason: exception,
date: new Date(reportBody.captureTime).getTime(),
};
return crash;
}
export function shouldShowiOSCrashNotification(
serial: string,
content: string,
legacy: boolean,
): boolean {
const appPath = parsePath(content);
const appPath = legacy ? parsePathLegacy(content) : parsePathModern(content);
if (!appPath || !appPath.includes(serial)) {
// Do not show notifications for the app which are not running on this device
return false;
@@ -54,7 +71,7 @@ export function shouldShowiOSCrashNotification(
return true;
}
export function parsePath(content: string): string | null {
export function parsePathLegacy(content: string): string | null {
const regex = /(?<=.*Path: *)[^\n]*/;
const arr = regex.exec(content);
if (!arr || arr.length <= 0) {
@@ -64,6 +81,14 @@ export function parsePath(content: string): string | null {
return path.trim();
}
export function parsePathModern(content: string): string | null {
const lines = content.split('\n');
// Skip the first line of the .ips file as it is useless
const reportBodyString = lines.slice(1).join('\n');
const reportBody = JSON.parse(reportBodyString);
return reportBody.procPath;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export class iOSCrashWatcher extends DeviceListener {
constructor(private readonly device: ServerDevice) {
@@ -77,11 +102,12 @@ export class iOSCrashWatcher extends DeviceListener {
}
const watcher = fs.watch(dir, async (_eventType, filename) => {
// We just parse the crash logs with extension `.crash`
// TODO: Make it work on MacOS 12. ASAP!
// MacOS 12 does not create .crash reports, but uses new .ips files instead with different format.
const checkFileExtension = /.crash$/.exec(filename);
if (!filename || !checkFileExtension) {
const checkFileExtensionLegacy = /.crash$/.exec(filename);
const checkFileExtensionModern = /.ips$/.exec(filename);
if (
!filename ||
!(checkFileExtensionLegacy || checkFileExtensionModern)
) {
return;
}
const filepath = path.join(dir, filename);
@@ -94,9 +120,17 @@ export class iOSCrashWatcher extends DeviceListener {
console.warn('Failed to read crash file', err);
return;
}
if (shouldShowiOSCrashNotification(this.device.serial, data)) {
if (
shouldShowiOSCrashNotification(
this.device.serial,
data,
!!checkFileExtensionLegacy,
)
) {
this.device.flipperServer.emit('device-crash', {
crash: parseIosCrash(data),
crash: checkFileExtensionLegacy
? parseIosCrashLegacy(data)
: parseIosCrashModern(data),
serial: this.device.serial,
});
}