Move crash reporting listener to the server

Summary: Changelog: Move crash watcher to the server. Add 'device-crash' event. Add 'device-start-crash-watcher', 'device-stop-crash-watcher' commands. Add 'onDeviceCrash' method to Plugin Client.

Reviewed By: mweststrate

Differential Revision: D33089810

fbshipit-source-id: ed62ee7c1129e5e25af18b444744b0796f567b72
This commit is contained in:
Andrey Goncharov
2021-12-20 11:37:25 -08:00
committed by Facebook GitHub Bot
parent 9fd45b96d2
commit 731749b41f
21 changed files with 519 additions and 426 deletions

View File

@@ -258,8 +258,6 @@ module.exports = {
// TODO: Remove specific plugin overrides down below // TODO: Remove specific plugin overrides down below
'plugins/fb/graphql/data/getQueryFromQueryId.tsx', 'plugins/fb/graphql/data/getQueryFromQueryId.tsx',
'plugins/fb/kaios-portal/kaios-debugger-client/client.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', 'plugins/public/reactdevtools/index.tsx',
], ],
rules: { rules: {

View File

@@ -114,6 +114,10 @@ export type FlipperServerEvents = {
serial: string; serial: string;
entry: DeviceLogEntry; entry: DeviceLogEntry;
}; };
'device-crash': {
serial: string;
crash: CrashLog;
};
'client-setup': UninitializedClient; 'client-setup': UninitializedClient;
'client-connected': ClientDescription; 'client-connected': ClientDescription;
'client-disconnected': {id: string}; 'client-disconnected': {id: string};
@@ -196,6 +200,8 @@ export type FlipperServerCommands = {
'device-list': () => Promise<DeviceDescription[]>; 'device-list': () => Promise<DeviceDescription[]>;
'device-start-logging': (serial: string) => Promise<void>; 'device-start-logging': (serial: string) => Promise<void>;
'device-stop-logging': (serial: string) => Promise<void>; 'device-stop-logging': (serial: string) => Promise<void>;
'device-start-crash-watcher': (serial: string) => Promise<void>;
'device-stop-crash-watcher': (serial: string) => Promise<void>;
'device-supports-screenshot': (serial: string) => Promise<boolean>; 'device-supports-screenshot': (serial: string) => Promise<boolean>;
'device-supports-screencapture': (serial: string) => Promise<boolean>; 'device-supports-screencapture': (serial: string) => Promise<boolean>;
'device-take-screenshot': (serial: string) => Promise<string>; // base64 encoded buffer 'device-take-screenshot': (serial: string) => Promise<string>; // base64 encoded buffer
@@ -571,3 +577,10 @@ export type ResponseMessage =
success?: never; success?: never;
error: ClientErrorType; error: ClientErrorType;
}; };
export type CrashLog = {
callstack?: string;
reason: string;
name: string;
date?: number;
};

View File

