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
@@ -289,6 +289,10 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
this.getDevice(serial).startLogging(),
|
||||
'device-stop-logging': async (serial: string) =>
|
||||
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) =>
|
||||
this.getDevice(serial).screenshotAvailable(),
|
||||
'device-supports-screencapture': async (serial: string) =>
|
||||
|
||||
@@ -15,6 +15,8 @@ export abstract class ServerDevice {
|
||||
readonly flipperServer: FlipperServerImpl;
|
||||
connected = true;
|
||||
|
||||
protected stopCrashWatcherCb?: () => void;
|
||||
|
||||
constructor(flipperServer: FlipperServerImpl, info: DeviceDescription) {
|
||||
this.flipperServer = flipperServer;
|
||||
this.info = info;
|
||||
@@ -46,6 +48,20 @@ export abstract class ServerDevice {
|
||||
// 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> {
|
||||
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 {ServerDevice} from '../ServerDevice';
|
||||
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
||||
import {startAndroidCrashWatcher} from './AndroidCrashUtils';
|
||||
|
||||
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
|
||||
|
||||
@@ -49,6 +50,7 @@ export default class AndroidDevice extends ServerDevice {
|
||||
this.adb = adb;
|
||||
}
|
||||
|
||||
// TODO: Prevent starting logging multiple times
|
||||
startLogging() {
|
||||
this.adb
|
||||
.openLogcat(this.serial, {clear: true})
|
||||
@@ -112,6 +114,10 @@ export default class AndroidDevice extends ServerDevice {
|
||||
this.reader = undefined;
|
||||
}
|
||||
|
||||
protected startCrashWatcherImpl(): () => void {
|
||||
return startAndroidCrashWatcher(this);
|
||||
}
|
||||
|
||||
reverse(ports: number[]): Promise<void> {
|
||||
return Promise.all(
|
||||
ports.map((port) =>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 {parseAndroidCrash, shouldParseAndroidLog} from '../AndroidCrashUtils';
|
||||
|
||||
function getAndroidLog(
|
||||
date: Date,
|
||||
priority: number,
|
||||
tag: string,
|
||||
message: string,
|
||||
) {
|
||||
return {date, priority, tag, message, pid: 0, tid: 0} as Entry;
|
||||
}
|
||||
|
||||
test('test shouldParseAndroidLog function for type error and tag is AndroidRuntime', () => {
|
||||
const referenceDate = new Date();
|
||||
const log = getAndroidLog(
|
||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||
Priority.ERROR,
|
||||
'AndroidRuntime',
|
||||
'Possible runtime crash',
|
||||
);
|
||||
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
|
||||
expect(shouldParseTheLog).toEqual(true);
|
||||
});
|
||||
test('test shouldParseAndroidLog function for type non-error', () => {
|
||||
const referenceDate = new Date();
|
||||
const log = getAndroidLog(
|
||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||
Priority.DEBUG,
|
||||
'fb4a.activitymanager',
|
||||
'Possible debug info in activitymanager',
|
||||
);
|
||||
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
|
||||
expect(shouldParseTheLog).toEqual(false);
|
||||
});
|
||||
test('test shouldParseAndroidLog function for the older android log', () => {
|
||||
const referenceDate = new Date();
|
||||
const log = getAndroidLog(
|
||||
new Date(referenceDate.getTime() - 10000), //This log arrives 10 secs before the refernce time
|
||||
Priority.ERROR,
|
||||
'fb4a.activitymanager',
|
||||
'Possible error info in activitymanager',
|
||||
);
|
||||
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
|
||||
expect(shouldParseTheLog).toEqual(false);
|
||||
});
|
||||
test('test shouldParseAndroidLog function for the fatal log', () => {
|
||||
const referenceDate = new Date();
|
||||
const log = getAndroidLog(
|
||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||
Priority.FATAL,
|
||||
'arbitrary tag',
|
||||
'Possible error info in activitymanager',
|
||||
);
|
||||
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
|
||||
expect(shouldParseTheLog).toEqual(true);
|
||||
});
|
||||
test('test shouldParseAndroidLog function for the error log which does not staisfy our tags check', () => {
|
||||
const referenceDate = new Date();
|
||||
const log = getAndroidLog(
|
||||
new Date(referenceDate.getTime() + 10000), //This log arrives 10 secs after the refernce time
|
||||
Priority.ERROR,
|
||||
'arbitrary tag',
|
||||
'Possible error info in fb4a',
|
||||
);
|
||||
const shouldParseTheLog = shouldParseAndroidLog(log, referenceDate);
|
||||
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 {ServerDevice} from '../ServerDevice';
|
||||
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
||||
import {addFileWatcherForiOSCrashLogs} from './iOSCrashUtils';
|
||||
|
||||
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 {
|
||||
switch (level) {
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 {CrashLog} from 'flipper-common';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import {ServerDevice} from '../ServerDevice';
|
||||
|
||||
export function parseIosCrash(content: string) {
|
||||
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] : 'Unknown';
|
||||
|
||||
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 | null =
|
||||
tmp1 && tmp1[0].length ? tmp1[0] : null;
|
||||
const date = extractedDateString
|
||||
? new Date(extractedDateString).getTime()
|
||||
: Date.now();
|
||||
|
||||
const crash: CrashLog = {
|
||||
callstack: content,
|
||||
name: exception,
|
||||
reason: exception,
|
||||
date,
|
||||
};
|
||||
return crash;
|
||||
}
|
||||
|
||||
export function shouldShowiOSCrashNotification(
|
||||
serial: string,
|
||||
content: string,
|
||||
): boolean {
|
||||
const appPath = parsePath(content);
|
||||
if (!appPath || !appPath.includes(serial)) {
|
||||
// Do not show notifications for the app which are not running on this device
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parsePath(content: string): string | null {
|
||||
const regex = /(?<=.*Path: *)[^\n]*/;
|
||||
const arr = regex.exec(content);
|
||||
if (!arr || arr.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
const path = arr[0];
|
||||
return path.trim();
|
||||
}
|
||||
|
||||
export function addFileWatcherForiOSCrashLogs(device: ServerDevice) {
|
||||
const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports');
|
||||
// eslint-disable-next-line node/no-sync
|
||||
if (!fs.pathExistsSync(dir)) {
|
||||
console.warn('Failed to start iOS crash watcher');
|
||||
return () => {};
|
||||
}
|
||||
const watcher = fs.watch(dir, async (_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);
|
||||
const exists = await fs.pathExists(filepath);
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
fs.readFile(filepath, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
console.warn('Failed to read crash file', err);
|
||||
return;
|
||||
}
|
||||
if (shouldShowiOSCrashNotification(device.info.serial, data)) {
|
||||
device.flipperServer.emit('device-crash', {
|
||||
crash: parseIosCrash(data),
|
||||
serial: device.info.serial,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => watcher.close();
|
||||
}
|
||||
Reference in New Issue
Block a user