Files
flipper/src/plugins/crash_reporter/index.js
Pritesh Nandgaonkar 0048fc6e4a Crash reporter plugin
Summary: This diff adds a static function `onRegisterDevice` which is being called whenever an  device gets registered. This callback is used to add loglisterner for android. I even moved the logic of iOS from `onRegisterPlugin` to this callback. The reason for not adding android log listener in `onRegisterPlugin` was that there were cases when baseDevice was not yet registered before calling `onRegisterPlugin`. For android, I want the instance of `BaseDevice` so that I can add logListener on it.

Reviewed By: danielbuechele

Differential Revision: D13563282

fbshipit-source-id: b5be40f3dbc808bdaeabae28423c563cf2345a22
2019-01-09 10:42:55 -08:00

396 lines
9.8 KiB
JavaScript

/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
* @flow
*/
import {
FlipperDevicePlugin,
Device,
View,
styled,
FlexColumn,
FlexRow,
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, DeviceLogEntry} from 'flipper';
export type Crash = {|
notificationID: string,
callstack: string,
reason: string,
name: string,
|};
export type CrashLog = {|
callstack: string,
reason: string,
name: string,
|};
export type PersistedState = {
crashes: Array<Crash>,
};
const Title = styled(View)({
fontWeight: 'bold',
fontSize: '100%',
color: 'red',
});
const Value = styled(View)({
paddingLeft: '8px',
fontSize: '100%',
fontFamily: 'Monospace',
maxHeight: '200px',
overflow: 'scroll',
});
const RootColumn = styled(FlexColumn)({
paddingLeft: '16px',
paddingRight: '16px',
paddingTop: '8px',
overflow: 'scroll',
});
const CrashRow = styled(FlexRow)({
paddingTop: '8px',
});
const CallStack = styled('pre')({
fontFamily: 'Monospace',
fontSize: '100%',
paddingLeft: '8px',
maxHeight: '500px',
overflow: 'scroll',
});
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,
store.getState().connections.selectedDevice?.os,
)
) {
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,
os: ?string,
): boolean {
if (os && os.includes('Android')) {
return true;
}
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: [],
};
static supportsDevice(device: Device) {
return device.os === 'iOS' || device.os === 'Android';
}
static notificationID: number = 0;
/*
* Reducer to process incoming "send" messages from the mobile counterpart.
*/
static persistedStateReducer = (
persistedState: PersistedState,
method: string,
payload: Object,
): PersistedState => {
if (method === 'crash-report') {
CrashReporterPlugin.notificationID++;
const mergedState: PersistedState = {
crashes: persistedState.crashes.concat([
{
notificationID: CrashReporterPlugin.notificationID.toString(), // All notifications are unique
callstack: payload.callstack,
name: payload.name,
reason: payload.reason,
},
]),
};
return mergedState;
}
return persistedState;
};
static trimCallStackIfPossible = (callstack: string): string => {
let regex = /Application Specific Information:/;
const query = regex.exec(callstack);
return query ? callstack.substring(0, query.index) : callstack;
};
/*
* Callback to provide the currently active notifications.
*/
static getActiveNotifications = (
persistedState: PersistedState,
): Array<Notification> => {
return persistedState.crashes.map((crash: Crash) => {
const id = crash.notificationID;
return {
id,
message: CrashReporterPlugin.trimCallStackIfPossible(crash.callstack),
severity: 'error',
title: 'CRASH: ' + crash.name + ' ' + crash.reason,
action: id,
};
});
};
/*
* This function gets called whenever the device is registered
*/
static onRegisterDevice = (
store: Store,
baseDevice: BaseDevice,
setPersistedState: (
pluginKey: string,
newPluginState: ?PersistedState,
) => void,
): void => {
if (baseDevice.os.includes('iOS')) {
addFileWatcherForiOSCrashLogs(store, setPersistedState);
} else {
const referenceDate = new Date();
(function(
store: Store,
date: Date,
setPersistedState: (
pluginKey: string,
newPluginState: ?PersistedState,
) => void,
) {
baseDevice.addLogListener((entry: DeviceLogEntry) => {
if (
entry.type === 'error' &&
entry.tag === 'AndroidRuntime' &&
entry.date.getTime() - date.getTime() > 0
) {
parseCrashLogAndUpdateState(
store,
entry.message,
setPersistedState,
);
}
});
})(store, referenceDate, setPersistedState);
}
};
openInLogs = (callstack: string) => {
this.props.selectPlugin('DeviceLogs', callstack);
};
render() {
let crash: ?Crash =
this.props.persistedState.crashes.length > 0
? this.props.persistedState.crashes[
this.props.persistedState.crashes.length - 1
]
: null;
if (this.props.deepLinkPayload) {
const id = this.props.deepLinkPayload;
const index = this.props.persistedState.crashes.findIndex(elem => {
return elem.notificationID === id;
});
if (index >= 0) {
crash = this.props.persistedState.crashes[index];
}
}
if (crash) {
const callstackString = crash.callstack;
return (
<RootColumn>
<CrashRow>
<Title>Name</Title>
<Value>{crash.name}</Value>
</CrashRow>
<CrashRow>
<Title>Reason</Title>
<Value>{crash.reason}</Value>
</CrashRow>
<CrashRow>
<Title>CallStack</Title>
</CrashRow>
<CrashRow>
<ContextMenu
items={[
{
label: 'copy',
click: () => {
clipboard.writeText(callstackString);
},
},
]}>
<CallStack>{callstackString}</CallStack>
</ContextMenu>
</CrashRow>
{this.device.os == 'Android' && (
<CrashRow>
<Button
onClick={() => {
//$FlowFixMe: checked that crash is not undefined
this.openInLogs(crash.callstack);
}}>
Open in Logs
</Button>
</CrashRow>
)}
</RootColumn>
);
}
return (
<RootColumn>
<Title>
Dedicated space to debug crashes. Look out for crash notifications
</Title>
</RootColumn>
);
}
}