From 01ea822341a8ef9845ad86552584c929a8f63eb6 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Tue, 4 May 2021 13:49:11 -0700 Subject: [PATCH] Code shuffle Summary: Moved a lot of utility logic in separate files for Android and iOS, to make cleanup in next diffs easier by having a bit smaller files. Purely a code shuffle, no functional changes Reviewed By: passy Differential Revision: D28102399 fbshipit-source-id: 2fd8f6669bdd2804fa8a7e1791c610ae7883eda6 --- desktop/app/src/index.tsx | 1 - .../app/src/utils/crashReporterUtility.tsx | 21 --- .../__tests__/crashReporterUtility.node.tsx} | 4 +- .../testCrashReporterPlugin.node.tsx | 17 +- .../crash_reporter/android-crash-utils.tsx | 55 ++++++ .../public/crash_reporter/crash-utils.tsx | 48 ++++++ .../plugins/public/crash_reporter/index.tsx | 157 +----------------- .../public/crash_reporter/ios-crash-utils.tsx | 104 ++++++++++++ 8 files changed, 228 insertions(+), 179 deletions(-) delete mode 100644 desktop/app/src/utils/crashReporterUtility.tsx rename desktop/{app/src/utils/__tests__/crashReporterUtility.node.js => plugins/public/crash_reporter/__tests__/crashReporterUtility.node.tsx} (95%) create mode 100644 desktop/plugins/public/crash_reporter/android-crash-utils.tsx create mode 100644 desktop/plugins/public/crash_reporter/crash-utils.tsx create mode 100644 desktop/plugins/public/crash_reporter/ios-crash-utils.tsx diff --git a/desktop/app/src/index.tsx b/desktop/app/src/index.tsx index 8f722f33b..257df70d1 100644 --- a/desktop/app/src/index.tsx +++ b/desktop/app/src/index.tsx @@ -45,7 +45,6 @@ export {Idler, Notification} from 'flipper-plugin'; export {Store, MiddlewareAPI, State as ReduxState} from './reducers/index'; export {default as BaseDevice} from './devices/BaseDevice'; export {DeviceLogEntry, LogLevel, DeviceLogListener} from 'flipper-plugin'; -export {shouldParseAndroidLog} from './utils/crashReporterUtility'; export {deconstructClientId} from './utils/clientUtils'; export {default as isProduction} from './utils/isProduction'; export {createTablePlugin} from './createTablePlugin'; diff --git a/desktop/app/src/utils/crashReporterUtility.tsx b/desktop/app/src/utils/crashReporterUtility.tsx deleted file mode 100644 index 827cd2061..000000000 --- a/desktop/app/src/utils/crashReporterUtility.tsx +++ /dev/null @@ -1,21 +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 {DeviceLogEntry} from 'flipper-plugin'; - -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') - ); -} diff --git a/desktop/app/src/utils/__tests__/crashReporterUtility.node.js b/desktop/plugins/public/crash_reporter/__tests__/crashReporterUtility.node.tsx similarity index 95% rename from desktop/app/src/utils/__tests__/crashReporterUtility.node.js rename to desktop/plugins/public/crash_reporter/__tests__/crashReporterUtility.node.tsx index 4b8650c82..6dab90fa9 100644 --- a/desktop/app/src/utils/__tests__/crashReporterUtility.node.js +++ b/desktop/plugins/public/crash_reporter/__tests__/crashReporterUtility.node.tsx @@ -7,8 +7,8 @@ * @format */ -import {shouldParseAndroidLog} from '../crashReporterUtility.tsx'; -import type {DeviceLogEntry, LogLevel} from '../../'; +import {DeviceLogEntry, LogLevel} from 'flipper-plugin'; +import {shouldParseAndroidLog} from '../android-crash-utils'; function getAndroidLog( date: Date, diff --git a/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx b/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx index a9b200fe3..68cc7e3dd 100644 --- a/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx +++ b/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx @@ -8,11 +8,12 @@ */ import {BaseDevice} from 'flipper'; -import {Crash, shouldShowiOSCrashNotification} from '../index'; -import {parseCrashLog, parsePath} from '../index'; +import {Crash} from '../index'; import {TestUtils} from 'flipper-plugin'; import {getPluginKey} from 'flipper'; import * as CrashReporterPlugin from '../index'; +import {parseCrashLog} from '../crash-utils'; +import {parsePath, shouldShowiOSCrashNotification} from '../ios-crash-utils'; function getCrash( id: number, @@ -41,7 +42,7 @@ function assertCrash(crash: Crash, expectedCrash: Crash) { test('test the parsing of the date and crash info for the log which matches the predefined regex', () => { const log = 'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa Date/Time: 2019-03-21 12:07:00.861 +0000 \n Blaa balaaa'; - const crash = parseCrashLog(log, 'iOS', null); + const crash = parseCrashLog(log, 'iOS', undefined); expect(crash.callstack).toEqual(log); expect(crash.reason).toEqual('SIGSEGV'); expect(crash.name).toEqual('SIGSEGV'); @@ -105,7 +106,7 @@ test('test the parsing of the Android crash log for the unknown crash format and }); test('test the parsing of the Android crash log for the partial format matching the crash format', () => { const log = 'First Line Break \n Blaa Blaa \n Blaa Blaa '; - const crash = parseCrashLog(log, 'Android', null); + const crash = parseCrashLog(log, 'Android', undefined); expect(crash.callstack).toEqual(log); expect(crash.reason).toEqual('Cannot figure out the cause'); expect(crash.name).toEqual('First Line Break '); @@ -113,7 +114,7 @@ test('test the parsing of the Android crash log for the partial format matching test('test the parsing of the Android crash log with os being iOS', () => { const log = 'FATAL EXCEPTION: main\nProcess: com.facebook.flipper.sample, PID: 27026\njava.lang.IndexOutOfBoundsException: Index: 190, Size: 0\n\tat java.util.ArrayList.get(ArrayList.java:437)\n\tat com.facebook.flipper.sample.RootComponentSpec.hitGetRequest(RootComponentSpec.java:72)\n\tat com.facebook.flipper.sample.RootComponent.hitGetRequest(RootComponent.java:46)\n'; - const crash = parseCrashLog(log, 'iOS', null); + const crash = parseCrashLog(log, 'iOS', undefined); expect(crash.callstack).toEqual(log); expect(crash.reason).toEqual('Cannot figure out the cause'); expect(crash.name).toEqual('Cannot figure out the cause'); @@ -164,7 +165,7 @@ test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState const pluginStateCrash = getCrash(0, 'callstack', 'crash1', 'crash1'); plugin.instance.reportCrash(pluginStateCrash); const content = 'Blaa Blaaa \n Blaa Blaaa'; - plugin.instance.reportCrash(parseCrashLog(content, 'iOS', null)); + plugin.instance.reportCrash(parseCrashLog(content, 'iOS', undefined)); const crashes = plugin.instance.crashes.get(); expect(crashes.length).toEqual(2); assertCrash(crashes[0], pluginStateCrash); @@ -183,7 +184,9 @@ test('test getNewPersistedStateFromCrashLog when os is undefined', () => { const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin); const content = 'Blaa Blaaa \n Blaa Blaaa'; expect(() => { - plugin.instance.reportCrash(parseCrashLog(content, undefined as any, null)); + plugin.instance.reportCrash( + parseCrashLog(content, undefined as any, undefined), + ); }).toThrowErrorMatchingInlineSnapshot(`"Unsupported OS"`); const crashes = plugin.instance.crashes.get(); expect(crashes.length).toEqual(0); diff --git a/desktop/plugins/public/crash_reporter/android-crash-utils.tsx b/desktop/plugins/public/crash_reporter/android-crash-utils.tsx new file mode 100644 index 000000000..4daa8934a --- /dev/null +++ b/desktop/plugins/public/crash_reporter/android-crash-utils.tsx @@ -0,0 +1,55 @@ +/** + * 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} from 'flipper-plugin'; +import type {CrashLog} from './index'; + +export function parseAndroidCrash( + content: string, + fallbackReason: string, + logDate?: Date, +) { + 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; +} + +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') + ); +} diff --git a/desktop/plugins/public/crash_reporter/crash-utils.tsx b/desktop/plugins/public/crash_reporter/crash-utils.tsx new file mode 100644 index 000000000..e5893531b --- /dev/null +++ b/desktop/plugins/public/crash_reporter/crash-utils.tsx @@ -0,0 +1,48 @@ +/** + * 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 unicodeSubstring from 'unicode-substring'; +import type {CrashLog} from './index'; +import {parseAndroidCrash} from './android-crash-utils'; +import {parseIosCrash} from './ios-crash-utils'; + +export const UNKNOWN_CRASH_REASON = 'Cannot figure out the cause'; + +export function parseCrashLog( + content: string, + os: string, + logDate?: Date, +): CrashLog { + const fallbackReason = UNKNOWN_CRASH_REASON; + switch (os) { + case 'iOS': { + return parseIosCrash(content, fallbackReason, logDate); + } + case 'Android': { + return parseAndroidCrash(content, fallbackReason, logDate); + } + default: { + throw new Error('Unsupported OS'); + } + } +} + +export 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 trimCallStackIfPossible(callstack: string): string { + const regex = /Application Specific Information:/; + const query = regex.exec(callstack); + return query ? callstack.substring(0, query.index) : callstack; +} diff --git a/desktop/plugins/public/crash_reporter/index.tsx b/desktop/plugins/public/crash_reporter/index.tsx index c4fd973ea..d79118863 100644 --- a/desktop/plugins/public/crash_reporter/index.tsx +++ b/desktop/plugins/public/crash_reporter/index.tsx @@ -15,27 +15,29 @@ import { ContextMenu, clipboard, Button, - shouldParseAndroidLog, Text, colors, Toolbar, Spacer, Select, } from 'flipper'; -import unicodeSubstring from 'unicode-substring'; -import fs from 'fs'; -import os from 'os'; -import path from 'path'; -import {promisify} from 'util'; -import type {DeviceLogEntry} from 'flipper'; import React from 'react'; import { createState, + DeviceLogEntry, DevicePluginClient, usePlugin, useValue, } from 'flipper-plugin'; import type {FSWatcher} from 'fs'; +import { + parseCrashLog, + trimCallStackIfPossible, + truncate, + UNKNOWN_CRASH_REASON, +} from './crash-utils'; +import {addFileWatcherForiOSCrashLogs} from './ios-crash-utils'; +import {shouldParseAndroidLog} from './android-crash-utils'; type Maybe = T | null | undefined; @@ -176,129 +178,6 @@ const StackTraceContainer = styled(FlexColumn)({ flexShrink: 0, }); -const UNKNOWN_CRASH_REASON = 'Cannot figure out the cause'; - -export function parseCrashLog( - content: string, - os: string, - logDate: Maybe, -): 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 = - 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 { - const regex = /(?<=.*Path: *)[^\n]*/; - const arr = regex.exec(content); - if (!arr || arr.length <= 0) { - return null; - } - const path = arr[0]; - return path.trim(); -} - -function addFileWatcherForiOSCrashLogs( - deviceOs: string, - serial: string, - reportCrash: (payload: CrashLog | Crash) => void, -) { - const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports'); - if (!fs.existsSync(dir)) { - // Directory doesn't exist - return; - } - 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 (err) { - console.warn('Failed to read crash file', err); - return; - } - if (shouldShowiOSCrashNotification(serial, data)) { - reportCrash(parseCrashLog(data, deviceOs, null)); - } - }); - }); - }); -} - class CrashSelector extends React.Component { render() { const {crashes, selectedCrashID, orderedIDs, onCrashChange} = this.props; @@ -632,21 +511,3 @@ export function Component() { ); } - -function trimCallStackIfPossible(callstack: string): string { - const regex = /Application Specific Information:/; - const query = regex.exec(callstack); - return query ? callstack.substring(0, query.index) : callstack; -} - -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; -} diff --git a/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx b/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx new file mode 100644 index 000000000..96da2d257 --- /dev/null +++ b/desktop/plugins/public/crash_reporter/ios-crash-utils.tsx @@ -0,0 +1,104 @@ +/** + * 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 {Crash, CrashLog} from './index'; +import {parseCrashLog} from './crash-utils'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import {promisify} from 'util'; + +export function parseIosCrash( + content: string, + fallbackReason: string, + logDate?: Date, +) { + const regex = /Exception Type: *\w*/; + const arr = regex.exec(content); + const exceptionString = arr ? arr[0] : ''; + const exceptionRegex = /\w*$/; + const tmp = exceptionRegex.exec(exceptionString); + const exception = tmp && tmp[0].length ? tmp[0] : fallbackReason; + + let date = logDate; + if (!date) { + const dateRegex = /Date\/Time: *[\w\s\.:-]*/; + const dateArr = dateRegex.exec(content); + const dateString = dateArr ? dateArr[0] : ''; + const dateRegex2 = /[\w\s\.:-]*$/; + const tmp1 = dateRegex2.exec(dateString); + const extractedDateString: string | null = + 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; +} + +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( + deviceOs: string, + serial: string, + reportCrash: (payload: CrashLog | Crash) => void, +) { + const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports'); + if (!fs.existsSync(dir)) { + // Directory doesn't exist + return; + } + 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 (err) { + console.warn('Failed to read crash file', err); + return; + } + if (shouldShowiOSCrashNotification(serial, data)) { + reportCrash(parseCrashLog(data, deviceOs, undefined)); + } + }); + }); + }); +}