Summary: This diff makes sure that devices will actually instantiate applicable sandy device plugins. Similar to how client plugins are owned by Client, device plugins are directly owned by BaseDevice, which significantly simplifies life cycle management and doesn't dispatch updates to all Redux connect components whenever something irrelevant changes. Also made sure `device.teardown()` is called. That API already existed, but wasn't used or implemented before. Updated Flipper test utils to support testing device plugins as well (both Sandy and classic ones) Reviewed By: passy, nikoant Differential Revision: D22693929 fbshipit-source-id: 73b2b8666ef7a0e748ea89360db84734d37eb5be
823 lines
22 KiB
TypeScript
823 lines
22 KiB
TypeScript
/**
|
|
* 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 {
|
|
FlipperBasePlugin,
|
|
FlipperDevicePlugin,
|
|
Device,
|
|
View,
|
|
styled,
|
|
FlexColumn,
|
|
FlexRow,
|
|
ContextMenu,
|
|
clipboard,
|
|
Button,
|
|
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 'flipper';
|
|
import type {Store, DeviceLogEntry, OS, Props} from 'flipper';
|
|
import React from 'react';
|
|
import {Component} from 'react';
|
|
|
|
type Maybe<T> = T | null | undefined;
|
|
|
|
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: (name: Maybe<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: Maybe<Date>;
|
|
};
|
|
|
|
export type PersistedState = {
|
|
crashes: Array<Crash>;
|
|
};
|
|
|
|
type State = {
|
|
crash?: Crash;
|
|
};
|
|
|
|
const Padder = styled.div<{
|
|
paddingLeft?: number;
|
|
paddingRight?: number;
|
|
paddingBottom?: number;
|
|
paddingTop?: number;
|
|
}>(({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 getNewPersistedStateFromCrashLog(
|
|
persistedState: Maybe<PersistedState>,
|
|
persistingPlugin: typeof FlipperBasePlugin,
|
|
content: string,
|
|
os: Maybe<OS>,
|
|
logDate: Maybe<Date>,
|
|
): Maybe<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: Maybe<PersistedState>,
|
|
) => void,
|
|
logDate: Maybe<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:
|
|
| typeof FlipperBasePlugin
|
|
| undefined = store
|
|
.getState()
|
|
.plugins.devicePlugins.get(CrashReporterPlugin.id) as any;
|
|
if (!persistingPlugin) {
|
|
return;
|
|
}
|
|
if (!persistingPlugin.persistedStateReducer) {
|
|
console.error('CrashReporterPlugin is incompatible');
|
|
return;
|
|
}
|
|
const pluginStates = store.getState().pluginStates;
|
|
const persistedState = getPersistedState(
|
|
pluginKey,
|
|
persistingPlugin,
|
|
pluginStates,
|
|
);
|
|
if (!persistedState) {
|
|
return;
|
|
}
|
|
const newPluginState = getNewPersistedStateFromCrashLog(
|
|
persistedState as PersistedState,
|
|
persistingPlugin,
|
|
content,
|
|
os,
|
|
logDate,
|
|
);
|
|
setPersistedState(pluginKey, newPluginState);
|
|
}
|
|
|
|
export function shouldShowCrashNotification(
|
|
baseDevice: Maybe<BaseDevice>,
|
|
content: string,
|
|
os: Maybe<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: Maybe<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: Maybe<string> =
|
|
tmp1 && tmp1[0].length ? tmp1[0] : null;
|
|
date = extractedDateString ? new Date(extractedDateString) : logDate;
|
|
}
|
|
|
|
const crash: CrashLog = {
|
|
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 reasonText =
|
|
remainingString.length > 0
|
|
? remainingString.split('\n').pop()
|
|
: fallbackReason;
|
|
const reason = reasonText ? reasonText : fallbackReason;
|
|
if (name[name.length - 1] === '\n') {
|
|
name = name.slice(0, -1);
|
|
}
|
|
const crash: CrashLog = {
|
|
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): Maybe<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: Maybe<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,
|
|
null,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
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 && selectedCrashID) {
|
|
const index = orderedIDs.indexOf(selectedCrashID as string);
|
|
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 && selectedCrashID) {
|
|
const index = orderedIDs.indexOf(selectedCrashID as string);
|
|
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,
|
|
any,
|
|
PersistedState
|
|
> {
|
|
static defaultPersistedState: PersistedState = {
|
|
crashes: [],
|
|
};
|
|
|
|
static supportsDevice(device: Device) {
|
|
return (
|
|
(device.os === 'iOS' && device.deviceType !== 'physical') ||
|
|
device.os === 'Android'
|
|
);
|
|
}
|
|
|
|
static notificationID: number = 0;
|
|
/*
|
|
* Reducer to process incoming "send" messages from the mobile counterpart.
|
|
*/
|
|
static persistedStateReducer = (
|
|
persistedState: PersistedState,
|
|
method: string,
|
|
payload: CrashLog | Crash,
|
|
): 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: Maybe<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: Maybe<PersistedState>,
|
|
) => void,
|
|
) {
|
|
let androidLog: string = '';
|
|
let androidLogUnderProcess = false;
|
|
let timer: Maybe<NodeJS.Timeout> = 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 | undefined = undefined;
|
|
if (
|
|
this.props.persistedState.crashes &&
|
|
this.props.persistedState.crashes.length > 0
|
|
) {
|
|
crash = this.props.persistedState.crashes[
|
|
this.props.persistedState.crashes.length - 1
|
|
];
|
|
}
|
|
|
|
let deeplinkedCrash: Crash | undefined = undefined;
|
|
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: Maybe<string>) => {
|
|
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: undefined,
|
|
orderedIDs: undefined,
|
|
selectedCrashID: undefined,
|
|
onCrashChange: () => void {},
|
|
};
|
|
return (
|
|
<StyledFlexGrowColumn>
|
|
<CrashReporterBar crashSelector={crashSelector} />
|
|
<StyledFlexColumn>
|
|
<Padder paddingBottom={8}>
|
|
<Title>No Crashes Logged</Title>
|
|
</Padder>
|
|
</StyledFlexColumn>
|
|
</StyledFlexGrowColumn>
|
|
);
|
|
}
|
|
}
|