Move crash reporting listener to the server
Summary: Changelog: Move crash watcher to the server. Add 'device-crash' event. Add 'device-start-crash-watcher', 'device-stop-crash-watcher' commands. Add 'onDeviceCrash' method to Plugin Client. Reviewed By: mweststrate Differential Revision: D33089810 fbshipit-source-id: ed62ee7c1129e5e25af18b444744b0796f567b72
This commit is contained in:
committed by
Facebook GitHub Bot
parent
9fd45b96d2
commit
731749b41f
@@ -258,8 +258,6 @@ module.exports = {
|
|||||||
// TODO: Remove specific plugin overrides down below
|
// TODO: Remove specific plugin overrides down below
|
||||||
'plugins/fb/graphql/data/getQueryFromQueryId.tsx',
|
'plugins/fb/graphql/data/getQueryFromQueryId.tsx',
|
||||||
'plugins/fb/kaios-portal/kaios-debugger-client/client.tsx',
|
'plugins/fb/kaios-portal/kaios-debugger-client/client.tsx',
|
||||||
'plugins/public/crash_reporter/index.tsx',
|
|
||||||
'plugins/public/crash_reporter/ios-crash-utils.tsx',
|
|
||||||
'plugins/public/reactdevtools/index.tsx',
|
'plugins/public/reactdevtools/index.tsx',
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ export type FlipperServerEvents = {
|
|||||||
serial: string;
|
serial: string;
|
||||||
entry: DeviceLogEntry;
|
entry: DeviceLogEntry;
|
||||||
};
|
};
|
||||||
|
'device-crash': {
|
||||||
|
serial: string;
|
||||||
|
crash: CrashLog;
|
||||||
|
};
|
||||||
'client-setup': UninitializedClient;
|
'client-setup': UninitializedClient;
|
||||||
'client-connected': ClientDescription;
|
'client-connected': ClientDescription;
|
||||||
'client-disconnected': {id: string};
|
'client-disconnected': {id: string};
|
||||||
@@ -196,6 +200,8 @@ export type FlipperServerCommands = {
|
|||||||
'device-list': () => Promise<DeviceDescription[]>;
|
'device-list': () => Promise<DeviceDescription[]>;
|
||||||
'device-start-logging': (serial: string) => Promise<void>;
|
'device-start-logging': (serial: string) => Promise<void>;
|
||||||
'device-stop-logging': (serial: string) => Promise<void>;
|
'device-stop-logging': (serial: string) => Promise<void>;
|
||||||
|
'device-start-crash-watcher': (serial: string) => Promise<void>;
|
||||||
|
'device-stop-crash-watcher': (serial: string) => Promise<void>;
|
||||||
'device-supports-screenshot': (serial: string) => Promise<boolean>;
|
'device-supports-screenshot': (serial: string) => Promise<boolean>;
|
||||||
'device-supports-screencapture': (serial: string) => Promise<boolean>;
|
'device-supports-screencapture': (serial: string) => Promise<boolean>;
|
||||||
'device-take-screenshot': (serial: string) => Promise<string>; // base64 encoded buffer
|
'device-take-screenshot': (serial: string) => Promise<string>; // base64 encoded buffer
|
||||||
@@ -571,3 +577,10 @@ export type ResponseMessage =
|
|||||||
success?: never;
|
success?: never;
|
||||||
error: ClientErrorType;
|
error: ClientErrorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CrashLog = {
|
||||||
|
callstack?: string;
|
||||||
|
reason: string;
|
||||||
|
name: string;
|
||||||
|
date?: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ test('Correct top level API exposed', () => {
|
|||||||
expect(exposedTypes.sort()).toMatchInlineSnapshot(`
|
expect(exposedTypes.sort()).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
"Atom",
|
"Atom",
|
||||||
|
"CrashLog",
|
||||||
|
"CrashLogListener",
|
||||||
"DataDescriptionType",
|
"DataDescriptionType",
|
||||||
"DataInspectorExpanded",
|
"DataInspectorExpanded",
|
||||||
"DataTableColumn",
|
"DataTableColumn",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export {
|
|||||||
Device,
|
Device,
|
||||||
DeviceLogListener,
|
DeviceLogListener,
|
||||||
DevicePluginClient,
|
DevicePluginClient,
|
||||||
|
CrashLogListener,
|
||||||
SandyDevicePluginInstance as _SandyDevicePluginInstance,
|
SandyDevicePluginInstance as _SandyDevicePluginInstance,
|
||||||
} from './plugin/DevicePlugin';
|
} from './plugin/DevicePlugin';
|
||||||
export {SandyPluginDefinition as _SandyPluginDefinition} from './plugin/SandyPluginDefinition';
|
export {SandyPluginDefinition as _SandyPluginDefinition} from './plugin/SandyPluginDefinition';
|
||||||
@@ -148,4 +149,5 @@ export {
|
|||||||
DeviceLogEntry,
|
DeviceLogEntry,
|
||||||
DeviceLogLevel,
|
DeviceLogLevel,
|
||||||
Logger,
|
Logger,
|
||||||
|
CrashLog,
|
||||||
} from 'flipper-common';
|
} from 'flipper-common';
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import {SandyPluginDefinition} from './SandyPluginDefinition';
|
|||||||
import {BasePluginInstance, BasePluginClient} from './PluginBase';
|
import {BasePluginInstance, BasePluginClient} from './PluginBase';
|
||||||
import {FlipperLib} from './FlipperLib';
|
import {FlipperLib} from './FlipperLib';
|
||||||
import {Atom, ReadOnlyAtom} from '../state/atom';
|
import {Atom, ReadOnlyAtom} from '../state/atom';
|
||||||
import {DeviceOS, DeviceType, DeviceLogEntry} from 'flipper-common';
|
import {DeviceOS, DeviceType, DeviceLogEntry, CrashLog} from 'flipper-common';
|
||||||
|
|
||||||
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
|
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
|
||||||
|
export type CrashLogListener = (crash: CrashLog) => void;
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
readonly isArchived: boolean;
|
readonly isArchived: boolean;
|
||||||
@@ -24,7 +25,9 @@ export interface Device {
|
|||||||
readonly connected: Atom<boolean>;
|
readonly connected: Atom<boolean>;
|
||||||
executeShell(command: string): Promise<string>;
|
executeShell(command: string): Promise<string>;
|
||||||
addLogListener(callback: DeviceLogListener): Symbol;
|
addLogListener(callback: DeviceLogListener): Symbol;
|
||||||
|
addCrashListener(callback: CrashLogListener): Symbol;
|
||||||
removeLogListener(id: Symbol): void;
|
removeLogListener(id: Symbol): void;
|
||||||
|
removeCrashListener(id: Symbol): void;
|
||||||
executeShell(command: string): Promise<string>;
|
executeShell(command: string): Promise<string>;
|
||||||
forwardPort(local: string, remote: string): Promise<boolean>;
|
forwardPort(local: string, remote: string): Promise<boolean>;
|
||||||
clearLogs(): Promise<void>;
|
clearLogs(): Promise<void>;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import EventEmitter from 'eventemitter3';
|
|||||||
import {SandyPluginDefinition} from './SandyPluginDefinition';
|
import {SandyPluginDefinition} from './SandyPluginDefinition';
|
||||||
import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
|
import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
|
||||||
import {FlipperLib} from './FlipperLib';
|
import {FlipperLib} from './FlipperLib';
|
||||||
import {Device, DeviceLogListener} from './DevicePlugin';
|
import {CrashLogListener, Device, DeviceLogListener} from './DevicePlugin';
|
||||||
import {batched} from '../state/batch';
|
import {batched} from '../state/batch';
|
||||||
import {Idler} from '../utils/Idler';
|
import {Idler} from '../utils/Idler';
|
||||||
import {Notification} from './Notification';
|
import {Notification} from './Notification';
|
||||||
@@ -84,6 +84,12 @@ export interface BasePluginClient {
|
|||||||
*/
|
*/
|
||||||
onDeviceLogEntry(cb: DeviceLogListener): () => void;
|
onDeviceLogEntry(cb: DeviceLogListener): () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener that is triggered if the underlying device crashes.
|
||||||
|
* Listeners established with this mechanism will automatically be cleaned up during destroy
|
||||||
|
*/
|
||||||
|
onDeviceCrash(cb: CrashLogListener): () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Paste (similar to a Github Gist).
|
* Creates a Paste (similar to a Github Gist).
|
||||||
* Facebook only function. Resolves to undefined if creating a paste failed.
|
* Facebook only function. Resolves to undefined if creating a paste failed.
|
||||||
@@ -186,6 +192,7 @@ export abstract class BasePluginInstance {
|
|||||||
|
|
||||||
menuEntries: NormalizedMenuEntry[] = [];
|
menuEntries: NormalizedMenuEntry[] = [];
|
||||||
logListeners: Symbol[] = [];
|
logListeners: Symbol[] = [];
|
||||||
|
crashListeners: Symbol[] = [];
|
||||||
|
|
||||||
readonly instanceId = ++staticInstanceId;
|
readonly instanceId = ++staticInstanceId;
|
||||||
|
|
||||||
@@ -316,6 +323,13 @@ export abstract class BasePluginInstance {
|
|||||||
this.device.removeLogListener(handle);
|
this.device.removeLogListener(handle);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
onDeviceCrash: (cb: CrashLogListener): (() => void) => {
|
||||||
|
const handle = this.device.addCrashListener(cb);
|
||||||
|
this.crashListeners.push(handle);
|
||||||
|
return () => {
|
||||||
|
this.device.removeCrashListener(handle);
|
||||||
|
};
|
||||||
|
},
|
||||||
writeTextToClipboard: this.flipperLib.writeTextToClipboard,
|
writeTextToClipboard: this.flipperLib.writeTextToClipboard,
|
||||||
createPaste: this.flipperLib.createPaste,
|
createPaste: this.flipperLib.createPaste,
|
||||||
isFB: this.flipperLib.isFB,
|
isFB: this.flipperLib.isFB,
|
||||||
@@ -365,6 +379,9 @@ export abstract class BasePluginInstance {
|
|||||||
this.logListeners.splice(0).forEach((handle) => {
|
this.logListeners.splice(0).forEach((handle) => {
|
||||||
this.device.removeLogListener(handle);
|
this.device.removeLogListener(handle);
|
||||||
});
|
});
|
||||||
|
this.crashListeners.splice(0).forEach((handle) => {
|
||||||
|
this.device.removeCrashListener(handle);
|
||||||
|
});
|
||||||
this.events.emit('destroy');
|
this.events.emit('destroy');
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
SandyDevicePluginInstance,
|
SandyDevicePluginInstance,
|
||||||
Device,
|
Device,
|
||||||
DeviceLogListener,
|
DeviceLogListener,
|
||||||
|
CrashLogListener,
|
||||||
} from '../plugin/DevicePlugin';
|
} from '../plugin/DevicePlugin';
|
||||||
import {BasePluginInstance} from '../plugin/PluginBase';
|
import {BasePluginInstance} from '../plugin/PluginBase';
|
||||||
import {FlipperLib} from '../plugin/FlipperLib';
|
import {FlipperLib} from '../plugin/FlipperLib';
|
||||||
@@ -553,6 +554,7 @@ function createMockDevice(options?: StartPluginOptions): Device & {
|
|||||||
addLogEntry(entry: DeviceLogEntry): void;
|
addLogEntry(entry: DeviceLogEntry): void;
|
||||||
} {
|
} {
|
||||||
const logListeners: (undefined | DeviceLogListener)[] = [];
|
const logListeners: (undefined | DeviceLogListener)[] = [];
|
||||||
|
const crashListeners: (undefined | CrashLogListener)[] = [];
|
||||||
return {
|
return {
|
||||||
os: 'Android',
|
os: 'Android',
|
||||||
deviceType: 'emulator',
|
deviceType: 'emulator',
|
||||||
@@ -566,6 +568,13 @@ function createMockDevice(options?: StartPluginOptions): Device & {
|
|||||||
removeLogListener(idx) {
|
removeLogListener(idx) {
|
||||||
logListeners[idx as any] = undefined;
|
logListeners[idx as any] = undefined;
|
||||||
},
|
},
|
||||||
|
addCrashListener(cb) {
|
||||||
|
crashListeners.push(cb);
|
||||||
|
return (crashListeners.length - 1) as any;
|
||||||
|
},
|
||||||
|
removeCrashListener(idx) {
|
||||||
|
crashListeners[idx as any] = undefined;
|
||||||
|
},
|
||||||
addLogEntry(entry: DeviceLogEntry) {
|
addLogEntry(entry: DeviceLogEntry) {
|
||||||
logListeners.forEach((f) => f?.(entry));
|
logListeners.forEach((f) => f?.(entry));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -289,6 +289,10 @@ export class FlipperServerImpl implements FlipperServer {
|
|||||||
this.getDevice(serial).startLogging(),
|
this.getDevice(serial).startLogging(),
|
||||||
'device-stop-logging': async (serial: string) =>
|
'device-stop-logging': async (serial: string) =>
|
||||||
this.getDevice(serial).stopLogging(),
|
this.getDevice(serial).stopLogging(),
|
||||||
|
'device-start-crash-watcher': async (serial: string) =>
|
||||||
|
this.getDevice(serial).startCrashWatcher(),
|
||||||
|
'device-stop-crash-watcher': async (serial: string) =>
|
||||||
|
this.getDevice(serial).stopCrashWatcher(),
|
||||||
'device-supports-screenshot': async (serial: string) =>
|
'device-supports-screenshot': async (serial: string) =>
|
||||||
this.getDevice(serial).screenshotAvailable(),
|
this.getDevice(serial).screenshotAvailable(),
|
||||||
'device-supports-screencapture': async (serial: string) =>
|
'device-supports-screencapture': async (serial: string) =>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export abstract class ServerDevice {
|
|||||||
readonly flipperServer: FlipperServerImpl;
|
readonly flipperServer: FlipperServerImpl;
|
||||||
connected = true;
|
connected = true;
|
||||||
|
|
||||||
|
protected stopCrashWatcherCb?: () => void;
|
||||||
|
|
||||||
constructor(flipperServer: FlipperServerImpl, info: DeviceDescription) {
|
constructor(flipperServer: FlipperServerImpl, info: DeviceDescription) {
|
||||||
this.flipperServer = flipperServer;
|
this.flipperServer = flipperServer;
|
||||||
this.info = info;
|
this.info = info;
|
||||||
@@ -46,6 +48,20 @@ export abstract class ServerDevice {
|
|||||||
// to be subclassed
|
// to be subclassed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startCrashWatcher() {
|
||||||
|
this.stopCrashWatcherCb = this.startCrashWatcherImpl?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected startCrashWatcherImpl(): () => void {
|
||||||
|
// to be subclassed
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCrashWatcher() {
|
||||||
|
this.stopCrashWatcherCb?.();
|
||||||
|
this.stopCrashWatcherCb = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async screenshotAvailable(): Promise<boolean> {
|
async screenshotAvailable(): Promise<boolean> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 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 {Entry, Priority} from 'adbkit-logcat';
|
||||||
|
import type {CrashLog} from 'flipper-common';
|
||||||
|
import AndroidDevice from './AndroidDevice';
|
||||||
|
|
||||||
|
export function parseAndroidCrash(content: string, logDate?: Date) {
|
||||||
|
const regForName = /.*\n/;
|
||||||
|
const nameRegArr = regForName.exec(content);
|
||||||
|
let name = nameRegArr ? nameRegArr[0] : 'Unknown';
|
||||||
|
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() : 'Unknown';
|
||||||
|
const reason = reasonText ? reasonText : 'Unknown';
|
||||||
|
if (name[name.length - 1] === '\n') {
|
||||||
|
name = name.slice(0, -1);
|
||||||
|
}
|
||||||
|
const crash: CrashLog = {
|
||||||
|
callstack: content,
|
||||||
|
name: name,
|
||||||
|
reason: reason,
|
||||||
|
date: logDate?.getTime(),
|
||||||
|
};
|
||||||
|
return crash;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldParseAndroidLog(entry: Entry, date: Date): boolean {
|
||||||
|
return (
|
||||||
|
entry.date.getTime() - date.getTime() > 0 && // The log should have arrived after the device has been registered
|
||||||
|
((entry.priority === Priority.ERROR && entry.tag === 'AndroidRuntime') ||
|
||||||
|
entry.priority === Priority.FATAL)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts listening ADB logs. Emits 'device-crash' on "error" and "fatal" entries.
|
||||||
|
* Listens to the logs in a separate stream.
|
||||||
|
* We can't leverage teh existing log listener mechanism (see `startLogging`)
|
||||||
|
* it is started externally (by the client). Refactoring how we start log listeners is a bit too much.
|
||||||
|
* It is easier to start its own stream for crash watcher and manage it independently.
|
||||||
|
*/
|
||||||
|
export function startAndroidCrashWatcher(device: AndroidDevice) {
|
||||||
|
const referenceDate = new Date();
|
||||||
|
let androidLog: string = '';
|
||||||
|
let androidLogUnderProcess = false;
|
||||||
|
let timer: null | NodeJS.Timeout = null;
|
||||||
|
let gracefulShutdown = false;
|
||||||
|
const readerPromise = device.adb
|
||||||
|
.openLogcat(device.serial, {clear: true})
|
||||||
|
.then((reader) =>
|
||||||
|
reader
|
||||||
|
.on('entry', (entry) => {
|
||||||
|
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) {
|
||||||
|
device.flipperServer.emit('device-crash', {
|
||||||
|
crash: parseAndroidCrash(androidLog, entry.date),
|
||||||
|
serial: device.info.serial,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
androidLogUnderProcess = false;
|
||||||
|
androidLog = '';
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
if (!gracefulShutdown) {
|
||||||
|
// logs didn't stop gracefully
|
||||||
|
setTimeout(() => {
|
||||||
|
if (device.connected) {
|
||||||
|
console.warn(
|
||||||
|
`Log stream broken: ${device.serial} - restarting`,
|
||||||
|
);
|
||||||
|
device.startCrashWatcher();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (e) => {
|
||||||
|
console.warn('Failed to read from adb logcat: ', e);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('Failed to open log stream: ', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
gracefulShutdown = true;
|
||||||
|
readerPromise
|
||||||
|
.then((reader) => reader?.end())
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Failed to close adb logcat stream: ', e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {dirname, join} from 'path';
|
|||||||
import {DeviceSpec} from 'flipper-common';
|
import {DeviceSpec} from 'flipper-common';
|
||||||
import {ServerDevice} from '../ServerDevice';
|
import {ServerDevice} from '../ServerDevice';
|
||||||
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
||||||
|
import {startAndroidCrashWatcher} from './AndroidCrashUtils';
|
||||||
|
|
||||||
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
|
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ export default class AndroidDevice extends ServerDevice {
|
|||||||
this.adb = adb;
|
this.adb = adb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Prevent starting logging multiple times
|
||||||
startLogging() {
|
startLogging() {
|
||||||
this.adb
|
this.adb
|
||||||
.openLogcat(this.serial, {clear: true})
|
.openLogcat(this.serial, {clear: true})
|
||||||
@@ -112,6 +114,10 @@ export default class AndroidDevice extends ServerDevice {
|
|||||||
this.reader = undefined;
|
this.reader = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected startCrashWatcherImpl(): () => void {
|
||||||
|
return startAndroidCrashWatcher(this);
|
||||||
|
}
|
||||||
|
|
||||||
reverse(ports: number[]): Promise<void> {
|
reverse(ports: number[]): Promise<void> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
ports.map((port) =>
|
ports.map((port) =>
|
||||||
|
|||||||
@@ -7,23 +7,23 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {DeviceLogEntry, DeviceLogLevel} from 'flipper-plugin';
|
import {Entry, Priority} from 'adbkit-logcat';
|
||||||
import {shouldParseAndroidLog} from '../android-crash-utils';
|
import {parseAndroidCrash, shouldParseAndroidLog} from '../AndroidCrashUtils';
|
||||||
|
|
||||||
function getAndroidLog(
|
function getAndroidLog(
|
||||||
date: Date,
|
date: Date,
|
||||||
type: DeviceLogLevel,
|
priority: number,
|
||||||
tag: string,
|
tag: string,
|
||||||
message: string,
|
message: string,
|
||||||
): DeviceLogEntry {
|
) {
|
||||||
return {date, type, tag, message, app: 'testapp', pid: 0, tid: 0};
|
return {date, priority, tag, message, pid: 0, tid: 0} as Entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
test('test shouldParseAndroidLog function for type error and tag is AndroidRuntime', () => {
|
test('test shouldParseAndroidLog function for type error and tag is AndroidRuntime', () => {
|
||||||
const referenceDate = new Date();
|
const referenceDate = new Date();
|
||||||
const log: DeviceLogEntry = getAndroidLog(
|
const log = getAndroidLog(
|
||||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||||
'error',
|
Priority.ERROR,
|
||||||
'AndroidRuntime',
|
'AndroidRuntime',
|
||||||
'Possible runtime crash',
|
'Possible runtime crash',
|
||||||
);
|
);
|
||||||
@@ -32,9 +32,9 @@ test('test shouldParseAndroidLog function for type error and tag is AndroidRunti
|
|||||||
});
|
});
|
||||||
test('test shouldParseAndroidLog function for type non-error', () => {
|
test('test shouldParseAndroidLog function for type non-error', () => {
|
||||||
const referenceDate = new Date();
|
const referenceDate = new Date();
|
||||||
const log: DeviceLogEntry = getAndroidLog(
|
const log = getAndroidLog(
|
||||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||||
'debug',
|
Priority.DEBUG,
|
||||||
'fb4a.activitymanager',
|
'fb4a.activitymanager',
|
||||||
'Possible debug info in activitymanager',
|
'Possible debug info in activitymanager',
|
||||||
);
|
);
|
||||||
@@ -43,9 +43,9 @@ test('test shouldParseAndroidLog function for type non-error', () => {
|
|||||||
});
|
});
|
||||||
test('test shouldParseAndroidLog function for the older android log', () => {
|
test('test shouldParseAndroidLog function for the older android log', () => {
|
||||||
const referenceDate = new Date();
|
const referenceDate = new Date();
|
||||||
const log: DeviceLogEntry = getAndroidLog(
|
const log = getAndroidLog(
|
||||||
new Date(referenceDate.getTime() - 10000), //This log arrives 10 secs before the refernce time
|
new Date(referenceDate.getTime() - 10000), //This log arrives 10 secs before the refernce time
|
||||||
'error',
|
Priority.ERROR,
|
||||||
'fb4a.activitymanager',
|
'fb4a.activitymanager',
|
||||||
'Possible error info in activitymanager',
|
'Possible error info in activitymanager',
|
||||||
);
|
);
|
||||||
@@ -54,9 +54,9 @@ test('test shouldParseAndroidLog function for the older android log', () => {
|
|||||||
});
|
});
|
||||||
test('test shouldParseAndroidLog function for the fatal log', () => {
|
test('test shouldParseAndroidLog function for the fatal log', () => {
|
||||||
const referenceDate = new Date();
|
const referenceDate = new Date();
|
||||||
const log: DeviceLogEntry = getAndroidLog(
|
const log = getAndroidLog(
|
||||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||||
'fatal',
|
Priority.FATAL,
|
||||||
'arbitrary tag',
|
'arbitrary tag',
|
||||||
'Possible error info in activitymanager',
|
'Possible error info in activitymanager',
|
||||||
);
|
);
|
||||||
@@ -65,12 +65,39 @@ test('test shouldParseAndroidLog function for the fatal log', () => {
|
|||||||
});
|
});
|
||||||
test('test shouldParseAndroidLog function for the error log which does not staisfy our tags check', () => {
|
test('test shouldParseAndroidLog function for the error log which does not staisfy our tags check', () => {
|
||||||
const referenceDate = new Date();
|
const referenceDate = new Date();
|
||||||
const log: DeviceLogEntry = getAndroidLog(
|
const log = getAndroidLog(
|
||||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||||
'error',
|
Priority.ERROR,
|
||||||
'arbitrary tag',
|
'arbitrary tag',
|
||||||
'Possible error info in fb4a',
|
'Possible error info in fb4a',
|
||||||
);
|
);
|
||||||
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
|
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
|
||||||
expect(shouldParseTheLog).toEqual(false);
|
expect(shouldParseTheLog).toEqual(false);
|
||||||
});
|
});
|
||||||
|
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 = parseAndroidCrash(log, 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.getTime());
|
||||||
|
});
|
||||||
|
test('test the parsing of the Android crash log for the unknown crash format and no date', () => {
|
||||||
|
const log = 'Blaa Blaa Blaa';
|
||||||
|
const crash = parseAndroidCrash(log, undefined);
|
||||||
|
expect(crash.callstack).toEqual(log);
|
||||||
|
expect(crash.reason).toEqual('Unknown');
|
||||||
|
expect(crash.name).toEqual('Unknown');
|
||||||
|
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 = parseAndroidCrash(log, undefined);
|
||||||
|
expect(crash.callstack).toEqual(log);
|
||||||
|
expect(crash.reason).toEqual('Unknown');
|
||||||
|
expect(crash.name).toEqual('First Line Break ');
|
||||||
|
});
|
||||||
@@ -20,6 +20,7 @@ import {ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB, IOSBridge} from './IOSBridge';
|
|||||||
import split2 from 'split2';
|
import split2 from 'split2';
|
||||||
import {ServerDevice} from '../ServerDevice';
|
import {ServerDevice} from '../ServerDevice';
|
||||||
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
||||||
|
import {addFileWatcherForiOSCrashLogs} from './iOSCrashUtils';
|
||||||
|
|
||||||
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
|
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
|
||||||
|
|
||||||
@@ -153,6 +154,10 @@ export default class IOSDevice extends ServerDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected startCrashWatcherImpl(): () => void {
|
||||||
|
return addFileWatcherForiOSCrashLogs(this);
|
||||||
|
}
|
||||||
|
|
||||||
static getLogLevel(level: string): DeviceLogLevel {
|
static getLogLevel(level: string): DeviceLogLevel {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'Default':
|
case 'Default':
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
parseIosCrash,
|
||||||
|
parsePath,
|
||||||
|
shouldShowiOSCrashNotification,
|
||||||
|
} from '../iOSCrashUtils';
|
||||||
|
|
||||||
|
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 = parseIosCrash(log);
|
||||||
|
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').getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = parseIosCrash(log);
|
||||||
|
expect(crash.callstack).toEqual(log);
|
||||||
|
expect(crash.reason).toEqual('SIGSEGV');
|
||||||
|
expect(crash.name).toEqual('SIGSEGV');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = parseIosCrash(log);
|
||||||
|
expect(crash.callstack).toEqual(log);
|
||||||
|
expect(crash.reason).toEqual('Unknown');
|
||||||
|
expect(crash.name).toEqual('Unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = parseIosCrash(log);
|
||||||
|
expect(crash.callstack).toEqual(log);
|
||||||
|
expect(crash.reason).toEqual('Unknown');
|
||||||
|
expect(crash.name).toEqual('Unknown');
|
||||||
|
});
|
||||||
|
test('test the parsing of the reason for crash when log is empty', () => {
|
||||||
|
const log = '';
|
||||||
|
const crash = parseIosCrash(log);
|
||||||
|
expect(crash.callstack).toEqual(log);
|
||||||
|
expect(crash.reason).toEqual('Unknown');
|
||||||
|
expect(crash.name).toEqual('Unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = parseIosCrash(log);
|
||||||
|
expect(crash.callstack).toEqual(log);
|
||||||
|
expect(crash.reason).toEqual('Unknown');
|
||||||
|
expect(crash.name).toEqual('Unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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 = shouldShowiOSCrashNotification(
|
||||||
|
'TH1S-15DEV1CE-1D',
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
expect(shouldShowNotification).toEqual(true);
|
||||||
|
});
|
||||||
|
test('test shouldShowiOSCrashNotification function for all correct inputs but incorrect id', () => {
|
||||||
|
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 = shouldShowiOSCrashNotification(
|
||||||
|
'TH1S-15DEV1CE-1D',
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
expect(shouldShowNotification).toEqual(false);
|
||||||
|
});
|
||||||
|
test('test shouldShowiOSCrashNotification 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 = shouldShowiOSCrashNotification(
|
||||||
|
null as any,
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
expect(shouldShowNotification).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only crashes from the correct device are picked up', () => {
|
||||||
|
const serial = 'AC9482A2-26A4-404F-A179-A9FB60B077F6';
|
||||||
|
const crash = `Process: Sample [87361]
|
||||||
|
Path: /Users/USER/Library/Developer/CoreSimulator/Devices/AC9482A2-26A4-404F-A179-A9FB60B077F6/data/Containers/Bundle/Application/9BF91EF9-F915-4745-BE91-EBA397451850/Sample.app/Sample
|
||||||
|
Identifier: Sample
|
||||||
|
Version: 1.0 (1)
|
||||||
|
Code Type: X86-64 (Native)
|
||||||
|
Parent Process: launchd_sim [70150]
|
||||||
|
Responsible: SimulatorTrampoline [1246]
|
||||||
|
User ID: 501`;
|
||||||
|
|
||||||
|
expect(shouldShowiOSCrashNotification(serial, crash)).toBe(true);
|
||||||
|
// wrong serial
|
||||||
|
expect(
|
||||||
|
shouldShowiOSCrashNotification(
|
||||||
|
'XC9482A2-26A4-404F-A179-A9FB60B077F6',
|
||||||
|
crash,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
@@ -7,11 +7,11 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {CrashLog} from './index';
|
import type {CrashLog} from 'flipper-common';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import {path} from 'flipper-plugin';
|
import path from 'path';
|
||||||
import {UNKNOWN_CRASH_REASON} from './crash-utils';
|
import {ServerDevice} from '../ServerDevice';
|
||||||
|
|
||||||
export function parseIosCrash(content: string) {
|
export function parseIosCrash(content: string) {
|
||||||
const regex = /Exception Type: *\w*/;
|
const regex = /Exception Type: *\w*/;
|
||||||
@@ -19,7 +19,7 @@ export function parseIosCrash(content: string) {
|
|||||||
const exceptionString = arr ? arr[0] : '';
|
const exceptionString = arr ? arr[0] : '';
|
||||||
const exceptionRegex = /\w*$/;
|
const exceptionRegex = /\w*$/;
|
||||||
const tmp = exceptionRegex.exec(exceptionString);
|
const tmp = exceptionRegex.exec(exceptionString);
|
||||||
const exception = tmp && tmp[0].length ? tmp[0] : UNKNOWN_CRASH_REASON;
|
const exception = tmp && tmp[0].length ? tmp[0] : 'Unknown';
|
||||||
|
|
||||||
const dateRegex = /Date\/Time: *[\w\s\.:-]*/;
|
const dateRegex = /Date\/Time: *[\w\s\.:-]*/;
|
||||||
const dateArr = dateRegex.exec(content);
|
const dateArr = dateRegex.exec(content);
|
||||||
@@ -63,16 +63,14 @@ export function parsePath(content: string): string | null {
|
|||||||
return path.trim();
|
return path.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFileWatcherForiOSCrashLogs(
|
export function addFileWatcherForiOSCrashLogs(device: ServerDevice) {
|
||||||
serial: string,
|
|
||||||
reportCrash: (payload: CrashLog) => void,
|
|
||||||
) {
|
|
||||||
const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports');
|
const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports');
|
||||||
if (!(await fs.pathExists(dir))) {
|
// eslint-disable-next-line node/no-sync
|
||||||
// Directory doesn't exist
|
if (!fs.pathExistsSync(dir)) {
|
||||||
return;
|
console.warn('Failed to start iOS crash watcher');
|
||||||
|
return () => {};
|
||||||
}
|
}
|
||||||
return fs.watch(dir, async (_eventType, filename) => {
|
const watcher = fs.watch(dir, async (_eventType, filename) => {
|
||||||
// We just parse the crash logs with extension `.crash`
|
// We just parse the crash logs with extension `.crash`
|
||||||
const checkFileExtension = /.crash$/.exec(filename);
|
const checkFileExtension = /.crash$/.exec(filename);
|
||||||
if (!filename || !checkFileExtension) {
|
if (!filename || !checkFileExtension) {
|
||||||
@@ -88,9 +86,14 @@ export async function addFileWatcherForiOSCrashLogs(
|
|||||||
console.warn('Failed to read crash file', err);
|
console.warn('Failed to read crash file', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (shouldShowiOSCrashNotification(serial, data)) {
|
if (shouldShowiOSCrashNotification(device.info.serial, data)) {
|
||||||
reportCrash(parseIosCrash(data));
|
device.flipperServer.emit('device-crash', {
|
||||||
|
crash: parseIosCrash(data),
|
||||||
|
serial: device.info.serial,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => watcher.close();
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Idler,
|
Idler,
|
||||||
createState,
|
createState,
|
||||||
getFlipperLib,
|
getFlipperLib,
|
||||||
|
CrashLogListener,
|
||||||
} from 'flipper-plugin';
|
} from 'flipper-plugin';
|
||||||
import {
|
import {
|
||||||
DeviceLogEntry,
|
DeviceLogEntry,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
DeviceType,
|
DeviceType,
|
||||||
DeviceDescription,
|
DeviceDescription,
|
||||||
FlipperServer,
|
FlipperServer,
|
||||||
|
CrashLog,
|
||||||
} from 'flipper-common';
|
} from 'flipper-common';
|
||||||
import {DeviceSpec, PluginDetails} from 'flipper-common';
|
import {DeviceSpec, PluginDetails} from 'flipper-common';
|
||||||
import {getPluginKey} from '../utils/pluginKey';
|
import {getPluginKey} from '../utils/pluginKey';
|
||||||
@@ -85,6 +87,8 @@ export default class BaseDevice implements Device {
|
|||||||
|
|
||||||
logListeners: Map<Symbol, DeviceLogListener> = new Map();
|
logListeners: Map<Symbol, DeviceLogListener> = new Map();
|
||||||
|
|
||||||
|
crashListeners: Map<Symbol, CrashLogListener> = new Map();
|
||||||
|
|
||||||
readonly connected = createState(true);
|
readonly connected = createState(true);
|
||||||
|
|
||||||
// if imported, stores the original source location
|
// if imported, stores the original source location
|
||||||
@@ -181,6 +185,52 @@ export default class BaseDevice implements Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private crashLogEventHandler = (payload: {
|
||||||
|
serial: string;
|
||||||
|
crash: CrashLog;
|
||||||
|
}) => {
|
||||||
|
if (payload.serial === this.serial && this.crashListeners.size > 0) {
|
||||||
|
this.addCrashEntry(payload.crash);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addCrashEntry(entry: CrashLog) {
|
||||||
|
this.crashListeners.forEach((listener) => {
|
||||||
|
// prevent breaking other listeners, if one listener doesn't work.
|
||||||
|
try {
|
||||||
|
listener(entry);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Crash listener exception:`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startCrashWatcher() {
|
||||||
|
await this.flipperServer.exec('device-start-crash-watcher', this.serial);
|
||||||
|
this.flipperServer.on('device-crash', this.crashLogEventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCrashWatcher() {
|
||||||
|
this.flipperServer.off('device-crash', this.crashLogEventHandler);
|
||||||
|
return this.flipperServer.exec('device-stop-crash-watcher', this.serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCrashListener(callback: CrashLogListener): Symbol {
|
||||||
|
if (this.crashListeners.size === 0) {
|
||||||
|
this.startCrashWatcher();
|
||||||
|
}
|
||||||
|
const id = Symbol();
|
||||||
|
this.crashListeners.set(id, callback);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCrashListener(id: Symbol) {
|
||||||
|
this.crashListeners.delete(id);
|
||||||
|
if (this.crashListeners.size === 0) {
|
||||||
|
this.stopCrashWatcher();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async navigateToLocation(location: string) {
|
async navigateToLocation(location: string) {
|
||||||
return this.flipperServer.exec('device-navigate', this.serial, location);
|
return this.flipperServer.exec('device-navigate', this.serial, location);
|
||||||
}
|
}
|
||||||
@@ -328,6 +378,8 @@ export default class BaseDevice implements Device {
|
|||||||
disconnect() {
|
disconnect() {
|
||||||
this.logListeners.clear();
|
this.logListeners.clear();
|
||||||
this.stopLogging();
|
this.stopLogging();
|
||||||
|
this.crashListeners.clear();
|
||||||
|
this.stopCrashWatcher();
|
||||||
this.connected.set(false);
|
this.connected.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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 {TestUtils, CrashLog} from 'flipper-plugin';
|
||||||
|
import * as CrashReporterPlugin from '../index';
|
||||||
|
|
||||||
|
test('test defaultPersistedState of CrashReporterPlugin', () => {
|
||||||
|
expect(
|
||||||
|
TestUtils.startDevicePlugin(CrashReporterPlugin).exportState(),
|
||||||
|
).toEqual({crashes: []});
|
||||||
|
});
|
||||||
|
test('test helper setdefaultPersistedState function', () => {
|
||||||
|
const crash: CrashLog = {
|
||||||
|
callstack: 'callstack',
|
||||||
|
reason: 'crash0',
|
||||||
|
name: 'crash0',
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
|
||||||
|
plugin.instance.reportCrash(crash);
|
||||||
|
expect(plugin.exportState()).toEqual({
|
||||||
|
crashes: [
|
||||||
|
{
|
||||||
|
...crash,
|
||||||
|
notificationID: '0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState', () => {
|
||||||
|
const crash: CrashLog = {
|
||||||
|
callstack: 'callstack',
|
||||||
|
reason: 'crash0',
|
||||||
|
name: 'crash0',
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
|
||||||
|
plugin.instance.reportCrash(crash);
|
||||||
|
const pluginStateCrash: CrashLog = {
|
||||||
|
callstack: 'callstack',
|
||||||
|
reason: 'crash1',
|
||||||
|
name: 'crash1',
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
plugin.instance.reportCrash(pluginStateCrash);
|
||||||
|
const crashes = plugin.instance.crashes.get();
|
||||||
|
expect(crashes).toBeDefined();
|
||||||
|
expect(crashes.length).toEqual(2);
|
||||||
|
expect(crashes[1]).toEqual({
|
||||||
|
...pluginStateCrash,
|
||||||
|
notificationID: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 {TestDevice} from 'flipper';
|
|
||||||
import {Crash, CrashLog} from '../index';
|
|
||||||
import {TestUtils} from 'flipper-plugin';
|
|
||||||
import {getPluginKey} from 'flipper';
|
|
||||||
import * as CrashReporterPlugin from '../index';
|
|
||||||
import {
|
|
||||||
parseIosCrash,
|
|
||||||
parsePath,
|
|
||||||
shouldShowiOSCrashNotification,
|
|
||||||
} from '../ios-crash-utils';
|
|
||||||
import {parseAndroidCrash} from '../android-crash-utils';
|
|
||||||
|
|
||||||
function getCrash(
|
|
||||||
id: number,
|
|
||||||
callstack: string,
|
|
||||||
name: string,
|
|
||||||
reason: string,
|
|
||||||
): Crash & CrashLog {
|
|
||||||
return {
|
|
||||||
notificationID: id.toString(),
|
|
||||||
callstack: callstack,
|
|
||||||
reason: reason,
|
|
||||||
name: name,
|
|
||||||
date: new Date().getTime(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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(Math.abs(date - expectedCrash.date)).toBeLessThan(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = parseIosCrash(log);
|
|
||||||
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').getTime());
|
|
||||||
});
|
|
||||||
|
|
||||||
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 = parseIosCrash(log);
|
|
||||||
expect(crash.callstack).toEqual(log);
|
|
||||||
expect(crash.reason).toEqual('SIGSEGV');
|
|
||||||
expect(crash.name).toEqual('SIGSEGV');
|
|
||||||
});
|
|
||||||
|
|
||||||
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 = parseIosCrash(log);
|
|
||||||
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 = parseIosCrash(log);
|
|
||||||
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 is empty', () => {
|
|
||||||
const log = '';
|
|
||||||
const crash = parseIosCrash(log);
|
|
||||||
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 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 = parseAndroidCrash(log, 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.getTime());
|
|
||||||
});
|
|
||||||
test('test the parsing of the Android crash log for the unknown crash format and no date', () => {
|
|
||||||
const log = 'Blaa Blaa Blaa';
|
|
||||||
const crash = parseAndroidCrash(log, undefined);
|
|
||||||
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 = parseAndroidCrash(log, undefined);
|
|
||||||
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 = parseIosCrash(log);
|
|
||||||
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 TestDevice('serial', 'emulator', 'test device', 'iOS');
|
|
||||||
const pluginKey = getPluginKey(null, device, 'CrashReporter');
|
|
||||||
expect(pluginKey).toEqual('serial#CrashReporter');
|
|
||||||
});
|
|
||||||
test('test the getter of pluginKey with undefined input', () => {
|
|
||||||
const pluginKey = getPluginKey(null, null, 'CrashReporter');
|
|
||||||
expect(pluginKey).toEqual('unknown#CrashReporter');
|
|
||||||
});
|
|
||||||
test('test the getter of pluginKey with defined selected app', () => {
|
|
||||||
const pluginKey = getPluginKey('selectedApp', null, 'CrashReporter');
|
|
||||||
expect(pluginKey).toEqual('selectedApp#CrashReporter');
|
|
||||||
});
|
|
||||||
test('test the getter of pluginKey with defined selected app and defined base device', () => {
|
|
||||||
const device = new TestDevice('serial', 'emulator', 'test device', 'iOS');
|
|
||||||
const pluginKey = getPluginKey('selectedApp', device, 'CrashReporter');
|
|
||||||
expect(pluginKey).toEqual('selectedApp#CrashReporter');
|
|
||||||
});
|
|
||||||
test('test defaultPersistedState of CrashReporterPlugin', () => {
|
|
||||||
expect(
|
|
||||||
TestUtils.startDevicePlugin(CrashReporterPlugin).exportState(),
|
|
||||||
).toEqual({crashes: []});
|
|
||||||
});
|
|
||||||
test('test helper setdefaultPersistedState function', () => {
|
|
||||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
|
||||||
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
|
|
||||||
plugin.instance.reportCrash(crash);
|
|
||||||
expect(plugin.exportState()).toEqual({
|
|
||||||
crashes: [crash],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState', () => {
|
|
||||||
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
|
|
||||||
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
|
|
||||||
plugin.instance.reportCrash(crash);
|
|
||||||
const pluginStateCrash = getCrash(1, 'callstack', 'crash1', 'crash1');
|
|
||||||
plugin.instance.reportCrash(pluginStateCrash);
|
|
||||||
const crashes = plugin.instance.crashes.get();
|
|
||||||
expect(crashes).toBeDefined();
|
|
||||||
expect(crashes.length).toEqual(2);
|
|
||||||
expect(crashes[1]).toEqual(pluginStateCrash);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState and improper crash log', () => {
|
|
||||||
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
|
|
||||||
const pluginStateCrash = getCrash(0, 'callstack', 'crash1', 'crash1');
|
|
||||||
plugin.instance.reportCrash(pluginStateCrash);
|
|
||||||
const content = 'Blaa Blaaa \n Blaa Blaaa';
|
|
||||||
plugin.instance.reportCrash(parseIosCrash(content));
|
|
||||||
const crashes = plugin.instance.crashes.get();
|
|
||||||
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 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 TestDevice(
|
|
||||||
'TH1S-15DEV1CE-1D',
|
|
||||||
'emulator',
|
|
||||||
'test device',
|
|
||||||
'iOS',
|
|
||||||
);
|
|
||||||
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 = shouldShowiOSCrashNotification(
|
|
||||||
device.serial,
|
|
||||||
content,
|
|
||||||
);
|
|
||||||
expect(shouldShowNotification).toEqual(true);
|
|
||||||
});
|
|
||||||
test('test shouldShowiOSCrashNotification function for all correct inputs but incorrect id', () => {
|
|
||||||
const device = new TestDevice(
|
|
||||||
'TH1S-15DEV1CE-1D',
|
|
||||||
'emulator',
|
|
||||||
'test device',
|
|
||||||
'iOS',
|
|
||||||
);
|
|
||||||
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 = shouldShowiOSCrashNotification(
|
|
||||||
device.serial,
|
|
||||||
content,
|
|
||||||
);
|
|
||||||
expect(shouldShowNotification).toEqual(false);
|
|
||||||
});
|
|
||||||
test('test shouldShowiOSCrashNotification 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 = shouldShowiOSCrashNotification(
|
|
||||||
null as any,
|
|
||||||
content,
|
|
||||||
);
|
|
||||||
expect(shouldShowNotification).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('only crashes from the correct device are picked up', () => {
|
|
||||||
const serial = 'AC9482A2-26A4-404F-A179-A9FB60B077F6';
|
|
||||||
const crash = `Process: Sample [87361]
|
|
||||||
Path: /Users/USER/Library/Developer/CoreSimulator/Devices/AC9482A2-26A4-404F-A179-A9FB60B077F6/data/Containers/Bundle/Application/9BF91EF9-F915-4745-BE91-EBA397451850/Sample.app/Sample
|
|
||||||
Identifier: Sample
|
|
||||||
Version: 1.0 (1)
|
|
||||||
Code Type: X86-64 (Native)
|
|
||||||
Parent Process: launchd_sim [70150]
|
|
||||||
Responsible: SimulatorTrampoline [1246]
|
|
||||||
User ID: 501`;
|
|
||||||
|
|
||||||
expect(shouldShowiOSCrashNotification(serial, crash)).toBe(true);
|
|
||||||
// wrong serial
|
|
||||||
expect(
|
|
||||||
shouldShowiOSCrashNotification(
|
|
||||||
'XC9482A2-26A4-404F-A179-A9FB60B077F6',
|
|
||||||
crash,
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 type {DeviceLogEntry, DevicePluginClient} from 'flipper-plugin';
|
|
||||||
import {UNKNOWN_CRASH_REASON} from './crash-utils';
|
|
||||||
import type {CrashLog} from './index';
|
|
||||||
|
|
||||||
export function parseAndroidCrash(content: string, logDate?: Date) {
|
|
||||||
const regForName = /.*\n/;
|
|
||||||
const nameRegArr = regForName.exec(content);
|
|
||||||
let name = nameRegArr ? nameRegArr[0] : UNKNOWN_CRASH_REASON;
|
|
||||||
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()
|
|
||||||
: UNKNOWN_CRASH_REASON;
|
|
||||||
const reason = reasonText ? reasonText : UNKNOWN_CRASH_REASON;
|
|
||||||
if (name[name.length - 1] === '\n') {
|
|
||||||
name = name.slice(0, -1);
|
|
||||||
}
|
|
||||||
const crash: CrashLog = {
|
|
||||||
callstack: content,
|
|
||||||
name: name,
|
|
||||||
reason: reason,
|
|
||||||
date: logDate?.getTime(),
|
|
||||||
};
|
|
||||||
return crash;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldParseAndroidLog(
|
|
||||||
entry: DeviceLogEntry,
|
|
||||||
date: Date,
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
entry.date.getTime() - date.getTime() > 0 && // The log should have arrived after the device has been registered
|
|
||||||
((entry.type === 'error' && entry.tag === 'AndroidRuntime') ||
|
|
||||||
entry.type === 'fatal')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startAndroidCrashWatcher(
|
|
||||||
client: DevicePluginClient,
|
|
||||||
reportCrash: (payload: CrashLog) => void,
|
|
||||||
) {
|
|
||||||
const referenceDate = new Date();
|
|
||||||
let androidLog: string = '';
|
|
||||||
let androidLogUnderProcess = false;
|
|
||||||
let timer: null | NodeJS.Timeout = null;
|
|
||||||
client.onDeviceLogEntry((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) {
|
|
||||||
reportCrash(parseAndroidCrash(androidLog, entry.date));
|
|
||||||
}
|
|
||||||
androidLogUnderProcess = false;
|
|
||||||
androidLog = '';
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ import unicodeSubstring from 'unicode-substring';
|
|||||||
import type {Crash} from './index';
|
import type {Crash} from './index';
|
||||||
import {DevicePluginClient} from 'flipper-plugin';
|
import {DevicePluginClient} from 'flipper-plugin';
|
||||||
|
|
||||||
export const UNKNOWN_CRASH_REASON = 'Cannot figure out the cause';
|
export const UNKNOWN_CRASH_REASON = 'Unknown';
|
||||||
|
|
||||||
function truncate(baseString: string, numOfChars: number): string {
|
function truncate(baseString: string, numOfChars: number): string {
|
||||||
if (baseString.length <= numOfChars) {
|
if (baseString.length <= numOfChars) {
|
||||||
|
|||||||
@@ -7,11 +7,8 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {FSWatcher} from 'fs';
|
import {createState, DevicePluginClient, CrashLog} from 'flipper-plugin';
|
||||||
import {createState, DevicePluginClient} from 'flipper-plugin';
|
|
||||||
import {showCrashNotification} from './crash-utils';
|
import {showCrashNotification} from './crash-utils';
|
||||||
import {addFileWatcherForiOSCrashLogs} from './ios-crash-utils';
|
|
||||||
import {startAndroidCrashWatcher} from './android-crash-utils';
|
|
||||||
|
|
||||||
export type Crash = {
|
export type Crash = {
|
||||||
notificationID: string;
|
notificationID: string;
|
||||||
@@ -21,16 +18,8 @@ export type Crash = {
|
|||||||
date: number;
|
date: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CrashLog = {
|
|
||||||
callstack: string;
|
|
||||||
reason: string;
|
|
||||||
name: string;
|
|
||||||
date?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function devicePlugin(client: DevicePluginClient) {
|
export function devicePlugin(client: DevicePluginClient) {
|
||||||
let notificationID = -1;
|
let notificationID = -1;
|
||||||
let watcher: Promise<FSWatcher | undefined> | undefined = undefined;
|
|
||||||
|
|
||||||
const crashes = createState<Crash[]>([], {persist: 'crashes'});
|
const crashes = createState<Crash[]>([], {persist: 'crashes'});
|
||||||
const selectedCrash = createState<string | undefined>();
|
const selectedCrash = createState<string | undefined>();
|
||||||
@@ -59,31 +48,13 @@ export function devicePlugin(client: DevicePluginClient) {
|
|||||||
|
|
||||||
// Startup logic to establish log monitoring
|
// Startup logic to establish log monitoring
|
||||||
if (client.device.isConnected) {
|
if (client.device.isConnected) {
|
||||||
if (client.device.os.includes('iOS')) {
|
client.onDeviceCrash(reportCrash);
|
||||||
watcher = addFileWatcherForiOSCrashLogs(
|
|
||||||
client.device.serial,
|
|
||||||
reportCrash,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
startAndroidCrashWatcher(client, reportCrash);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.onDestroy(() => {
|
|
||||||
watcher
|
|
||||||
?.then((watcher) => watcher?.close())
|
|
||||||
.catch((e) =>
|
|
||||||
console.error(
|
|
||||||
'[crash_reporter] FSWatcher failed resoving on destroy:',
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reportCrash,
|
|
||||||
crashes,
|
crashes,
|
||||||
selectedCrash,
|
selectedCrash,
|
||||||
|
reportCrash,
|
||||||
openInLogs(callstack: string) {
|
openInLogs(callstack: string) {
|
||||||
client.selectPlugin('DeviceLogs', callstack);
|
client.selectPlugin('DeviceLogs', callstack);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user