Crashreporter plugin by adding a watchman for crash log

Summary:
This diff adds a watcher on `~/Library/Logs/Diagnostics/` library and fires a notification whenever a crash log is added there. This will only work for iOS crashes. With this change, for iOS we should be able to see all kind of crash notification be it due to uncaught exception or a native crash or signal errors.

For android, it will still show notifications due to uncaught exceptions. In upcoming diffs, I will change the logic for android too by parsing Logcat logs.

This diff doesn't support physical device crash reporting. The crashes for physical devices are synced to other folder and that too they are symbolicated.

Reviewed By: danielbuechele

Differential Revision: D13404648

fbshipit-source-id: 7219855ebc73451af87f77f90cc3ed0f2ab5287c
This commit is contained in:
Pritesh Nandgaonkar
2018-12-18 13:30:50 -08:00
committed by Facebook Github Bot
parent e3fb1e1d84
commit a12768539e
3 changed files with 353 additions and 15 deletions

View File

@@ -9,7 +9,8 @@ import type {ChildProcess} from 'child_process';
import type {Store} from '../reducers/index.js';
import type Logger from '../fb-stubs/Logger.js';
import type {DeviceType} from '../devices/BaseDevice';
import BaseDevice from '../devices/BaseDevice';
import type {PersistedState} from '../plugins/crash_reporter';
import {RecurringError} from '../utils/errors';
import {promisify} from 'util';
import path from 'path';
@@ -19,6 +20,12 @@ import IOSDevice from '../devices/IOSDevice';
import iosUtil from '../fb-stubs/iOSContainerUtility';
import isProduction from '../utils/isProduction.js';
import GK from '../fb-stubs/GK';
import fs from 'fs';
import os from 'os';
import util from 'util';
import {setPluginState} from '../reducers/pluginStates.js';
import {FlipperDevicePlugin, FlipperPlugin} from '../plugin.js';
import type {State as PluginStatesState} from '../reducers/pluginStates.js';
type iOSSimulatorDevice = {|
state: 'Booted' | 'Shutdown' | 'Shutting Down',
@@ -28,6 +35,12 @@ type iOSSimulatorDevice = {|
udid: string,
|};
type Crash = {|
callstack: string,
reason: string,
name: string,
|};
type IOSDeviceParams = {udid: string, type: DeviceType, name: string};
const portforwardingClient = isProduction()
@@ -51,6 +64,120 @@ window.addEventListener('beforeunload', () => {
portForwarders.forEach(process => process.kill());
});
export function parseCrashLog(content: string): Crash {
const regex = /Exception Type: *[aA-zZ0-9]*/;
const arr = regex.exec(content);
const exceptionString = arr ? arr[0] : '';
const exceptionRegex = /[aA-zZ0-9]*$/;
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 getPersistedState(
pluginKey: string,
persistingPlugin: ?Class<FlipperPlugin<> | FlipperDevicePlugin<>>,
pluginStates: PluginStatesState,
): ?PersistedState {
if (!persistingPlugin) {
return null;
}
const persistedState = {
...persistingPlugin.defaultPersistedState,
...pluginStates[pluginKey],
};
return persistedState;
}
export function getPluginKey(
selectedDevice: ?BaseDevice,
pluginID: string,
): string {
return `${selectedDevice?.serial || 'unknown'}#${pluginID}`;
}
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;
}
function parseCrashLogAndUpdateState(store: Store, content: string) {
const pluginID = 'CrashReporter';
const pluginKey = getPluginKey(
store.getState().connections.selectedDevice,
pluginID,
);
const persistingPlugin: ?Class<
FlipperDevicePlugin<> | FlipperPlugin<>,
> = store.getState().plugins.devicePlugins.get('CrashReporter');
if (!persistingPlugin) {
return;
}
const pluginStates = store.getState().pluginStates;
const persistedState = getPersistedState(
pluginKey,
persistingPlugin,
pluginStates,
);
const newPluginState = getNewPersisitedStateFromCrashLog(
persistedState,
persistingPlugin,
content,
);
if (newPluginState && persistedState !== newPluginState) {
store.dispatch(
setPluginState({
pluginKey,
state: newPluginState,
}),
);
}
}
function addFileWatcherForiOSCrashLogs(store: Store, logger: Logger) {
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));
});
});
}
function queryDevices(store: Store, logger: Logger): Promise<void> {
const {connections} = store.getState();
const currentDeviceIDs: Set<string> = new Set(
@@ -132,6 +259,7 @@ export default (store: Store, logger: Logger) => {
if (process.platform !== 'darwin') {
return;
}
addFileWatcherForiOSCrashLogs(store, logger);
queryDevices(store, logger)
.then(() => {
const simulatorUpdateInterval = setInterval(() => {