Refactor CrashReporter plugin

Summary: This diff refactors CrashReporter Plugin. The business logic of crash reporter plugin was placed in the `iOSDevice.js` as it gets the hook to the initialisation of iOSDevice. This diff moves the business logic to the Crash Reporter plugin files under the plugins folder. To get the hook, so that we can add our watcher, I have added a static function over `FlipperBasePlugin`, which if exists, will be called once `REGISTER_PLUGIN` event is dispatched

Reviewed By: danielbuechele

Differential Revision: D13529035

fbshipit-source-id: 28216b273b4032727859107d8a6751c6465af9ac
This commit is contained in:
Pritesh Nandgaonkar
2018-12-21 06:58:31 -08:00
committed by Facebook Github Bot
parent 780ac863b8
commit 9d4bb45dbc
8 changed files with 282 additions and 192 deletions

View File

@@ -16,8 +16,17 @@ import {
ContextMenu,
clipboard,
Button,
FlipperPlugin,
getPluginKey,
getPersistedState,
BaseDevice,
} from 'flipper';
import fs from 'fs';
import os from 'os';
import util from 'util';
import path from 'path';
import type {Notification} from '../../plugin';
import type {Store} from 'flipper';
export type Crash = {|
notificationID: string,
@@ -26,10 +35,16 @@ export type Crash = {|
name: string,
|};
export type PersistedState = {|
crashes: Array<Crash>,
export type CrashLog = {|
callstack: string,
reason: string,
name: string,
|};
export type PersistedState = {
crashes: Array<Crash>,
};
const Title = styled(View)({
fontWeight: 'bold',
fontSize: '100%',
@@ -63,7 +78,147 @@ const CallStack = styled('pre')({
overflow: 'scroll',
});
export default class CrashReporterPlugin extends FlipperDevicePlugin {
export function getNewPersisitedStateFromCrashLog(
persistedState: ?PersistedState,
persistingPlugin: Class<FlipperDevicePlugin<> | FlipperPlugin<>>,
content: string,
): ?PersistedState {
const crash = parseCrashLog(content);
if (!persistingPlugin.persistedStateReducer) {
return null;
}
const newPluginState = persistingPlugin.persistedStateReducer(
persistedState,
'crash-report',
crash,
);
return newPluginState;
}
export function parseCrashLogAndUpdateState(
store: Store,
content: string,
setPersistedState: (
pluginKey: string,
newPluginState: ?PersistedState,
) => void,
) {
if (
!shouldShowCrashNotification(
store.getState().connections.selectedDevice,
content,
)
) {
return;
}
const pluginID = CrashReporterPlugin.id;
const pluginKey = getPluginKey(
null,
store.getState().connections.selectedDevice,
pluginID,
);
const persistingPlugin: ?Class<
FlipperDevicePlugin<> | FlipperPlugin<>,
> = store.getState().plugins.devicePlugins.get(CrashReporterPlugin.id);
if (!persistingPlugin) {
return;
}
const pluginStates = store.getState().pluginStates;
const persistedState = getPersistedState(
pluginKey,
persistingPlugin,
pluginStates,
);
const newPluginState = getNewPersisitedStateFromCrashLog(
persistedState,
persistingPlugin,
content,
);
setPersistedState(pluginKey, newPluginState);
}
export function shouldShowCrashNotification(
baseDevice: ?BaseDevice,
content: string,
): boolean {
const appPath = parsePath(content);
const serial: string = baseDevice?.serial || 'unknown';
if (!appPath || !appPath.includes(serial)) {
// Do not show notifications for the app which are not the selected one
return false;
}
return true;
}
export function parseCrashLog(content: string): CrashLog {
const regex = /Exception Type: *[\w]*/;
const arr = regex.exec(content);
const exceptionString = arr ? arr[0] : '';
const exceptionRegex = /[\w]*$/;
const tmp = exceptionRegex.exec(exceptionString);
const exception =
tmp && tmp[0].length ? tmp[0] : 'Cannot figure out the cause';
const crash = {
callstack: content,
name: exception,
reason: exception,
};
return crash;
}
export function parsePath(content: string): ?string {
const regex = /Path: *[\w\-\/\.\t\ \_\%]*\n/;
const arr = regex.exec(content);
if (!arr || arr.length <= 0) {
return null;
}
const pathString = arr[0];
const pathRegex = /[\w\-\/\.\t\ \_\%]*\n/;
const tmp = pathRegex.exec(pathString);
if (!tmp || tmp.length == 0) {
return null;
}
const path = tmp[0];
return path.trim();
}
function addFileWatcherForiOSCrashLogs(
store: Store,
setPersistedState: (
pluginKey: string,
newPluginState: ?PersistedState,
) => void,
) {
const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports');
if (!fs.existsSync(dir)) {
// Directory doesn't exist
return;
}
fs.watch(dir, (eventType, filename) => {
// We just parse the crash logs with extension `.crash`
const checkFileExtension = /.crash$/.exec(filename);
if (!filename || !checkFileExtension) {
return;
}
fs.readFile(path.join(dir, filename), 'utf8', function(err, data) {
if (store.getState().connections.selectedDevice?.os != 'iOS') {
// If the selected device is not iOS don't show crash notifications
return;
}
if (err) {
console.error(err);
return;
}
parseCrashLogAndUpdateState(store, util.format(data), setPersistedState);
});
});
}
export default class CrashReporterPlugin extends FlipperDevicePlugin<
*,
*,
PersistedState,
> {
static defaultPersistedState = {
crashes: [],
};
@@ -98,6 +253,19 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin {
return persistedState;
};
/*
* This function gets called whenever plugin is registered
*/
static onRegisterPlugin = (
store: Store,
setPersistedState: (
pluginKey: string,
newPluginState: ?PersistedState,
) => void,
): void => {
addFileWatcherForiOSCrashLogs(store, setPersistedState);
};
static trimCallStackIfPossible = (callstack: string): string => {
let regex = /Application Specific Information:/;
const query = regex.exec(callstack);
@@ -136,7 +304,7 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin {
if (this.props.deepLinkPayload) {
const id = this.props.deepLinkPayload;
const index = this.props.persistedState.crashes.findIndex(elem => {
return elem.notificationID === Number(id);
return elem.notificationID === id;
});
if (index >= 0) {
crash = this.props.persistedState.crashes[index];