@@ -78,6 +78,8 @@ test('Correct top level API exposed', () => {
expect(exposedTypes.sort()).toMatchInlineSnapshot(` expect(exposedTypes.sort()).toMatchInlineSnapshot(`
Array [ Array [
"Atom", "Atom",
"CrashLog",
"CrashLogListener",
"DataDescriptionType", "DataDescriptionType",
"DataInspectorExpanded", "DataInspectorExpanded",
"DataTableColumn", "DataTableColumn",

View File

@@ -22,6 +22,7 @@ export {
Device, Device,
DeviceLogListener, DeviceLogListener,
DevicePluginClient, DevicePluginClient,
CrashLogListener,
SandyDevicePluginInstance as _SandyDevicePluginInstance, SandyDevicePluginInstance as _SandyDevicePluginInstance,
} from './plugin/DevicePlugin'; } from './plugin/DevicePlugin';
export {SandyPluginDefinition as _SandyPluginDefinition} from './plugin/SandyPluginDefinition'; export {SandyPluginDefinition as _SandyPluginDefinition} from './plugin/SandyPluginDefinition';
@@ -148,4 +149,5 @@ export {
DeviceLogEntry, DeviceLogEntry,
DeviceLogLevel, DeviceLogLevel,
Logger, Logger,
CrashLog,
} from 'flipper-common'; } from 'flipper-common';

View File

@@ -11,9 +11,10 @@ import {SandyPluginDefinition} from './SandyPluginDefinition';
import {BasePluginInstance, BasePluginClient} from './PluginBase'; import {BasePluginInstance, BasePluginClient} from './PluginBase';
import {FlipperLib} from './FlipperLib'; import {FlipperLib} from './FlipperLib';
import {Atom, ReadOnlyAtom} from '../state/atom'; 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 DeviceLogListener = (entry: DeviceLogEntry) => void;
export type CrashLogListener = (crash: CrashLog) => void;
export interface Device { export interface Device {
readonly isArchived: boolean; readonly isArchived: boolean;
@@ -24,7 +25,9 @@ export interface Device {
readonly connected: Atom<boolean>; readonly connected: Atom<boolean>;
executeShell(command: string): Promise<string>; executeShell(command: string): Promise<string>;
addLogListener(callback: DeviceLogListener): Symbol; addLogListener(callback: DeviceLogListener): Symbol;
addCrashListener(callback: CrashLogListener): Symbol;
removeLogListener(id: Symbol): void; removeLogListener(id: Symbol): void;
removeCrashListener(id: Symbol): void;
executeShell(command: string): Promise<string>; executeShell(command: string): Promise<string>;
forwardPort(local: string, remote: string): Promise<boolean>; forwardPort(local: string, remote: string): Promise<boolean>;
clearLogs(): Promise<void>; clearLogs(): Promise<void>;

View File

@@ -12,7 +12,7 @@ import EventEmitter from 'eventemitter3';
import {SandyPluginDefinition} from './SandyPluginDefinition'; import {SandyPluginDefinition} from './SandyPluginDefinition';
import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry'; import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
import {FlipperLib} from './FlipperLib'; import {FlipperLib} from './FlipperLib';
import {Device, DeviceLogListener} from './DevicePlugin'; import {CrashLogListener, Device, DeviceLogListener} from './DevicePlugin';
import {batched} from '../state/batch'; import {batched} from '../state/batch';
import {Idler} from '../utils/Idler'; import {Idler} from '../utils/Idler';
import {Notification} from './Notification'; import {Notification} from './Notification';
@@ -84,6 +84,12 @@ export interface BasePluginClient {
*/ */
onDeviceLogEntry(cb: DeviceLogListener): () => void; 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). * Creates a Paste (similar to a Github Gist).
* Facebook only function. Resolves to undefined if creating a paste failed. * Facebook only function. Resolves to undefined if creating a paste failed.
@@ -186,6 +192,7 @@ export abstract class BasePluginInstance {
menuEntries: NormalizedMenuEntry[] = []; menuEntries: NormalizedMenuEntry[] = [];
logListeners: Symbol[] = []; logListeners: Symbol[] = [];
crashListeners: Symbol[] = [];
readonly instanceId = ++staticInstanceId; readonly instanceId = ++staticInstanceId;
@@ -316,6 +323,13 @@ export abstract class BasePluginInstance {
this.device.removeLogListener(handle); 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, writeTextToClipboard: this.flipperLib.writeTextToClipboard,
createPaste: this.flipperLib.createPaste, createPaste: this.flipperLib.createPaste,
isFB: this.flipperLib.isFB, isFB: this.flipperLib.isFB,
@@ -365,6 +379,9 @@ export abstract class BasePluginInstance {
this.logListeners.splice(0).forEach((handle) => { this.logListeners.splice(0).forEach((handle) => {
this.device.removeLogListener(handle); this.device.removeLogListener(handle);
}); });
this.crashListeners.splice(0).forEach((handle) => {
this.device.removeCrashListener(handle);
});
this.events.emit('destroy'); this.events.emit('destroy');
this.destroyed = true; this.destroyed = true;
} }

View File

@@ -32,6 +32,7 @@ import {
SandyDevicePluginInstance, SandyDevicePluginInstance,
Device, Device,
DeviceLogListener, DeviceLogListener,
CrashLogListener,
} from '../plugin/DevicePlugin'; } from '../plugin/DevicePlugin';
import {BasePluginInstance} from '../plugin/PluginBase'; import {BasePluginInstance} from '../plugin/PluginBase';
import {FlipperLib} from '../plugin/FlipperLib'; import {FlipperLib} from '../plugin/FlipperLib';
@@ -553,6 +554,7 @@ function createMockDevice(options?: StartPluginOptions): Device & {
addLogEntry(entry: DeviceLogEntry): void; addLogEntry(entry: DeviceLogEntry): void;
} { } {
const logListeners: (undefined | DeviceLogListener)[] = []; const logListeners: (undefined | DeviceLogListener)[] = [];
const crashListeners: (undefined | CrashLogListener)[] = [];
return { return {
os: 'Android', os: 'Android',
deviceType: 'emulator', deviceType: 'emulator',
@@ -566,6 +568,13 @@ function createMockDevice(options?: StartPluginOptions): Device & {
removeLogListener(idx) { removeLogListener(idx) {
logListeners[idx as any] = undefined; 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) { addLogEntry(entry: DeviceLogEntry) {
logListeners.forEach((f) => f?.(entry)); logListeners.forEach((f) => f?.(entry));
}, },

View File

@@ -289,6 +289,10 @@ export class FlipperServerImpl implements FlipperServer {
this.getDevice(serial).startLogging(), this.getDevice(serial).startLogging(),
'device-stop-logging': async (serial: string) => 'device-stop-logging': async (serial: string) =>
this.getDevice(serial).stopLogging(), 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) => 'device-supports-screenshot': async (serial: string) =>
this.getDevice(serial).screenshotAvailable(), this.getDevice(serial).screenshotAvailable(),
'device-supports-screencapture': async (serial: string) => 'device-supports-screencapture': async (serial: string) =>

View File

@@ -15,6 +15,8 @@ export abstract class ServerDevice {
readonly flipperServer: FlipperServerImpl; readonly flipperServer: FlipperServerImpl;
connected = true; connected = true;
protected stopCrashWatcherCb?: () => void;
constructor(flipperServer: FlipperServerImpl, info: DeviceDescription) { constructor(flipperServer: FlipperServerImpl, info: DeviceDescription) {
this.flipperServer = flipperServer; this.flipperServer = flipperServer;
this.info = info; this.info = info;
@@ -46,6 +48,20 @@ export abstract class ServerDevice {
// to be subclassed // to be subclassed
} }
startCrashWatcher() {
this.stopCrashWatcherCb = this.startCrashWatcherImpl?.();
}
protected startCrashWatcherImpl(): () => void {
// to be subclassed
return () => {};
}
stopCrashWatcher() {
this.stopCrashWatcherCb?.();
this.stopCrashWatcherCb = undefined;
}
async screenshotAvailable(): Promise<boolean> { async screenshotAvailable(): Promise<boolean> {
return false; return false;
} }

View File

@@ -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);
});
};
}

View File

@@ -17,6 +17,7 @@ import {dirname, join} from 'path';
import {DeviceSpec} from 'flipper-common'; import {DeviceSpec} from 'flipper-common';
import {ServerDevice} from '../ServerDevice'; import {ServerDevice} from '../ServerDevice';
import {FlipperServerImpl} from '../../FlipperServerImpl'; import {FlipperServerImpl} from '../../FlipperServerImpl';
import {startAndroidCrashWatcher} from './AndroidCrashUtils';
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder'; const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
@@ -49,6 +50,7 @@ export default class AndroidDevice extends ServerDevice {
this.adb = adb; this.adb = adb;
} }
// TODO: Prevent starting logging multiple times
startLogging() { startLogging() {
this.adb this.adb
.openLogcat(this.serial, {clear: true}) .openLogcat(this.serial, {clear: true})
@@ -112,6 +114,10 @@ export default class AndroidDevice extends ServerDevice {
this.reader = undefined; this.reader = undefined;
} }
protected startCrashWatcherImpl(): () => void {
return startAndroidCrashWatcher(this);
}
reverse(ports: number[]): Promise<void> { reverse(ports: number[]): Promise<void> {
return Promise.all( return Promise.all(
ports.map((port) => ports.map((port) =>

View File

@@ -7,23 +7,23 @@
* @format * @format
*/ */
import {DeviceLogEntry, DeviceLogLevel} from 'flipper-plugin'; import {Entry, Priority} from 'adbkit-logcat';
import {shouldParseAndroidLog} from '../android-crash-utils'; import {parseAndroidCrash, shouldParseAndroidLog} from '../AndroidCrashUtils';
function getAndroidLog( function getAndroidLog(
date: Date, date: Date,
type: DeviceLogLevel, priority: number,
tag: string, tag: string,
message: 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', () => { test('test shouldParseAndroidLog function for type error and tag is AndroidRuntime', () => {
const referenceDate = new Date(); 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 new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
'error', Priority.ERROR,
'AndroidRuntime', 'AndroidRuntime',
'Possible runtime crash', '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', () => { test('test shouldParseAndroidLog function for type non-error', () => {
const referenceDate = new Date(); 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 new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
'debug', Priority.DEBUG,
'fb4a.activitymanager', 'fb4a.activitymanager',
'Possible debug info in 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', () => { test('test shouldParseAndroidLog function for the older android log', () => {
const referenceDate = new Date(); 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 new Date(referenceDate.getTime() - 10000), //This log arrives 10 secs before the refernce time
'error', Priority.ERROR,
'fb4a.activitymanager', 'fb4a.activitymanager',
'Possible error info in 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', () => { test('test shouldParseAndroidLog function for the fatal log', () => {
const referenceDate = new Date(); 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 new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
'fatal', Priority.FATAL,
'arbitrary tag', 'arbitrary tag',
'Possible error info in activitymanager', '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', () => { test('test shouldParseAndroidLog function for the error log which does not staisfy our tags check', () => {
const referenceDate = new Date(); 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 new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
'error', Priority.ERROR,
'arbitrary tag', 'arbitrary tag',
'Possible error info in fb4a', 'Possible error info in fb4a',
); );
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate); const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
expect(shouldParseTheLog).toEqual(false); 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 ');
});

View File

@@ -20,6 +20,7 @@ import {ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB, IOSBridge} from './IOSBridge';
import split2 from 'split2'; import split2 from 'split2';
import {ServerDevice} from '../ServerDevice'; import {ServerDevice} from '../ServerDevice';
import {FlipperServerImpl} from '../../FlipperServerImpl'; import {FlipperServerImpl} from '../../FlipperServerImpl';
import {addFileWatcherForiOSCrashLogs} from './iOSCrashUtils';
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault'; 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 { static getLogLevel(level: string): DeviceLogLevel {
switch (level) { switch (level) {
case 'Default': case 'Default':

View File

@@ -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);
});

View File

@@ -7,11 +7,11 @@
* @format * @format
*/ */
import type {CrashLog} from './index'; import type {CrashLog} from 'flipper-common';
import fs from 'fs-extra'; import fs from 'fs-extra';
import os from 'os'; import os from 'os';
import {path} from 'flipper-plugin'; import path from 'path';
import {UNKNOWN_CRASH_REASON} from './crash-utils'; import {ServerDevice} from '../ServerDevice';
export function parseIosCrash(content: string) { export function parseIosCrash(content: string) {
const regex = /Exception Type: *\w*/; const regex = /Exception Type: *\w*/;
@@ -19,7 +19,7 @@ export function parseIosCrash(content: string) {
const exceptionString = arr ? arr[0] : ''; const exceptionString = arr ? arr[0] : '';
const exceptionRegex = /\w*$/; const exceptionRegex = /\w*$/;
const tmp = exceptionRegex.exec(exceptionString); 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 dateRegex = /Date\/Time: *[\w\s\.:-]*/;
const dateArr = dateRegex.exec(content); const dateArr = dateRegex.exec(content);
@@ -63,16 +63,14 @@ export function parsePath(content: string): string | null {
return path.trim(); return path.trim();
} }
export async function addFileWatcherForiOSCrashLogs( export function addFileWatcherForiOSCrashLogs(device: ServerDevice) {
serial: string,
reportCrash: (payload: CrashLog) => void,
) {
const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports'); const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports');
if (!(await fs.pathExists(dir))) { // eslint-disable-next-line node/no-sync
// Directory doesn't exist if (!fs.pathExistsSync(dir)) {
return; 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` // We just parse the crash logs with extension `.crash`
const checkFileExtension = /.crash$/.exec(filename); const checkFileExtension = /.crash$/.exec(filename);
if (!filename || !checkFileExtension) { if (!filename || !checkFileExtension) {
@@ -88,9 +86,14 @@ export async function addFileWatcherForiOSCrashLogs(
console.warn('Failed to read crash file', err); console.warn('Failed to read crash file', err);
return; return;
} }
if (shouldShowiOSCrashNotification(serial, data)) { if (shouldShowiOSCrashNotification(device.info.serial, data)) {
reportCrash(parseIosCrash(data)); device.flipperServer.emit('device-crash', {
crash: parseIosCrash(data),
serial: device.info.serial,
});
} }
}); });
}); });
return () => watcher.close();
} }

View File

@@ -15,6 +15,7 @@ import {
Idler, Idler,
createState, createState,
getFlipperLib, getFlipperLib,
CrashLogListener,
} from 'flipper-plugin'; } from 'flipper-plugin';
import { import {
DeviceLogEntry, DeviceLogEntry,
@@ -22,6 +23,7 @@ import {
DeviceType, DeviceType,
DeviceDescription, DeviceDescription,
FlipperServer, FlipperServer,
CrashLog,
} from 'flipper-common'; } from 'flipper-common';
import {DeviceSpec, PluginDetails} from 'flipper-common'; import {DeviceSpec, PluginDetails} from 'flipper-common';
import {getPluginKey} from '../utils/pluginKey'; import {getPluginKey} from '../utils/pluginKey';
@@ -85,6 +87,8 @@ export default class BaseDevice implements Device {
logListeners: Map<Symbol, DeviceLogListener> = new Map(); logListeners: Map<Symbol, DeviceLogListener> = new Map();
crashListeners: Map<Symbol, CrashLogListener> = new Map();
readonly connected = createState(true); readonly connected = createState(true);
// if imported, stores the original source location // 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) { async navigateToLocation(location: string) {
return this.flipperServer.exec('device-navigate', this.serial, location); return this.flipperServer.exec('device-navigate', this.serial, location);
} }
@@ -328,6 +378,8 @@ export default class BaseDevice implements Device {
disconnect() { disconnect() {
this.logListeners.clear(); this.logListeners.clear();
this.stopLogging(); this.stopLogging();
this.crashListeners.clear();
this.stopCrashWatcher();
this.connected.set(false); this.connected.set(false);
} }

View File

@@ -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',
});
});

View File

@@ -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);
});

View File

@@ -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);
}
});
}

View File

@@ -11,7 +11,7 @@ import unicodeSubstring from 'unicode-substring';
import type {Crash} from './index'; import type {Crash} from './index';
import {DevicePluginClient} from 'flipper-plugin'; 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 { function truncate(baseString: string, numOfChars: number): string {
if (baseString.length <= numOfChars) { if (baseString.length <= numOfChars) {

View File

@@ -7,11 +7,8 @@
* @format * @format
*/ */
import type {FSWatcher} from 'fs'; import {createState, DevicePluginClient, CrashLog} from 'flipper-plugin';
import {createState, DevicePluginClient} from 'flipper-plugin';
import {showCrashNotification} from './crash-utils'; import {showCrashNotification} from './crash-utils';
import {addFileWatcherForiOSCrashLogs} from './ios-crash-utils';
import {startAndroidCrashWatcher} from './android-crash-utils';
export type Crash = { export type Crash = {
notificationID: string; notificationID: string;
@@ -21,16 +18,8 @@ export type Crash = {
date: number; date: number;
}; };
export type CrashLog = {
callstack: string;
reason: string;
name: string;
date?: number;
};
export function devicePlugin(client: DevicePluginClient) { export function devicePlugin(client: DevicePluginClient) {
let notificationID = -1; let notificationID = -1;
let watcher: Promise<FSWatcher | undefined> | undefined = undefined;
const crashes = createState<Crash[]>([], {persist: 'crashes'}); const crashes = createState<Crash[]>([], {persist: 'crashes'});
const selectedCrash = createState<string | undefined>(); const selectedCrash = createState<string | undefined>();
@@ -59,31 +48,13 @@ export function devicePlugin(client: DevicePluginClient) {
// Startup logic to establish log monitoring // Startup logic to establish log monitoring
if (client.device.isConnected) { if (client.device.isConnected) {
if (client.device.os.includes('iOS')) { client.onDeviceCrash(reportCrash);
watcher = addFileWatcherForiOSCrashLogs(
client.device.serial,
reportCrash,
);
} else {
startAndroidCrashWatcher(client, reportCrash);
}
} }
client.onDestroy(() => {
watcher
?.then((watcher) => watcher?.close())
.catch((e) =>
console.error(
'[crash_reporter] FSWatcher failed resoving on destroy:',
e,
),
);
});
return { return {
reportCrash,
crashes, crashes,
selectedCrash, selectedCrash,
reportCrash,
openInLogs(callstack: string) { openInLogs(callstack: string) {
client.selectPlugin('DeviceLogs', callstack); client.selectPlugin('DeviceLogs', callstack);
}, },