Move plugins to "sonar/desktop/plugins"
Summary: Plugins moved from "sonar/desktop/src/plugins" to "sonar/desktop/plugins". Fixed all the paths after moving. New "desktop" folder structure: - `src` - Flipper desktop app JS code executing in Electron Renderer (Chrome) process. - `static` - Flipper desktop app JS code executing in Electron Main (Node.js) process. - `plugins` - Flipper desktop JS plugins. - `pkg` - Flipper packaging lib and CLI tool. - `doctor` - Flipper diagnostics lib and CLI tool. - `scripts` - Build scripts for Flipper desktop app. - `headless` - Headless version of Flipper desktop app. - `headless-tests` - Integration tests running agains Flipper headless version. Reviewed By: mweststrate Differential Revision: D20344186 fbshipit-source-id: d020da970b2ea1e001f9061a8782bfeb54e31ba0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
beb5c85e69
commit
10d990c32c
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* 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 BaseDevice from '../../../src/devices/BaseDevice.tsx';
|
||||
import CrashReporterPlugin from '../../crash_reporter';
|
||||
import type {PersistedState, Crash} from '../../crash_reporter';
|
||||
import {
|
||||
parseCrashLog,
|
||||
getNewPersisitedStateFromCrashLog,
|
||||
parsePath,
|
||||
shouldShowCrashNotification,
|
||||
} from '../../crash_reporter';
|
||||
import {
|
||||
getPluginKey,
|
||||
getPersistedState,
|
||||
} from '../../../src/utils/pluginUtils.tsx';
|
||||
|
||||
function setDefaultPersistedState(defaultState: PersistedState) {
|
||||
CrashReporterPlugin.defaultPersistedState = defaultState;
|
||||
}
|
||||
|
||||
function setNotificationID(notificationID: number) {
|
||||
CrashReporterPlugin.notificationID = notificationID;
|
||||
}
|
||||
|
||||
function setCrashReporterPluginID(id: string) {
|
||||
CrashReporterPlugin.id = id;
|
||||
}
|
||||
|
||||
function getCrash(
|
||||
id: number,
|
||||
callstack: string,
|
||||
name: string,
|
||||
reason: string,
|
||||
): Crash {
|
||||
return {
|
||||
notificationID: id.toString(),
|
||||
callstack: callstack,
|
||||
reason: reason,
|
||||
name: name,
|
||||
date: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
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(date.toDateString()).toEqual(expectedCrash.date.toDateString());
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setNotificationID(0); // Resets notificationID to 0
|
||||
setDefaultPersistedState({crashes: []}); // Resets defaultpersistedstate
|
||||
setCrashReporterPluginID('CrashReporter');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Reset values
|
||||
setNotificationID(0);
|
||||
setDefaultPersistedState({crashes: []});
|
||||
setCrashReporterPluginID('');
|
||||
});
|
||||
|
||||
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 = parseCrashLog(log, 'iOS');
|
||||
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'));
|
||||
});
|
||||
|
||||
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 = parseCrashLog(log, 'iOS');
|
||||
expect(crash.callstack).toEqual(log);
|
||||
expect(crash.reason).toEqual('SIGSEGV');
|
||||
expect(crash.name).toEqual('SIGSEGV');
|
||||
expect(crash.date).toBeUndefined();
|
||||
});
|
||||
|
||||
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 = parseCrashLog(log, 'iOS');
|
||||
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 = parseCrashLog(log, 'iOS');
|
||||
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 reason for crash when log is empty', () => {
|
||||
const log = '';
|
||||
const crash = parseCrashLog(log, 'iOS');
|
||||
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 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 = parseCrashLog(log, 'Android', 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);
|
||||
});
|
||||
test('test the parsing of the Android crash log for the unknown crash format and no date', () => {
|
||||
const log = 'Blaa Blaa Blaa';
|
||||
const crash = parseCrashLog(log, 'Android');
|
||||
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 = parseCrashLog(log, 'Android');
|
||||
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 = parseCrashLog(log, 'iOS');
|
||||
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 BaseDevice('serial', 'emulator', 'test device');
|
||||
const pluginKey = getPluginKey(null, device, 'CrashReporter');
|
||||
expect(pluginKey).toEqual('serial#CrashReporter');
|
||||
});
|
||||
test('test the getter of pluginKey with undefined input', () => {
|
||||
const pluginKey = getPluginKey(null, undefined, 'CrashReporter');
|
||||
expect(pluginKey).toEqual('unknown#CrashReporter');
|
||||
});
|
||||
test('test the getter of pluginKey with defined selected app', () => {
|
||||
const pluginKey = getPluginKey('selectedApp', undefined, 'CrashReporter');
|
||||
expect(pluginKey).toEqual('selectedApp#CrashReporter');
|
||||
});
|
||||
test('test the getter of pluginKey with defined selected app and defined base device', () => {
|
||||
const device = new BaseDevice('serial', 'emulator', 'test device');
|
||||
const pluginKey = getPluginKey('selectedApp', device, 'CrashReporter');
|
||||
expect(pluginKey).toEqual('selectedApp#CrashReporter');
|
||||
});
|
||||
test('test defaultPersistedState of CrashReporterPlugin', () => {
|
||||
expect(CrashReporterPlugin.defaultPersistedState).toEqual({crashes: []});
|
||||
});
|
||||
test('test helper setdefaultPersistedState function', () => {
|
||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
||||
setDefaultPersistedState({crashes: [crash]});
|
||||
expect(CrashReporterPlugin.defaultPersistedState).toEqual({crashes: [crash]});
|
||||
});
|
||||
test('test getPersistedState for non-empty defaultPersistedState and undefined pluginState', () => {
|
||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
||||
setDefaultPersistedState({crashes: [crash]});
|
||||
const pluginStates = {};
|
||||
const perisistedState = getPersistedState(
|
||||
getPluginKey(null, null, CrashReporterPlugin.id),
|
||||
CrashReporterPlugin,
|
||||
pluginStates,
|
||||
);
|
||||
expect(perisistedState).toEqual({crashes: [crash]});
|
||||
});
|
||||
test('test getPersistedState for non-empty defaultPersistedState and defined pluginState', () => {
|
||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
||||
const pluginKey = getPluginKey(null, null, CrashReporterPlugin.id);
|
||||
setDefaultPersistedState({crashes: [crash]});
|
||||
const pluginStateCrash = getCrash(1, 'callstack', 'crash1', 'crash1');
|
||||
const pluginStates = {'unknown#CrashReporter': {crashes: [pluginStateCrash]}};
|
||||
const perisistedState = getPersistedState(
|
||||
pluginKey,
|
||||
CrashReporterPlugin,
|
||||
pluginStates,
|
||||
);
|
||||
expect(perisistedState).toEqual({crashes: [pluginStateCrash]});
|
||||
});
|
||||
test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState', () => {
|
||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
||||
const pluginKey = getPluginKey(null, null, CrashReporterPlugin.id);
|
||||
setDefaultPersistedState({crashes: [crash]});
|
||||
const pluginStateCrash = getCrash(1, 'callstack', 'crash1', 'crash1');
|
||||
const pluginStates = {'unknown#CrashReporter': {crashes: [pluginStateCrash]}};
|
||||
const perisistedState = getPersistedState(
|
||||
pluginKey,
|
||||
CrashReporterPlugin,
|
||||
pluginStates,
|
||||
);
|
||||
const content =
|
||||
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa';
|
||||
expect(perisistedState).toBeDefined();
|
||||
const {crashes} = perisistedState;
|
||||
expect(crashes).toBeDefined();
|
||||
expect(crashes.length).toEqual(1);
|
||||
expect(crashes[0]).toEqual(pluginStateCrash);
|
||||
const newPersistedState = getNewPersisitedStateFromCrashLog(
|
||||
perisistedState,
|
||||
CrashReporterPlugin,
|
||||
content,
|
||||
'iOS',
|
||||
);
|
||||
expect(newPersistedState).toBeDefined();
|
||||
// $FlowFixMe: Checked if perisistedState is defined or not
|
||||
const newPersistedStateCrashes = newPersistedState.crashes;
|
||||
expect(newPersistedStateCrashes).toBeDefined();
|
||||
expect(newPersistedStateCrashes.length).toEqual(2);
|
||||
assertCrash(newPersistedStateCrashes[0], pluginStateCrash);
|
||||
assertCrash(
|
||||
newPersistedStateCrashes[1],
|
||||
getCrash(1, content, 'SIGSEGV', 'SIGSEGV'),
|
||||
);
|
||||
});
|
||||
test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState and undefined pluginState', () => {
|
||||
setNotificationID(0);
|
||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
||||
const pluginKey = getPluginKey(null, null, CrashReporterPlugin.id);
|
||||
setDefaultPersistedState({crashes: [crash]});
|
||||
const pluginStates = {};
|
||||
const perisistedState = getPersistedState(
|
||||
pluginKey,
|
||||
CrashReporterPlugin,
|
||||
pluginStates,
|
||||
);
|
||||
const content = 'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV';
|
||||
expect(perisistedState).toEqual({crashes: [crash]});
|
||||
const newPersistedState = getNewPersisitedStateFromCrashLog(
|
||||
perisistedState,
|
||||
CrashReporterPlugin,
|
||||
content,
|
||||
'iOS',
|
||||
);
|
||||
expect(newPersistedState).toBeDefined();
|
||||
// $FlowFixMe: Checked if perisistedState is defined or not
|
||||
const {crashes} = newPersistedState;
|
||||
expect(crashes).toBeDefined();
|
||||
expect(crashes.length).toEqual(2);
|
||||
assertCrash(crashes[0], crash);
|
||||
assertCrash(crashes[1], getCrash(1, content, 'SIGSEGV', 'SIGSEGV'));
|
||||
});
|
||||
test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState and improper crash log', () => {
|
||||
setNotificationID(0);
|
||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
||||
const pluginKey = getPluginKey(null, null, CrashReporterPlugin.id);
|
||||
setDefaultPersistedState({crashes: [crash]});
|
||||
const pluginStateCrash = getCrash(1, 'callstack', 'crash1', 'crash1');
|
||||
const pluginStates = {'unknown#CrashReporter': {crashes: [pluginStateCrash]}};
|
||||
const perisistedState = getPersistedState(
|
||||
pluginKey,
|
||||
CrashReporterPlugin,
|
||||
pluginStates,
|
||||
);
|
||||
const content = 'Blaa Blaaa \n Blaa Blaaa';
|
||||
expect(perisistedState).toEqual({crashes: [pluginStateCrash]});
|
||||
const newPersistedState = getNewPersisitedStateFromCrashLog(
|
||||
perisistedState,
|
||||
CrashReporterPlugin,
|
||||
content,
|
||||
'iOS',
|
||||
);
|
||||
expect(newPersistedState).toBeDefined();
|
||||
// $FlowFixMe: Checked if perisistedState is defined or not
|
||||
const {crashes} = newPersistedState;
|
||||
expect(crashes).toBeDefined();
|
||||
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 getNewPersisitedStateFromCrashLog when os is undefined', () => {
|
||||
setNotificationID(0);
|
||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
||||
const pluginKey = getPluginKey(null, null, CrashReporterPlugin.id);
|
||||
setDefaultPersistedState({crashes: [crash]});
|
||||
const pluginStateCrash = getCrash(1, 'callstack', 'crash1', 'crash1');
|
||||
const pluginStates = {'unknown#CrashReporter': {crashes: [pluginStateCrash]}};
|
||||
const perisistedState = getPersistedState(
|
||||
pluginKey,
|
||||
CrashReporterPlugin,
|
||||
pluginStates,
|
||||
);
|
||||
const content = 'Blaa Blaaa \n Blaa Blaaa';
|
||||
const newPersistedState = getNewPersisitedStateFromCrashLog(
|
||||
perisistedState,
|
||||
CrashReporterPlugin,
|
||||
content,
|
||||
);
|
||||
expect(newPersistedState).toEqual(null);
|
||||
});
|
||||
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 BaseDevice('TH1S-15DEV1CE-1D', 'emulator', 'test device');
|
||||
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 = shouldShowCrashNotification(device, content);
|
||||
expect(shouldShowNotification).toEqual(true);
|
||||
});
|
||||
test('test shouldShowCrashNotification function for all correct inputs but incorrect id', () => {
|
||||
const device = new BaseDevice('TH1S-15DEV1CE-1D', 'emulator', 'test 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 = shouldShowCrashNotification(device, content);
|
||||
expect(shouldShowNotification).toEqual(false);
|
||||
});
|
||||
test('test shouldShowCrashNotification 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 = shouldShowCrashNotification(null, content);
|
||||
expect(shouldShowNotification).toEqual(false);
|
||||
});
|
||||
800
desktop/plugins/crash_reporter/index.js
Normal file
800
desktop/plugins/crash_reporter/index.js
Normal file
@@ -0,0 +1,800 @@
|
||||
/**
|
||||
* 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
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
FlipperDevicePlugin,
|
||||
Device,
|
||||
View,
|
||||
styled,
|
||||
FlexColumn,
|
||||
FlexRow,
|
||||
ContextMenu,
|
||||
clipboard,
|
||||
Button,
|
||||
FlipperPlugin,
|
||||
getPluginKey,
|
||||
getPersistedState,
|
||||
BaseDevice,
|
||||
shouldParseAndroidLog,
|
||||
Text,
|
||||
colors,
|
||||
Toolbar,
|
||||
Spacer,
|
||||
Select,
|
||||
} from 'flipper';
|
||||
import unicodeSubstring from 'unicode-substring';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import util from 'util';
|
||||
import path from 'path';
|
||||
import {promisify} from 'util';
|
||||
import type {Notification} from '../../src/plugin.tsx';
|
||||
import type {Store, DeviceLogEntry, OS, Props} from 'flipper';
|
||||
import {Component} from 'react';
|
||||
|
||||
type HeaderRowProps = {
|
||||
title: string,
|
||||
value: string,
|
||||
};
|
||||
type openLogsCallbackType = () => void;
|
||||
|
||||
type CrashReporterBarProps = {|
|
||||
openLogsCallback?: openLogsCallbackType,
|
||||
crashSelector: CrashSelectorProps,
|
||||
|};
|
||||
|
||||
type CrashSelectorProps = {|
|
||||
crashes: ?{[key: string]: string},
|
||||
orderedIDs: ?Array<string>,
|
||||
selectedCrashID: ?string,
|
||||
onCrashChange: ?(string) => void,
|
||||
|};
|
||||
|
||||
export type Crash = {|
|
||||
notificationID: string,
|
||||
callstack: ?string,
|
||||
reason: string,
|
||||
name: string,
|
||||
date: Date,
|
||||
|};
|
||||
|
||||
export type CrashLog = {|
|
||||
callstack: string,
|
||||
reason: string,
|
||||
name: string,
|
||||
date: ?Date,
|
||||
|};
|
||||
|
||||
export type PersistedState = {
|
||||
crashes: Array<Crash>,
|
||||
};
|
||||
|
||||
type State = {
|
||||
crash: ?Crash,
|
||||
};
|
||||
|
||||
const Padder = styled.div(
|
||||
({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({
|
||||
paddingLeft: paddingLeft || 0,
|
||||
paddingRight: paddingRight || 0,
|
||||
paddingBottom: paddingBottom || 0,
|
||||
paddingTop: paddingTop || 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const Title = styled(Text)({
|
||||
fontWeight: 'bold',
|
||||
color: colors.greyTint3,
|
||||
height: 'auto',
|
||||
width: 200,
|
||||
textOverflow: 'ellipsis',
|
||||
});
|
||||
|
||||
const Line = styled(View)({
|
||||
backgroundColor: colors.greyTint2,
|
||||
height: 1,
|
||||
width: 'auto',
|
||||
marginTop: 2,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
const Container = styled(FlexColumn)({
|
||||
overflow: 'auto',
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
const Value = styled(Text)({
|
||||
fontWeight: 'bold',
|
||||
color: colors.greyTint3,
|
||||
height: 'auto',
|
||||
maxHeight: 200,
|
||||
flexGrow: 1,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'normal',
|
||||
wordWrap: 'break-word',
|
||||
lineHeight: 2,
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
const FlexGrowColumn = styled(FlexColumn)({
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
const PluginRootContainer = styled(FlexColumn)({
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const ScrollableColumn = styled(FlexGrowColumn)({
|
||||
overflow: 'auto',
|
||||
height: 'auto',
|
||||
});
|
||||
|
||||
const StyledFlexGrowColumn = styled(FlexColumn)({
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
const StyledFlexRowColumn = styled(FlexRow)({
|
||||
aligItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const StyledFlexColumn = styled(StyledFlexGrowColumn)({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const MatchParentHeightComponent = styled(FlexRow)({
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const ButtonGroupContainer = styled(FlexRow)({
|
||||
paddingLeft: 4,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const StyledSelectContainer = styled(FlexRow)({
|
||||
paddingLeft: 8,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const StyledSelect = styled(Select)({
|
||||
height: '100%',
|
||||
maxWidth: 200,
|
||||
});
|
||||
|
||||
const StackTraceContainer = styled(FlexColumn)({
|
||||
backgroundColor: colors.greyStackTraceTint,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
const UNKNOWN_CRASH_REASON = 'Cannot figure out the cause';
|
||||
|
||||
export function getNewPersisitedStateFromCrashLog(
|
||||
persistedState: ?PersistedState,
|
||||
persistingPlugin: Class<FlipperDevicePlugin<> | FlipperPlugin<>>,
|
||||
content: string,
|
||||
os: ?OS,
|
||||
logDate: ?Date,
|
||||
): ?PersistedState {
|
||||
const persistedStateReducer = persistingPlugin.persistedStateReducer;
|
||||
if (!os || !persistedStateReducer) {
|
||||
return null;
|
||||
}
|
||||
const crash = parseCrashLog(content, os, logDate);
|
||||
const newPluginState = persistedStateReducer(
|
||||
persistedState,
|
||||
'crash-report',
|
||||
crash,
|
||||
);
|
||||
return newPluginState;
|
||||
}
|
||||
|
||||
export function parseCrashLogAndUpdateState(
|
||||
store: Store,
|
||||
content: string,
|
||||
setPersistedState: (
|
||||
pluginKey: string,
|
||||
newPluginState: ?PersistedState,
|
||||
) => void,
|
||||
logDate: ?Date,
|
||||
) {
|
||||
const os = store.getState().connections.selectedDevice?.os;
|
||||
if (
|
||||
!shouldShowCrashNotification(
|
||||
store.getState().connections.selectedDevice,
|
||||
content,
|
||||
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,
|
||||
os,
|
||||
logDate,
|
||||
);
|
||||
setPersistedState(pluginKey, newPluginState);
|
||||
}
|
||||
|
||||
export function shouldShowCrashNotification(
|
||||
baseDevice: ?BaseDevice,
|
||||
content: string,
|
||||
os: ?OS,
|
||||
): boolean {
|
||||
if (os && os === '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,
|
||||
os: OS,
|
||||
logDate: ?Date,
|
||||
): CrashLog {
|
||||
const fallbackReason = UNKNOWN_CRASH_REASON;
|
||||
switch (os) {
|
||||
case 'iOS': {
|
||||
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] : fallbackReason;
|
||||
|
||||
let date = logDate;
|
||||
if (!date) {
|
||||
const dateRegex = /Date\/Time: *[\w\s\.:-]*/;
|
||||
const dateArr = dateRegex.exec(content);
|
||||
const dateString = dateArr ? dateArr[0] : '';
|
||||
const dateRegex2 = /[\w\s\.:-]*$/;
|
||||
const tmp1 = dateRegex2.exec(dateString);
|
||||
const extractedDateString: ?string =
|
||||
tmp1 && tmp1[0].length ? tmp1[0] : null;
|
||||
date = extractedDateString ? new Date(extractedDateString) : logDate;
|
||||
}
|
||||
|
||||
const crash = {
|
||||
callstack: content,
|
||||
name: exception,
|
||||
reason: exception,
|
||||
date,
|
||||
};
|
||||
return crash;
|
||||
}
|
||||
case 'Android': {
|
||||
const regForName = /.*\n/;
|
||||
const nameRegArr = regForName.exec(content);
|
||||
let name = nameRegArr ? nameRegArr[0] : fallbackReason;
|
||||
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 reason =
|
||||
remainingString.length > 0
|
||||
? remainingString.split('\n').pop()
|
||||
: fallbackReason;
|
||||
if (name[name.length - 1] === '\n') {
|
||||
name = name.slice(0, -1);
|
||||
}
|
||||
const crash = {
|
||||
callstack: content,
|
||||
name: name,
|
||||
reason: reason,
|
||||
date: logDate,
|
||||
};
|
||||
return crash;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unsupported OS');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function truncate(baseString: string, numOfChars: number): string {
|
||||
if (baseString.length <= numOfChars) {
|
||||
return baseString;
|
||||
}
|
||||
const truncated_string = unicodeSubstring(baseString, 0, numOfChars - 1);
|
||||
return truncated_string + '\u2026';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const filepath = path.join(dir, filename);
|
||||
promisify(fs.exists)(filepath).then(exists => {
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
fs.readFile(filepath, '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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class CrashSelector extends Component<CrashSelectorProps> {
|
||||
render() {
|
||||
const {crashes, selectedCrashID, orderedIDs, onCrashChange} = this.props;
|
||||
return (
|
||||
<StyledFlexRowColumn>
|
||||
<ButtonGroupContainer>
|
||||
<MatchParentHeightComponent>
|
||||
<Button
|
||||
disabled={Boolean(!orderedIDs || orderedIDs.length <= 1)}
|
||||
compact={true}
|
||||
onClick={() => {
|
||||
if (onCrashChange && orderedIDs) {
|
||||
const index = orderedIDs.indexOf(selectedCrashID);
|
||||
const nextIndex =
|
||||
index < 1 ? orderedIDs.length - 1 : index - 1;
|
||||
const nextID = orderedIDs[nextIndex];
|
||||
onCrashChange(nextID);
|
||||
}
|
||||
}}
|
||||
icon="chevron-left"
|
||||
iconSize={12}
|
||||
title="Previous Crash"
|
||||
/>
|
||||
</MatchParentHeightComponent>
|
||||
<MatchParentHeightComponent>
|
||||
<Button
|
||||
disabled={Boolean(!orderedIDs || orderedIDs.length <= 1)}
|
||||
compact={true}
|
||||
onClick={() => {
|
||||
if (onCrashChange && orderedIDs) {
|
||||
const index = orderedIDs.indexOf(selectedCrashID);
|
||||
const nextIndex =
|
||||
index >= orderedIDs.length - 1 ? 0 : index + 1;
|
||||
const nextID = orderedIDs[nextIndex];
|
||||
onCrashChange(nextID);
|
||||
}
|
||||
}}
|
||||
icon="chevron-right"
|
||||
iconSize={12}
|
||||
title="Next Crash"
|
||||
/>
|
||||
</MatchParentHeightComponent>
|
||||
</ButtonGroupContainer>
|
||||
<StyledSelectContainer>
|
||||
<StyledSelect
|
||||
grow={true}
|
||||
selected={selectedCrashID || 'NoCrashID'}
|
||||
options={crashes || {NoCrashID: 'No Crash'}}
|
||||
onChangeWithKey={(key: string) => {
|
||||
if (onCrashChange) {
|
||||
onCrashChange(key);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StyledSelectContainer>
|
||||
</StyledFlexRowColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CrashReporterBar extends Component<CrashReporterBarProps> {
|
||||
render() {
|
||||
const {openLogsCallback, crashSelector} = this.props;
|
||||
return (
|
||||
<Toolbar>
|
||||
<CrashSelector {...crashSelector} />
|
||||
<Spacer />
|
||||
<Button
|
||||
disabled={Boolean(!openLogsCallback)}
|
||||
onClick={openLogsCallback}>
|
||||
Open In Logs
|
||||
</Button>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderRow extends Component<HeaderRowProps> {
|
||||
render() {
|
||||
const {title, value} = this.props;
|
||||
return (
|
||||
<Padder paddingTop={8} paddingBottom={2} paddingLeft={8}>
|
||||
<Container>
|
||||
<FlexRow>
|
||||
<Title>{title}</Title>
|
||||
<ContextMenu
|
||||
items={[
|
||||
{
|
||||
label: 'copy',
|
||||
click: () => {
|
||||
clipboard.writeText(value);
|
||||
},
|
||||
},
|
||||
]}>
|
||||
<Value code={true}>{value}</Value>
|
||||
</ContextMenu>
|
||||
</FlexRow>
|
||||
<Line />
|
||||
</Container>
|
||||
</Padder>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type StackTraceComponentProps = {
|
||||
stacktrace: string,
|
||||
};
|
||||
|
||||
class StackTraceComponent extends Component<StackTraceComponentProps> {
|
||||
render() {
|
||||
const {stacktrace} = this.props;
|
||||
return (
|
||||
<StackTraceContainer>
|
||||
<Padder paddingTop={8} paddingBottom={2} paddingLeft={8}>
|
||||
<Value code={true}>{stacktrace}</Value>
|
||||
</Padder>
|
||||
<Line />
|
||||
</StackTraceContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class CrashReporterPlugin extends FlipperDevicePlugin<
|
||||
State,
|
||||
void,
|
||||
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' || method === 'flipper-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,
|
||||
date: payload.date || new Date(),
|
||||
},
|
||||
]),
|
||||
};
|
||||
return mergedState;
|
||||
}
|
||||
return persistedState;
|
||||
};
|
||||
|
||||
static trimCallStackIfPossible = (callstack: string): string => {
|
||||
const 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> => {
|
||||
const filteredCrashes = persistedState.crashes.filter(crash => {
|
||||
const ignore = !crash.name && !crash.reason;
|
||||
const unknownCrashCause = crash.reason === UNKNOWN_CRASH_REASON;
|
||||
if (ignore || unknownCrashCause) {
|
||||
console.error('Ignored the notification for the crash', crash);
|
||||
}
|
||||
return !ignore && !unknownCrashCause;
|
||||
});
|
||||
return filteredCrashes.map((crash: Crash) => {
|
||||
const id = crash.notificationID;
|
||||
const name: string = crash.name || crash.reason;
|
||||
let title: string = 'CRASH: ' + truncate(name, 50);
|
||||
title = `${
|
||||
name == crash.reason
|
||||
? title
|
||||
: title + 'Reason: ' + truncate(crash.reason, 50)
|
||||
}`;
|
||||
const callstack = crash.callstack
|
||||
? CrashReporterPlugin.trimCallStackIfPossible(crash.callstack)
|
||||
: 'No callstack available';
|
||||
const msg = `Callstack: ${truncate(callstack, 200)}`;
|
||||
return {
|
||||
id,
|
||||
message: msg,
|
||||
severity: 'error',
|
||||
title: title,
|
||||
action: id,
|
||||
category: crash.reason || 'Unknown reason',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* 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,
|
||||
) {
|
||||
let androidLog: string = '';
|
||||
let androidLogUnderProcess = false;
|
||||
let timer = null;
|
||||
baseDevice.addLogListener((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) {
|
||||
parseCrashLogAndUpdateState(
|
||||
store,
|
||||
androidLog,
|
||||
setPersistedState,
|
||||
entry.date,
|
||||
);
|
||||
}
|
||||
androidLogUnderProcess = false;
|
||||
androidLog = '';
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
})(store, referenceDate, setPersistedState);
|
||||
}
|
||||
};
|
||||
openInLogs = (callstack: string) => {
|
||||
this.props.selectPlugin('DeviceLogs', callstack);
|
||||
};
|
||||
|
||||
constructor(props: Props<PersistedState>) {
|
||||
// Required step: always call the parent class' constructor
|
||||
super(props);
|
||||
let crash: ?Crash = null;
|
||||
if (
|
||||
this.props.persistedState.crashes &&
|
||||
this.props.persistedState.crashes.length > 0
|
||||
) {
|
||||
crash = this.props.persistedState.crashes[
|
||||
this.props.persistedState.crashes.length - 1
|
||||
];
|
||||
}
|
||||
|
||||
let deeplinkedCrash = 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) {
|
||||
deeplinkedCrash = this.props.persistedState.crashes[index];
|
||||
}
|
||||
}
|
||||
// Set the state directly. Use props if necessary.
|
||||
this.state = {
|
||||
crash: deeplinkedCrash || crash,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let crashToBeInspected = this.state.crash;
|
||||
|
||||
if (!crashToBeInspected && this.props.persistedState.crashes.length > 0) {
|
||||
crashToBeInspected = this.props.persistedState.crashes[
|
||||
this.props.persistedState.crashes.length - 1
|
||||
];
|
||||
}
|
||||
const crash = crashToBeInspected;
|
||||
if (crash) {
|
||||
const {crashes} = this.props.persistedState;
|
||||
const crashMap = crashes.reduce(
|
||||
(acc: {[key: string]: string}, persistedCrash: Crash) => {
|
||||
const {notificationID, date} = persistedCrash;
|
||||
const name = 'Crash at ' + date.toLocaleString();
|
||||
acc[notificationID] = name;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const orderedIDs = crashes.map(
|
||||
persistedCrash => persistedCrash.notificationID,
|
||||
);
|
||||
const selectedCrashID = crash.notificationID;
|
||||
const onCrashChange = id => {
|
||||
const newSelectedCrash = crashes.find(
|
||||
element => element.notificationID === id,
|
||||
);
|
||||
this.setState({crash: newSelectedCrash});
|
||||
};
|
||||
|
||||
const callstackString = crash.callstack || '';
|
||||
const children = callstackString.split('\n').map(str => {
|
||||
return {message: str};
|
||||
});
|
||||
const crashSelector: CrashSelectorProps = {
|
||||
crashes: crashMap,
|
||||
orderedIDs,
|
||||
selectedCrashID,
|
||||
onCrashChange,
|
||||
};
|
||||
const showReason = crash.reason !== UNKNOWN_CRASH_REASON;
|
||||
return (
|
||||
<PluginRootContainer>
|
||||
{this.device.os == 'Android' ? (
|
||||
<CrashReporterBar
|
||||
crashSelector={crashSelector}
|
||||
openLogsCallback={() => {
|
||||
if (crash.callstack) {
|
||||
this.openInLogs(crash.callstack);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CrashReporterBar crashSelector={crashSelector} />
|
||||
)}
|
||||
<ScrollableColumn>
|
||||
<HeaderRow title="Name" value={crash.name} />
|
||||
{showReason ? (
|
||||
<HeaderRow title="Reason" value={crash.reason} />
|
||||
) : null}
|
||||
<Padder paddingLeft={8} paddingTop={4} paddingBottom={2}>
|
||||
<Title> Stacktrace </Title>
|
||||
</Padder>
|
||||
<ContextMenu
|
||||
items={[
|
||||
{
|
||||
label: 'copy',
|
||||
click: () => {
|
||||
clipboard.writeText(callstackString);
|
||||
},
|
||||
},
|
||||
]}>
|
||||
<Line />
|
||||
{children.map(child => {
|
||||
return (
|
||||
<StackTraceComponent
|
||||
key={child.message}
|
||||
stacktrace={child.message}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ContextMenu>
|
||||
</ScrollableColumn>
|
||||
</PluginRootContainer>
|
||||
);
|
||||
}
|
||||
const crashSelector = {
|
||||
crashes: null,
|
||||
orderedIDs: null,
|
||||
selectedCrashID: null,
|
||||
onCrashChange: null,
|
||||
};
|
||||
return (
|
||||
<StyledFlexGrowColumn>
|
||||
<CrashReporterBar crashSelector={crashSelector} />
|
||||
<StyledFlexColumn>
|
||||
<Padder paddingBottom={8}>
|
||||
<Title>No Crashes Logged</Title>
|
||||
</Padder>
|
||||
</StyledFlexColumn>
|
||||
</StyledFlexGrowColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
desktop/plugins/crash_reporter/package.json
Normal file
17
desktop/plugins/crash_reporter/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "CrashReporter",
|
||||
"version": "0.1.0",
|
||||
"description": "A plugin which will display a crash",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/facebook/flipper",
|
||||
"license": "MIT",
|
||||
"keywords": ["flipper-plugin"],
|
||||
"title": "Crash Reporter",
|
||||
"bugs": {
|
||||
"email": "prit91@fb.com",
|
||||
"url": "https://fb.workplace.com/groups/220760072184928/"
|
||||
},
|
||||
"dependencies": {
|
||||
"unicode-substring": "^1.0.0"
|
||||
}
|
||||
}
|
||||
8
desktop/plugins/crash_reporter/yarn.lock
Normal file
8
desktop/plugins/crash_reporter/yarn.lock
Normal file
@@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
unicode-substring@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unicode-substring/-/unicode-substring-1.0.0.tgz#659fb839078e7bee84b86c27210ac4db215bf885"
|
||||
integrity sha512-2acGIOTaqS/GWocwKdyL1Vk9MHglCss1mR0CL2o/YJTwKrAt6JbTrw4X187VkSDmFcpJ8n2i3/+gJSYEdvXJMg==
|
||||
Reference in New Issue
Block a user