diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index 2c0b7fff3..117c54ea9 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -258,8 +258,6 @@ module.exports = { // TODO: Remove specific plugin overrides down below 'plugins/fb/graphql/data/getQueryFromQueryId.tsx', 'plugins/fb/kaios-portal/kaios-debugger-client/client.tsx', - 'plugins/public/crash_reporter/index.tsx', - 'plugins/public/crash_reporter/ios-crash-utils.tsx', 'plugins/public/reactdevtools/index.tsx', ], rules: { diff --git a/desktop/flipper-common/src/server-types.tsx b/desktop/flipper-common/src/server-types.tsx index 633a26752..d2b76a898 100644 --- a/desktop/flipper-common/src/server-types.tsx +++ b/desktop/flipper-common/src/server-types.tsx @@ -114,6 +114,10 @@ export type FlipperServerEvents = { serial: string; entry: DeviceLogEntry; }; + 'device-crash': { + serial: string; + crash: CrashLog; + }; 'client-setup': UninitializedClient; 'client-connected': ClientDescription; 'client-disconnected': {id: string}; @@ -196,6 +200,8 @@ export type FlipperServerCommands = { 'device-list': () => Promise; 'device-start-logging': (serial: string) => Promise; 'device-stop-logging': (serial: string) => Promise; + 'device-start-crash-watcher': (serial: string) => Promise; + 'device-stop-crash-watcher': (serial: string) => Promise; 'device-supports-screenshot': (serial: string) => Promise; 'device-supports-screencapture': (serial: string) => Promise; 'device-take-screenshot': (serial: string) => Promise; // base64 encoded buffer @@ -571,3 +577,10 @@ export type ResponseMessage = success?: never; error: ClientErrorType; }; + +export type CrashLog = { + callstack?: string; + reason: string; + name: string; + date?: number; +}; diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index 2cb331546..2cb6c20fc 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -78,6 +78,8 @@ test('Correct top level API exposed', () => { expect(exposedTypes.sort()).toMatchInlineSnapshot(` Array [ "Atom", + "CrashLog", + "CrashLogListener", "DataDescriptionType", "DataInspectorExpanded", "DataTableColumn", diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 1ed5dc15f..406c11842 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -22,6 +22,7 @@ export { Device, DeviceLogListener, DevicePluginClient, + CrashLogListener, SandyDevicePluginInstance as _SandyDevicePluginInstance, } from './plugin/DevicePlugin'; export {SandyPluginDefinition as _SandyPluginDefinition} from './plugin/SandyPluginDefinition'; @@ -148,4 +149,5 @@ export { DeviceLogEntry, DeviceLogLevel, Logger, + CrashLog, } from 'flipper-common'; diff --git a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx index c655b5d6d..cd1d0a44d 100644 --- a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx +++ b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx @@ -11,9 +11,10 @@ import {SandyPluginDefinition} from './SandyPluginDefinition'; import {BasePluginInstance, BasePluginClient} from './PluginBase'; import {FlipperLib} from './FlipperLib'; import {Atom, ReadOnlyAtom} from '../state/atom'; -import {DeviceOS, DeviceType, DeviceLogEntry} from 'flipper-common'; +import {DeviceOS, DeviceType, DeviceLogEntry, CrashLog} from 'flipper-common'; export type DeviceLogListener = (entry: DeviceLogEntry) => void; +export type CrashLogListener = (crash: CrashLog) => void; export interface Device { readonly isArchived: boolean; @@ -24,7 +25,9 @@ export interface Device { readonly connected: Atom; executeShell(command: string): Promise; addLogListener(callback: DeviceLogListener): Symbol; + addCrashListener(callback: CrashLogListener): Symbol; removeLogListener(id: Symbol): void; + removeCrashListener(id: Symbol): void; executeShell(command: string): Promise; forwardPort(local: string, remote: string): Promise; clearLogs(): Promise; diff --git a/desktop/flipper-plugin/src/plugin/PluginBase.tsx b/desktop/flipper-plugin/src/plugin/PluginBase.tsx index 2da1277de..84ee7b098 100644 --- a/desktop/flipper-plugin/src/plugin/PluginBase.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginBase.tsx @@ -12,7 +12,7 @@ import EventEmitter from 'eventemitter3'; import {SandyPluginDefinition} from './SandyPluginDefinition'; import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry'; import {FlipperLib} from './FlipperLib'; -import {Device, DeviceLogListener} from './DevicePlugin'; +import {CrashLogListener, Device, DeviceLogListener} from './DevicePlugin'; import {batched} from '../state/batch'; import {Idler} from '../utils/Idler'; import {Notification} from './Notification'; @@ -84,6 +84,12 @@ export interface BasePluginClient { */ onDeviceLogEntry(cb: DeviceLogListener): () => void; + /** + * Listener that is triggered if the underlying device crashes. + * Listeners established with this mechanism will automatically be cleaned up during destroy + */ + onDeviceCrash(cb: CrashLogListener): () => void; + /** * Creates a Paste (similar to a Github Gist). * Facebook only function. Resolves to undefined if creating a paste failed. @@ -186,6 +192,7 @@ export abstract class BasePluginInstance { menuEntries: NormalizedMenuEntry[] = []; logListeners: Symbol[] = []; + crashListeners: Symbol[] = []; readonly instanceId = ++staticInstanceId; @@ -316,6 +323,13 @@ export abstract class BasePluginInstance { this.device.removeLogListener(handle); }; }, + onDeviceCrash: (cb: CrashLogListener): (() => void) => { + const handle = this.device.addCrashListener(cb); + this.crashListeners.push(handle); + return () => { + this.device.removeCrashListener(handle); + }; + }, writeTextToClipboard: this.flipperLib.writeTextToClipboard, createPaste: this.flipperLib.createPaste, isFB: this.flipperLib.isFB, @@ -365,6 +379,9 @@ export abstract class BasePluginInstance { this.logListeners.splice(0).forEach((handle) => { this.device.removeLogListener(handle); }); + this.crashListeners.splice(0).forEach((handle) => { + this.device.removeCrashListener(handle); + }); this.events.emit('destroy'); this.destroyed = true; } diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index 0e66855ba..5b300f842 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -32,6 +32,7 @@ import { SandyDevicePluginInstance, Device, DeviceLogListener, + CrashLogListener, } from '../plugin/DevicePlugin'; import {BasePluginInstance} from '../plugin/PluginBase'; import {FlipperLib} from '../plugin/FlipperLib'; @@ -553,6 +554,7 @@ function createMockDevice(options?: StartPluginOptions): Device & { addLogEntry(entry: DeviceLogEntry): void; } { const logListeners: (undefined | DeviceLogListener)[] = []; + const crashListeners: (undefined | CrashLogListener)[] = []; return { os: 'Android', deviceType: 'emulator', @@ -566,6 +568,13 @@ function createMockDevice(options?: StartPluginOptions): Device & { removeLogListener(idx) { logListeners[idx as any] = undefined; }, + addCrashListener(cb) { + crashListeners.push(cb); + return (crashListeners.length - 1) as any; + }, + removeCrashListener(idx) { + crashListeners[idx as any] = undefined; + }, addLogEntry(entry: DeviceLogEntry) { logListeners.forEach((f) => f?.(entry)); }, diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index 869e648cd..8ed6827f9 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -289,6 +289,10 @@ export class FlipperServerImpl implements FlipperServer { this.getDevice(serial).startLogging(), 'device-stop-logging': async (serial: string) => this.getDevice(serial).stopLogging(), + 'device-start-crash-watcher': async (serial: string) => + this.getDevice(serial).startCrashWatcher(), + 'device-stop-crash-watcher': async (serial: string) => + this.getDevice(serial).stopCrashWatcher(), 'device-supports-screenshot': async (serial: string) => this.getDevice(serial).screenshotAvailable(), 'device-supports-screencapture': async (serial: string) => diff --git a/desktop/flipper-server-core/src/devices/ServerDevice.tsx b/desktop/flipper-server-core/src/devices/ServerDevice.tsx index c03ba9d5c..72693ea09 100644 --- a/desktop/flipper-server-core/src/devices/ServerDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ServerDevice.tsx @@ -15,6 +15,8 @@ export abstract class ServerDevice { readonly flipperServer: FlipperServerImpl; connected = true; + protected stopCrashWatcherCb?: () => void; + constructor(flipperServer: FlipperServerImpl, info: DeviceDescription) { this.flipperServer = flipperServer; this.info = info; @@ -46,6 +48,20 @@ export abstract class ServerDevice { // to be subclassed } + startCrashWatcher() { + this.stopCrashWatcherCb = this.startCrashWatcherImpl?.(); + } + + protected startCrashWatcherImpl(): () => void { + // to be subclassed + return () => {}; + } + + stopCrashWatcher() { + this.stopCrashWatcherCb?.(); + this.stopCrashWatcherCb = undefined; + } + async screenshotAvailable(): Promise { return false; } diff --git a/desktop/flipper-server-core/src/devices/android/AndroidCrashUtils.tsx b/desktop/flipper-server-core/src/devices/android/AndroidCrashUtils.tsx new file mode 100644 index 000000000..2c89b530a --- /dev/null +++ b/desktop/flipper-server-core/src/devices/android/AndroidCrashUtils.tsx @@ -0,0 +1,119 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import {Entry, Priority} from 'adbkit-logcat'; +import type {CrashLog} from 'flipper-common'; +import AndroidDevice from './AndroidDevice'; + +export function parseAndroidCrash(content: string, logDate?: Date) { + const regForName = /.*\n/; + const nameRegArr = regForName.exec(content); + let name = nameRegArr ? nameRegArr[0] : 'Unknown'; + const regForCallStack = /\tat[\w\s\n\.$&+,:;=?@#|'<>.^*()%!-]*$/; + const callStackArray = regForCallStack.exec(content); + const callStack = callStackArray ? callStackArray[0] : ''; + let remainingString = + callStack.length > 0 ? content.replace(callStack, '') : ''; + if (remainingString[remainingString.length - 1] === '\n') { + remainingString = remainingString.slice(0, -1); + } + const reasonText = + remainingString.length > 0 ? remainingString.split('\n').pop() : 'Unknown'; + const reason = reasonText ? reasonText : 'Unknown'; + if (name[name.length - 1] === '\n') { + name = name.slice(0, -1); + } + const crash: CrashLog = { + callstack: content, + name: name, + reason: reason, + date: logDate?.getTime(), + }; + return crash; +} + +export function shouldParseAndroidLog(entry: Entry, date: Date): boolean { + return ( + entry.date.getTime() - date.getTime() > 0 && // The log should have arrived after the device has been registered + ((entry.priority === Priority.ERROR && entry.tag === 'AndroidRuntime') || + entry.priority === Priority.FATAL) + ); +} + +/** + * Starts listening ADB logs. Emits 'device-crash' on "error" and "fatal" entries. + * Listens to the logs in a separate stream. + * We can't leverage teh existing log listener mechanism (see `startLogging`) + * it is started externally (by the client). Refactoring how we start log listeners is a bit too much. + * It is easier to start its own stream for crash watcher and manage it independently. + */ +export function startAndroidCrashWatcher(device: AndroidDevice) { + const referenceDate = new Date(); + let androidLog: string = ''; + let androidLogUnderProcess = false; + let timer: null | NodeJS.Timeout = null; + let gracefulShutdown = false; + const readerPromise = device.adb + .openLogcat(device.serial, {clear: true}) + .then((reader) => + reader + .on('entry', (entry) => { + if (shouldParseAndroidLog(entry, referenceDate)) { + if (androidLogUnderProcess) { + androidLog += '\n' + entry.message; + androidLog = androidLog.trim(); + if (timer) { + clearTimeout(timer); + } + } else { + androidLog = entry.message; + androidLogUnderProcess = true; + } + timer = setTimeout(() => { + if (androidLog.length > 0) { + device.flipperServer.emit('device-crash', { + crash: parseAndroidCrash(androidLog, entry.date), + serial: device.info.serial, + }); + } + androidLogUnderProcess = false; + androidLog = ''; + }, 50); + } + }) + .on('end', () => { + if (!gracefulShutdown) { + // logs didn't stop gracefully + setTimeout(() => { + if (device.connected) { + console.warn( + `Log stream broken: ${device.serial} - restarting`, + ); + device.startCrashWatcher(); + } + }, 100); + } + }) + .on('error', (e) => { + console.warn('Failed to read from adb logcat: ', e); + }), + ) + .catch((e) => { + console.warn('Failed to open log stream: ', e); + }); + + return () => { + gracefulShutdown = true; + readerPromise + .then((reader) => reader?.end()) + .catch((e) => { + console.error('Failed to close adb logcat stream: ', e); + }); + }; +} diff --git a/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx b/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx index 780fd4c9a..971d7926e 100644 --- a/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx +++ b/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx @@ -17,6 +17,7 @@ import {dirname, join} from 'path'; import {DeviceSpec} from 'flipper-common'; import {ServerDevice} from '../ServerDevice'; import {FlipperServerImpl} from '../../FlipperServerImpl'; +import {startAndroidCrashWatcher} from './AndroidCrashUtils'; const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder'; @@ -49,6 +50,7 @@ export default class AndroidDevice extends ServerDevice { this.adb = adb; } + // TODO: Prevent starting logging multiple times startLogging() { this.adb .openLogcat(this.serial, {clear: true}) @@ -112,6 +114,10 @@ export default class AndroidDevice extends ServerDevice { this.reader = undefined; } + protected startCrashWatcherImpl(): () => void { + return startAndroidCrashWatcher(this); + } + reverse(ports: number[]): Promise { return Promise.all( ports.map((port) => diff --git a/desktop/plugins/public/crash_reporter/__tests__/crashReporterUtility.node.tsx b/desktop/flipper-server-core/src/devices/android/__tests__/AndroidCrashUtils.node.tsx similarity index 54% rename from desktop/plugins/public/crash_reporter/__tests__/crashReporterUtility.node.tsx rename to desktop/flipper-server-core/src/devices/android/__tests__/AndroidCrashUtils.node.tsx index bfc9eaa04..f6f7cd8f8 100644 --- a/desktop/plugins/public/crash_reporter/__tests__/crashReporterUtility.node.tsx +++ b/desktop/flipper-server-core/src/devices/android/__tests__/AndroidCrashUtils.node.tsx @@ -7,23 +7,23 @@ * @format */ -import {DeviceLogEntry, DeviceLogLevel} from 'flipper-plugin'; -import {shouldParseAndroidLog} from '../android-crash-utils'; +import {Entry, Priority} from 'adbkit-logcat'; +import {parseAndroidCrash, shouldParseAndroidLog} from '../AndroidCrashUtils'; function getAndroidLog( date: Date, - type: DeviceLogLevel, + priority: number, tag: string, message: string, -): DeviceLogEntry { - return {date, type, tag, message, app: 'testapp', pid: 0, tid: 0}; +) { + return {date, priority, tag, message, pid: 0, tid: 0} as Entry; } test('test shouldParseAndroidLog function for type error and tag is AndroidRuntime', () => { const referenceDate = new Date(); - const log: DeviceLogEntry = getAndroidLog( + const log = getAndroidLog( new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time - 'error', + Priority.ERROR, 'AndroidRuntime', 'Possible runtime crash', ); @@ -32,9 +32,9 @@ test('test shouldParseAndroidLog function for type error and tag is AndroidRunti }); test('test shouldParseAndroidLog function for type non-error', () => { const referenceDate = new Date(); - const log: DeviceLogEntry = getAndroidLog( + const log = getAndroidLog( new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time - 'debug', + Priority.DEBUG, 'fb4a.activitymanager', 'Possible debug info in activitymanager', ); @@ -43,9 +43,9 @@ test('test shouldParseAndroidLog function for type non-error', () => { }); test('test shouldParseAndroidLog function for the older android log', () => { const referenceDate = new Date(); - const log: DeviceLogEntry = getAndroidLog( + const log = getAndroidLog( new Date(referenceDate.getTime() - 10000), //This log arrives 10 secs before the refernce time - 'error', + Priority.ERROR, 'fb4a.activitymanager', 'Possible error info in activitymanager', ); @@ -54,9 +54,9 @@ test('test shouldParseAndroidLog function for the older android log', () => { }); test('test shouldParseAndroidLog function for the fatal log', () => { const referenceDate = new Date(); - const log: DeviceLogEntry = getAndroidLog( + const log = getAndroidLog( new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time - 'fatal', + Priority.FATAL, 'arbitrary tag', 'Possible error info in activitymanager', ); @@ -65,12 +65,39 @@ test('test shouldParseAndroidLog function for the fatal log', () => { }); test('test shouldParseAndroidLog function for the error log which does not staisfy our tags check', () => { const referenceDate = new Date(); - const log: DeviceLogEntry = getAndroidLog( + const log = getAndroidLog( new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time - 'error', + Priority.ERROR, 'arbitrary tag', 'Possible error info in fb4a', ); const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate); expect(shouldParseTheLog).toEqual(false); }); +test('test the parsing of the Android crash log for the proper android crash format', () => { + 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 date = new Date(); + const crash = parseAndroidCrash(log, date); + expect(crash.callstack).toEqual(log); + expect(crash.reason).toEqual( + 'java.lang.IndexOutOfBoundsException: Index: 190, Size: 0', + ); + expect(crash.name).toEqual('FATAL EXCEPTION: main'); + expect(crash.date).toEqual(date.getTime()); +}); +test('test the parsing of the Android crash log for the unknown crash format and no date', () => { + const log = 'Blaa Blaa Blaa'; + const crash = parseAndroidCrash(log, undefined); + expect(crash.callstack).toEqual(log); + expect(crash.reason).toEqual('Unknown'); + expect(crash.name).toEqual('Unknown'); + expect(crash.date).toBeUndefined(); +}); +test('test the parsing of the Android crash log for the partial format matching the crash format', () => { + const log = 'First Line Break \n Blaa Blaa \n Blaa Blaa '; + const crash = parseAndroidCrash(log, undefined); + expect(crash.callstack).toEqual(log); + expect(crash.reason).toEqual('Unknown'); + expect(crash.name).toEqual('First Line Break '); +}); diff --git a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx index 2415d558f..6b6bd6f5f 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx @@ -20,6 +20,7 @@ import {ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB, IOSBridge} from './IOSBridge'; import split2 from 'split2'; import {ServerDevice} from '../ServerDevice'; import {FlipperServerImpl} from '../../FlipperServerImpl'; +import {addFileWatcherForiOSCrashLogs} from './iOSCrashUtils'; type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault'; @@ -153,6 +154,10 @@ export default class IOSDevice extends ServerDevice { } } + protected startCrashWatcherImpl(): () => void { + return addFileWatcherForiOSCrashLogs(this); + } + static getLogLevel(level: string): DeviceLogLevel { switch (level) { case 'Default': 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 new file mode 100644 index 000000000..51a5d5b7a --- /dev/null +++ b/desktop/flipper-server-core/src/devices/ios/__tests__/iOSCrashUtils.node.tsx @@ -0,0 +1,147 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import { + parseIosCrash, + parsePath, + shouldShowiOSCrashNotification, +} from '../iOSCrashUtils'; + +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); + expect(crash.callstack).toEqual(log); + expect(crash.reason).toEqual('SIGSEGV'); + expect(crash.name).toEqual('SIGSEGV'); + expect(crash.date).toEqual(new Date('2019-03-21 12:07:00.861').getTime()); +}); + +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); + expect(crash.callstack).toEqual(log); + expect(crash.reason).toEqual('SIGSEGV'); + expect(crash.name).toEqual('SIGSEGV'); +}); + +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); + 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 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); + 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); + expect(crash.callstack).toEqual(log); + expect(crash.reason).toEqual('Unknown'); + expect(crash.name).toEqual('Unknown'); +}); + +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); + expect(crash.callstack).toEqual(log); + expect(crash.reason).toEqual('Unknown'); + expect(crash.name).toEqual('Unknown'); +}); + +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); + 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); + 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); + 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); + 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); + expect(id).toEqual(null); +}); + +test('test shouldShowCrashNotification function for all correct inputs', () => { + const 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'; + const shouldShowNotification = shouldShowiOSCrashNotification( + 'TH1S-15DEV1CE-1D', + content, + ); + expect(shouldShowNotification).toEqual(true); +}); +test('test shouldShowiOSCrashNotification function for all correct inputs but incorrect id', () => { + const content = + 'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-1598DEV1CE-2D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa'; + const shouldShowNotification = shouldShowiOSCrashNotification( + 'TH1S-15DEV1CE-1D', + content, + ); + expect(shouldShowNotification).toEqual(false); +}); +test('test shouldShowiOSCrashNotification function for undefined device', () => { + const content = + 'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-1598DEV1CE-2D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa'; + const shouldShowNotification = shouldShowiOSCrashNotification( + null as any, + content, + ); + expect(shouldShowNotification).toEqual(false); +}); + +test('only crashes from the correct device are picked up', () => { + const serial = 'AC9482A2-26A4-404F-A179-A9FB60B077F6'; + const crash = `Process: Sample [87361] + Path: /Users/USER/Library/Developer/CoreSimulator/Devices/AC9482A2-26A4-404F-A179-A9FB60B077F6/data/Containers/Bundle/Application/9BF91EF9-F915-4745-BE91-EBA397451850/Sample.app/Sample + Identifier: Sample + Version: 1.0 (1) + Code Type: X86-64 (Native) + Parent Process: launchd_sim [70150] + Responsible: SimulatorTrampoline [1246] + User ID: 501`; + + expect(shouldShowiOSCrashNotification(serial, crash)).toBe(true); + // wrong serial + expect( + shouldShowiOSCrashNotification( + 'XC9482A2-26A4-404F-A179-A9FB60B077F6', + crash, + ), + ).toBe(false); +}); diff --git a/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx b/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx similarity index 75% rename from desktop/plugins/public/crash_reporter/ios-crash-utils.tsx rename to desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx index eb67d4985..647676291 100644 --- a/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx @@ -7,11 +7,11 @@ * @format */ -import type {CrashLog} from './index'; +import type {CrashLog} from 'flipper-common'; import fs from 'fs-extra'; import os from 'os'; -import {path} from 'flipper-plugin'; -import {UNKNOWN_CRASH_REASON} from './crash-utils'; +import path from 'path'; +import {ServerDevice} from '../ServerDevice'; export function parseIosCrash(content: string) { const regex = /Exception Type: *\w*/; @@ -19,7 +19,7 @@ export function parseIosCrash(content: string) { const exceptionString = arr ? arr[0] : ''; const exceptionRegex = /\w*$/; const tmp = exceptionRegex.exec(exceptionString); - const exception = tmp && tmp[0].length ? tmp[0] : UNKNOWN_CRASH_REASON; + const exception = tmp && tmp[0].length ? tmp[0] : 'Unknown'; const dateRegex = /Date\/Time: *[\w\s\.:-]*/; const dateArr = dateRegex.exec(content); @@ -63,16 +63,14 @@ export function parsePath(content: string): string | null { return path.trim(); } -export async function addFileWatcherForiOSCrashLogs( - serial: string, - reportCrash: (payload: CrashLog) => void, -) { +export function addFileWatcherForiOSCrashLogs(device: ServerDevice) { const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports'); - if (!(await fs.pathExists(dir))) { - // Directory doesn't exist - return; + // eslint-disable-next-line node/no-sync + if (!fs.pathExistsSync(dir)) { + console.warn('Failed to start iOS crash watcher'); + return () => {}; } - return fs.watch(dir, async (_eventType, filename) => { + const watcher = fs.watch(dir, async (_eventType, filename) => { // We just parse the crash logs with extension `.crash` const checkFileExtension = /.crash$/.exec(filename); if (!filename || !checkFileExtension) { @@ -88,9 +86,14 @@ export async function addFileWatcherForiOSCrashLogs( console.warn('Failed to read crash file', err); return; } - if (shouldShowiOSCrashNotification(serial, data)) { - reportCrash(parseIosCrash(data)); + if (shouldShowiOSCrashNotification(device.info.serial, data)) { + device.flipperServer.emit('device-crash', { + crash: parseIosCrash(data), + serial: device.info.serial, + }); } }); }); + + return () => watcher.close(); } diff --git a/desktop/flipper-ui-core/src/devices/BaseDevice.tsx b/desktop/flipper-ui-core/src/devices/BaseDevice.tsx index 6baa6c7d9..eccddcb1a 100644 --- a/desktop/flipper-ui-core/src/devices/BaseDevice.tsx +++ b/desktop/flipper-ui-core/src/devices/BaseDevice.tsx @@ -15,6 +15,7 @@ import { Idler, createState, getFlipperLib, + CrashLogListener, } from 'flipper-plugin'; import { DeviceLogEntry, @@ -22,6 +23,7 @@ import { DeviceType, DeviceDescription, FlipperServer, + CrashLog, } from 'flipper-common'; import {DeviceSpec, PluginDetails} from 'flipper-common'; import {getPluginKey} from '../utils/pluginKey'; @@ -85,6 +87,8 @@ export default class BaseDevice implements Device { logListeners: Map = new Map(); + crashListeners: Map = new Map(); + readonly connected = createState(true); // if imported, stores the original source location @@ -181,6 +185,52 @@ export default class BaseDevice implements Device { } } + private crashLogEventHandler = (payload: { + serial: string; + crash: CrashLog; + }) => { + if (payload.serial === this.serial && this.crashListeners.size > 0) { + this.addCrashEntry(payload.crash); + } + }; + + addCrashEntry(entry: CrashLog) { + this.crashListeners.forEach((listener) => { + // prevent breaking other listeners, if one listener doesn't work. + try { + listener(entry); + } catch (e) { + console.error(`Crash listener exception:`, e); + } + }); + } + + async startCrashWatcher() { + await this.flipperServer.exec('device-start-crash-watcher', this.serial); + this.flipperServer.on('device-crash', this.crashLogEventHandler); + } + + stopCrashWatcher() { + this.flipperServer.off('device-crash', this.crashLogEventHandler); + return this.flipperServer.exec('device-stop-crash-watcher', this.serial); + } + + addCrashListener(callback: CrashLogListener): Symbol { + if (this.crashListeners.size === 0) { + this.startCrashWatcher(); + } + const id = Symbol(); + this.crashListeners.set(id, callback); + return id; + } + + removeCrashListener(id: Symbol) { + this.crashListeners.delete(id); + if (this.crashListeners.size === 0) { + this.stopCrashWatcher(); + } + } + async navigateToLocation(location: string) { return this.flipperServer.exec('device-navigate', this.serial, location); } @@ -328,6 +378,8 @@ export default class BaseDevice implements Device { disconnect() { this.logListeners.clear(); this.stopLogging(); + this.crashListeners.clear(); + this.stopCrashWatcher(); this.connected.set(false); } diff --git a/desktop/plugins/public/crash_reporter/__tests__/index.node.tsx b/desktop/plugins/public/crash_reporter/__tests__/index.node.tsx new file mode 100644 index 000000000..6ecbaa8db --- /dev/null +++ b/desktop/plugins/public/crash_reporter/__tests__/index.node.tsx @@ -0,0 +1,59 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import {TestUtils, CrashLog} from 'flipper-plugin'; +import * as CrashReporterPlugin from '../index'; + +test('test defaultPersistedState of CrashReporterPlugin', () => { + expect( + TestUtils.startDevicePlugin(CrashReporterPlugin).exportState(), + ).toEqual({crashes: []}); +}); +test('test helper setdefaultPersistedState function', () => { + const crash: CrashLog = { + callstack: 'callstack', + reason: 'crash0', + name: 'crash0', + date: Date.now(), + }; + const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin); + plugin.instance.reportCrash(crash); + expect(plugin.exportState()).toEqual({ + crashes: [ + { + ...crash, + notificationID: '0', + }, + ], + }); +}); +test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState', () => { + const crash: CrashLog = { + callstack: 'callstack', + reason: 'crash0', + name: 'crash0', + date: Date.now(), + }; + const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin); + plugin.instance.reportCrash(crash); + const pluginStateCrash: CrashLog = { + callstack: 'callstack', + reason: 'crash1', + name: 'crash1', + date: Date.now(), + }; + plugin.instance.reportCrash(pluginStateCrash); + const crashes = plugin.instance.crashes.get(); + expect(crashes).toBeDefined(); + expect(crashes.length).toEqual(2); + expect(crashes[1]).toEqual({ + ...pluginStateCrash, + notificationID: '1', + }); +}); diff --git a/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx b/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx deleted file mode 100644 index e5d3e65e9..000000000 --- a/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -import {TestDevice} from 'flipper'; -import {Crash, CrashLog} from '../index'; -import {TestUtils} from 'flipper-plugin'; -import {getPluginKey} from 'flipper'; -import * as CrashReporterPlugin from '../index'; -import { - parseIosCrash, - parsePath, - shouldShowiOSCrashNotification, -} from '../ios-crash-utils'; -import {parseAndroidCrash} from '../android-crash-utils'; - -function getCrash( - id: number, - callstack: string, - name: string, - reason: string, -): Crash & CrashLog { - return { - notificationID: id.toString(), - callstack: callstack, - reason: reason, - name: name, - date: new Date().getTime(), - }; -} - -function assertCrash(crash: Crash, expectedCrash: Crash) { - const {notificationID, callstack, reason, name, date} = crash; - expect(notificationID).toEqual(expectedCrash.notificationID); - expect(callstack).toEqual(expectedCrash.callstack); - expect(reason).toEqual(expectedCrash.reason); - expect(name).toEqual(expectedCrash.name); - expect(Math.abs(date - expectedCrash.date)).toBeLessThan(1000); -} - -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); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('SIGSEGV'); - expect(crash.name).toEqual('SIGSEGV'); - expect(crash.date).toEqual(new Date('2019-03-21 12:07:00.861').getTime()); -}); - -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); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('SIGSEGV'); - expect(crash.name).toEqual('SIGSEGV'); -}); - -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); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('Cannot figure out the cause'); - expect(crash.name).toEqual('Cannot figure out the cause'); -}); - -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); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('Cannot figure out the cause'); - expect(crash.name).toEqual('Cannot figure out the cause'); -}); -test('test the parsing of the reason for crash when log is empty', () => { - const log = ''; - const crash = parseIosCrash(log); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('Cannot figure out the cause'); - expect(crash.name).toEqual('Cannot figure out the cause'); -}); -test('test the parsing of the Android crash log for the proper android crash format', () => { - 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 date = new Date(); - const crash = parseAndroidCrash(log, date); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual( - 'java.lang.IndexOutOfBoundsException: Index: 190, Size: 0', - ); - expect(crash.name).toEqual('FATAL EXCEPTION: main'); - expect(crash.date).toEqual(date.getTime()); -}); -test('test the parsing of the Android crash log for the unknown crash format and no date', () => { - const log = 'Blaa Blaa Blaa'; - const crash = parseAndroidCrash(log, undefined); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('Cannot figure out the cause'); - expect(crash.name).toEqual('Cannot figure out the cause'); - expect(crash.date).toBeUndefined(); -}); -test('test the parsing of the Android crash log for the partial format matching the crash format', () => { - const log = 'First Line Break \n Blaa Blaa \n Blaa Blaa '; - const crash = parseAndroidCrash(log, undefined); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('Cannot figure out the cause'); - expect(crash.name).toEqual('First Line Break '); -}); -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); - expect(crash.callstack).toEqual(log); - expect(crash.reason).toEqual('Cannot figure out the cause'); - expect(crash.name).toEqual('Cannot figure out the cause'); -}); -test('test the getter of pluginKey with proper input', () => { - const device = new TestDevice('serial', 'emulator', 'test device', 'iOS'); - const pluginKey = getPluginKey(null, device, 'CrashReporter'); - expect(pluginKey).toEqual('serial#CrashReporter'); -}); -test('test the getter of pluginKey with undefined input', () => { - const pluginKey = getPluginKey(null, null, 'CrashReporter'); - expect(pluginKey).toEqual('unknown#CrashReporter'); -}); -test('test the getter of pluginKey with defined selected app', () => { - const pluginKey = getPluginKey('selectedApp', null, 'CrashReporter'); - expect(pluginKey).toEqual('selectedApp#CrashReporter'); -}); -test('test the getter of pluginKey with defined selected app and defined base device', () => { - const device = new TestDevice('serial', 'emulator', 'test device', 'iOS'); - const pluginKey = getPluginKey('selectedApp', device, 'CrashReporter'); - expect(pluginKey).toEqual('selectedApp#CrashReporter'); -}); -test('test defaultPersistedState of CrashReporterPlugin', () => { - expect( - TestUtils.startDevicePlugin(CrashReporterPlugin).exportState(), - ).toEqual({crashes: []}); -}); -test('test helper setdefaultPersistedState function', () => { - const crash = getCrash(0, 'callstack', 'crash0', 'crash0'); - const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin); - plugin.instance.reportCrash(crash); - expect(plugin.exportState()).toEqual({ - crashes: [crash], - }); -}); -test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState', () => { - const crash = getCrash(0, 'callstack', 'crash0', 'crash0'); - const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin); - plugin.instance.reportCrash(crash); - const pluginStateCrash = getCrash(1, 'callstack', 'crash1', 'crash1'); - plugin.instance.reportCrash(pluginStateCrash); - const crashes = plugin.instance.crashes.get(); - expect(crashes).toBeDefined(); - expect(crashes.length).toEqual(2); - expect(crashes[1]).toEqual(pluginStateCrash); -}); - -test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState and improper crash log', () => { - const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin); - const pluginStateCrash = getCrash(0, 'callstack', 'crash1', 'crash1'); - plugin.instance.reportCrash(pluginStateCrash); - const content = 'Blaa Blaaa \n Blaa Blaaa'; - plugin.instance.reportCrash(parseIosCrash(content)); - const crashes = plugin.instance.crashes.get(); - expect(crashes.length).toEqual(2); - assertCrash(crashes[0], pluginStateCrash); - assertCrash( - crashes[1], - getCrash( - 1, - content, - 'Cannot figure out the cause', - 'Cannot figure out the cause', - ), - ); -}); - -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); - 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); - 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); - 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); - 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); - expect(id).toEqual(null); -}); - -test('test shouldShowCrashNotification function for all correct inputs', () => { - const device = new TestDevice( - 'TH1S-15DEV1CE-1D', - 'emulator', - 'test device', - 'iOS', - ); - const 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'; - const shouldShowNotification = shouldShowiOSCrashNotification( - device.serial, - content, - ); - expect(shouldShowNotification).toEqual(true); -}); -test('test shouldShowiOSCrashNotification function for all correct inputs but incorrect id', () => { - const device = new TestDevice( - 'TH1S-15DEV1CE-1D', - 'emulator', - 'test device', - 'iOS', - ); - const content = - 'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-1598DEV1CE-2D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa'; - const shouldShowNotification = shouldShowiOSCrashNotification( - device.serial, - content, - ); - expect(shouldShowNotification).toEqual(false); -}); -test('test shouldShowiOSCrashNotification function for undefined device', () => { - const content = - 'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-1598DEV1CE-2D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa'; - const shouldShowNotification = shouldShowiOSCrashNotification( - null as any, - content, - ); - expect(shouldShowNotification).toEqual(false); -}); - -test('only crashes from the correct device are picked up', () => { - const serial = 'AC9482A2-26A4-404F-A179-A9FB60B077F6'; - const crash = `Process: Sample [87361] - Path: /Users/USER/Library/Developer/CoreSimulator/Devices/AC9482A2-26A4-404F-A179-A9FB60B077F6/data/Containers/Bundle/Application/9BF91EF9-F915-4745-BE91-EBA397451850/Sample.app/Sample - Identifier: Sample - Version: 1.0 (1) - Code Type: X86-64 (Native) - Parent Process: launchd_sim [70150] - Responsible: SimulatorTrampoline [1246] - User ID: 501`; - - expect(shouldShowiOSCrashNotification(serial, crash)).toBe(true); - // wrong serial - expect( - shouldShowiOSCrashNotification( - 'XC9482A2-26A4-404F-A179-A9FB60B077F6', - crash, - ), - ).toBe(false); -}); diff --git a/desktop/plugins/public/crash_reporter/android-crash-utils.tsx b/desktop/plugins/public/crash_reporter/android-crash-utils.tsx deleted file mode 100644 index bf9d0c487..000000000 --- a/desktop/plugins/public/crash_reporter/android-crash-utils.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -import type {DeviceLogEntry, DevicePluginClient} from 'flipper-plugin'; -import {UNKNOWN_CRASH_REASON} from './crash-utils'; -import type {CrashLog} from './index'; - -export function parseAndroidCrash(content: string, logDate?: Date) { - const regForName = /.*\n/; - const nameRegArr = regForName.exec(content); - let name = nameRegArr ? nameRegArr[0] : UNKNOWN_CRASH_REASON; - const regForCallStack = /\tat[\w\s\n\.$&+,:;=?@#|'<>.^*()%!-]*$/; - const callStackArray = regForCallStack.exec(content); - const callStack = callStackArray ? callStackArray[0] : ''; - let remainingString = - callStack.length > 0 ? content.replace(callStack, '') : ''; - if (remainingString[remainingString.length - 1] === '\n') { - remainingString = remainingString.slice(0, -1); - } - const reasonText = - remainingString.length > 0 - ? remainingString.split('\n').pop() - : UNKNOWN_CRASH_REASON; - const reason = reasonText ? reasonText : UNKNOWN_CRASH_REASON; - if (name[name.length - 1] === '\n') { - name = name.slice(0, -1); - } - const crash: CrashLog = { - callstack: content, - name: name, - reason: reason, - date: logDate?.getTime(), - }; - return crash; -} - -export function shouldParseAndroidLog( - entry: DeviceLogEntry, - date: Date, -): boolean { - return ( - entry.date.getTime() - date.getTime() > 0 && // The log should have arrived after the device has been registered - ((entry.type === 'error' && entry.tag === 'AndroidRuntime') || - entry.type === 'fatal') - ); -} - -export function startAndroidCrashWatcher( - client: DevicePluginClient, - reportCrash: (payload: CrashLog) => void, -) { - const referenceDate = new Date(); - let androidLog: string = ''; - let androidLogUnderProcess = false; - let timer: null | NodeJS.Timeout = null; - client.onDeviceLogEntry((entry: DeviceLogEntry) => { - if (shouldParseAndroidLog(entry, referenceDate)) { - if (androidLogUnderProcess) { - androidLog += '\n' + entry.message; - androidLog = androidLog.trim(); - if (timer) { - clearTimeout(timer); - } - } else { - androidLog = entry.message; - androidLogUnderProcess = true; - } - timer = setTimeout(() => { - if (androidLog.length > 0) { - reportCrash(parseAndroidCrash(androidLog, entry.date)); - } - androidLogUnderProcess = false; - androidLog = ''; - }, 50); - } - }); -} diff --git a/desktop/plugins/public/crash_reporter/crash-utils.tsx b/desktop/plugins/public/crash_reporter/crash-utils.tsx index 58560a3fd..71ab14fcc 100644 --- a/desktop/plugins/public/crash_reporter/crash-utils.tsx +++ b/desktop/plugins/public/crash_reporter/crash-utils.tsx @@ -11,7 +11,7 @@ import unicodeSubstring from 'unicode-substring'; import type {Crash} from './index'; import {DevicePluginClient} from 'flipper-plugin'; -export const UNKNOWN_CRASH_REASON = 'Cannot figure out the cause'; +export const UNKNOWN_CRASH_REASON = 'Unknown'; function truncate(baseString: string, numOfChars: number): string { if (baseString.length <= numOfChars) { diff --git a/desktop/plugins/public/crash_reporter/index.tsx b/desktop/plugins/public/crash_reporter/index.tsx index bb35c68de..2a8ae252b 100644 --- a/desktop/plugins/public/crash_reporter/index.tsx +++ b/desktop/plugins/public/crash_reporter/index.tsx @@ -7,11 +7,8 @@ * @format */ -import type {FSWatcher} from 'fs'; -import {createState, DevicePluginClient} from 'flipper-plugin'; +import {createState, DevicePluginClient, CrashLog} from 'flipper-plugin'; import {showCrashNotification} from './crash-utils'; -import {addFileWatcherForiOSCrashLogs} from './ios-crash-utils'; -import {startAndroidCrashWatcher} from './android-crash-utils'; export type Crash = { notificationID: string; @@ -21,16 +18,8 @@ export type Crash = { date: number; }; -export type CrashLog = { - callstack: string; - reason: string; - name: string; - date?: number; -}; - export function devicePlugin(client: DevicePluginClient) { let notificationID = -1; - let watcher: Promise | undefined = undefined; const crashes = createState([], {persist: 'crashes'}); const selectedCrash = createState(); @@ -59,31 +48,13 @@ export function devicePlugin(client: DevicePluginClient) { // Startup logic to establish log monitoring if (client.device.isConnected) { - if (client.device.os.includes('iOS')) { - watcher = addFileWatcherForiOSCrashLogs( - client.device.serial, - reportCrash, - ); - } else { - startAndroidCrashWatcher(client, reportCrash); - } + client.onDeviceCrash(reportCrash); } - client.onDestroy(() => { - watcher - ?.then((watcher) => watcher?.close()) - .catch((e) => - console.error( - '[crash_reporter] FSWatcher failed resoving on destroy:', - e, - ), - ); - }); - return { - reportCrash, crashes, selectedCrash, + reportCrash, openInLogs(callstack: string) { client.selectPlugin('DeviceLogs', callstack); },