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

@@ -200,12 +200,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{gutter ? ( {gutter ? (
<GutterWrapper position={position}> <GutterWrapper position={position}>
{/* Stop propagating mousedown events to prevent SidebarInteractiveContainer from resizing whenever a user starts selecting text in a child */} {/* Stop propagating mousedown events to prevent SidebarInteractiveContainer from resizing whenever a user starts selecting text in a child */}
<Layout.Container onMouseDown={(e) => e.stopPropagation()}> <Layout.Container grow onMouseDown={(e) => e.stopPropagation()}>
{children} {children}
</Layout.Container> </Layout.Container>
</GutterWrapper> </GutterWrapper>
) : ( ) : (
<Layout.Container onMouseDown={(e) => e.stopPropagation()}> <Layout.Container grow onMouseDown={(e) => e.stopPropagation()}>
{children} {children}
</Layout.Container> </Layout.Container>
)} )}

View File

@@ -8,15 +8,41 @@
*/ */
import { import {
parseIosCrash, parseIosCrashLegacy,
parsePath, parsePathLegacy,
shouldShowiOSCrashNotification, shouldShowiOSCrashNotification,
} from '../iOSCrashUtils'; } 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', () => { test('test the parsing of the date and crash info for the log which matches the predefined regex', () => {
const log = 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'; '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.callstack).toEqual(log);
expect(crash.reason).toEqual('SIGSEGV'); expect(crash.reason).toEqual('SIGSEGV');
expect(crash.name).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', () => { test('test the parsing of the reason for crash when log matches the crash regex, but there is no mention of date', () => {
const log = const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa'; '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.callstack).toEqual(log);
expect(crash.reason).toEqual('SIGSEGV'); expect(crash.reason).toEqual('SIGSEGV');
expect(crash.name).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', () => { 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 log = 'Blaa Blaaa \n Blaa Blaaa \n Blaa Blaaa';
const crash = parseIosCrash(log); const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log); expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown'); expect(crash.reason).toEqual('Unknown');
expect(crash.name).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', () => { test('test the parsing of the reason for crash when log does not match the predefined regex contains unicode character', () => {
const log = const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: 🍕🐬 \n Blaa Blaa \n Blaa Blaa'; '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.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown'); expect(crash.reason).toEqual('Unknown');
expect(crash.name).toEqual('Unknown'); expect(crash.name).toEqual('Unknown');
}); });
test('test the parsing of the reason for crash when log is empty', () => { test('test the parsing of the reason for crash when log is empty', () => {
const log = ''; const log = '';
const crash = parseIosCrash(log); const crash = parseIosCrashLegacy(log);
expect(crash.callstack).toEqual(log); expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown'); expect(crash.reason).toEqual('Unknown');
expect(crash.name).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', () => { test('test the parsing of the Android crash log with os being iOS', () => {
const log = 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'; '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.callstack).toEqual(log);
expect(crash.reason).toEqual('Unknown'); expect(crash.reason).toEqual('Unknown');
expect(crash.name).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', () => { test('test parsing of path when inputs are correct', () => {
const content = const content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/AppName.app/AppName \n Blaa Blaa \n Blaa Blaa'; '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'); expect(id).toEqual('path/to/simulator/TH1S-15DEV1CE-1D/AppName.app/AppName');
}); });
test('test parsing of path when path has special characters in it', () => { test('test parsing of path when path has special characters in it', () => {
let content = 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'; '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( expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App Name.app/App Name', 'path/to/simulator/TH1S-15DEV1CE-1D/App Name.app/App Name',
); );
content = 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'; '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( expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App_Name.app/App_Name', 'path/to/simulator/TH1S-15DEV1CE-1D/App_Name.app/App_Name',
); );
content = 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'; '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( expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App%20Name.app/App%20Name', 'path/to/simulator/TH1S-15DEV1CE-1D/App%20Name.app/App%20Name',
); );
}); });
test('test parsing of path when a regex is not present', () => { 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 content = 'Blaa Blaaa \n Blaa Blaaa \n Blaa Blaa \n Blaa Blaa';
const id = parsePath(content); const id = parsePathLegacy(content);
expect(id).toEqual(null); expect(id).toEqual(null);
}); });
@@ -103,6 +129,7 @@ test('test shouldShowCrashNotification function for all correct inputs', () => {
const shouldShowNotification = shouldShowiOSCrashNotification( const shouldShowNotification = shouldShowiOSCrashNotification(
'TH1S-15DEV1CE-1D', 'TH1S-15DEV1CE-1D',
content, content,
true,
); );
expect(shouldShowNotification).toEqual(true); expect(shouldShowNotification).toEqual(true);
}); });
@@ -112,6 +139,7 @@ test('test shouldShowiOSCrashNotification function for all correct inputs but in
const shouldShowNotification = shouldShowiOSCrashNotification( const shouldShowNotification = shouldShowiOSCrashNotification(
'TH1S-15DEV1CE-1D', 'TH1S-15DEV1CE-1D',
content, content,
true,
); );
expect(shouldShowNotification).toEqual(false); expect(shouldShowNotification).toEqual(false);
}); });
@@ -121,6 +149,7 @@ test('test shouldShowiOSCrashNotification function for undefined device', () =>
const shouldShowNotification = shouldShowiOSCrashNotification( const shouldShowNotification = shouldShowiOSCrashNotification(
null as any, null as any,
content, content,
true,
); );
expect(shouldShowNotification).toEqual(false); expect(shouldShowNotification).toEqual(false);
}); });
@@ -136,12 +165,29 @@ test('only crashes from the correct device are picked up', () => {
Responsible: SimulatorTrampoline [1246] Responsible: SimulatorTrampoline [1246]
User ID: 501`; User ID: 501`;
expect(shouldShowiOSCrashNotification(serial, crash)).toBe(true); expect(shouldShowiOSCrashNotification(serial, crash, true)).toBe(true);
// wrong serial // wrong serial
expect( expect(
shouldShowiOSCrashNotification( shouldShowiOSCrashNotification(
'XC9482A2-26A4-404F-A179-A9FB60B077F6', 'XC9482A2-26A4-404F-A179-A9FB60B077F6',
crash, 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); ).toBe(false);
}); });

View File

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