diff --git a/desktop/flipper-plugin/src/ui/Sidebar.tsx b/desktop/flipper-plugin/src/ui/Sidebar.tsx index a5a18e3ae..4e5878645 100644 --- a/desktop/flipper-plugin/src/ui/Sidebar.tsx +++ b/desktop/flipper-plugin/src/ui/Sidebar.tsx @@ -200,12 +200,12 @@ export class Sidebar extends Component { {gutter ? ( {/* Stop propagating mousedown events to prevent SidebarInteractiveContainer from resizing whenever a user starts selecting text in a child */} - e.stopPropagation()}> + e.stopPropagation()}> {children} ) : ( - e.stopPropagation()}> + e.stopPropagation()}> {children} )} diff --git a/desktop/flipper-server-core/src/devices/ios/__tests__/iOSCrashUtils.node.tsx b/desktop/flipper-server-core/src/devices/ios/__tests__/iOSCrashUtils.node.tsx index 7b885d0ed..e837c1bfa 100644 --- a/desktop/flipper-server-core/src/devices/ios/__tests__/iOSCrashUtils.node.tsx +++ b/desktop/flipper-server-core/src/devices/ios/__tests__/iOSCrashUtils.node.tsx @@ -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); }); diff --git a/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx b/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx index 4160accae..a58660c20 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx @@ -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, }); }