log listener

Summary:
The logs plugin opened a new log connection every time it was activated and never closed the connection.

This is now changed. Once a device is connected, a log connection is opened. The logs plugin subscribes and unsubscribes to this connection. This allows the logs plugin it even access the logs from when it was not activated and ensures to only open on connection to read the logs. Logs are persisted when switching away from the plugin.

Also removes the spinner from the logs plugin, as it loads much faster now.

Reviewed By: jknoxville

Differential Revision: D9613054

fbshipit-source-id: e37ea56c563450e7fc4e3c85a015292be1f2dbfc
This commit is contained in:
Daniel Büchele
2018-08-31 10:02:51 -07:00
committed by Facebook Github Bot
parent a30e0b53e9
commit afdc846a8b
7 changed files with 245 additions and 237 deletions

View File

@@ -8,6 +8,7 @@ import type {SonarPlugin, SonarBasePlugin} from './plugin.js';
import type LogManager from './fb-stubs/Logger'; import type LogManager from './fb-stubs/Logger';
import type Client from './Client.js'; import type Client from './Client.js';
import type BaseDevice from './devices/BaseDevice.js'; import type BaseDevice from './devices/BaseDevice.js';
import type {Props as PluginProps} from './plugin';
import {SonarDevicePlugin} from './plugin.js'; import {SonarDevicePlugin} from './plugin.js';
import { import {
@@ -43,7 +44,9 @@ type Props = {
selectedDevice: BaseDevice, selectedDevice: BaseDevice,
selectedPlugin: ?string, selectedPlugin: ?string,
selectedApp: ?string, selectedApp: ?string,
pluginStates: Object, pluginStates: {
[pluginKey: string]: Object,
},
clients: Array<Client>, clients: Array<Client>,
setPluginState: (payload: { setPluginState: (payload: {
pluginKey: string, pluginKey: string,
@@ -128,6 +131,15 @@ class PluginContainer extends Component<Props, State> {
return null; return null;
} }
const props: PluginProps<Object> = {
key: pluginKey,
logger: this.props.logger,
persistedState: pluginStates[pluginKey] || {},
setPersistedState: state => setPluginState({pluginKey, state}),
target,
ref: this.refChanged,
};
return ( return (
<React.Fragment> <React.Fragment>
<Container key="plugin"> <Container key="plugin">
@@ -136,14 +148,7 @@ class PluginContainer extends Component<Props, State> {
activePlugin.title activePlugin.title
}" encountered an error during render`} }" encountered an error during render`}
logger={this.props.logger}> logger={this.props.logger}>
{React.createElement(activePlugin, { {React.createElement(activePlugin, props)}
key: pluginKey,
logger: this.props.logger,
persistedState: pluginStates[pluginKey] || {},
setPersistedState: state => setPluginState({pluginKey, state}),
target,
ref: this.refChanged,
})}
</ErrorBoundary> </ErrorBoundary>
</Container> </Container>
<SidebarContainer id="sonarSidebar" /> <SidebarContainer id="sonarSidebar" />

View File

@@ -13,14 +13,13 @@ import type {
} from 'sonar'; } from 'sonar';
import type {Counter} from './LogWatcher.js'; import type {Counter} from './LogWatcher.js';
import type {DeviceLogEntry} from '../../devices/BaseDevice.js'; import type {DeviceLogEntry} from '../../devices/BaseDevice.js';
import type {Props as PluginProps} from '../../plugin';
import { import {
Text, Text,
ManagedTable, ManagedTable,
Button, Button,
colors, colors,
FlexCenter,
LoadingIndicator,
ContextMenu, ContextMenu,
FlexColumn, FlexColumn,
Glyph, Glyph,
@@ -39,8 +38,7 @@ type Entries = Array<{
entry: DeviceLogEntry, entry: DeviceLogEntry,
}>; }>;
type LogsState = {| type State = {|
initialising: boolean,
rows: Array<TableBodyRow>, rows: Array<TableBodyRow>,
entries: Entries, entries: Entries,
key2entry: {[key: string]: DeviceLogEntry}, key2entry: {[key: string]: DeviceLogEntry},
@@ -48,6 +46,10 @@ type LogsState = {|
counters: Array<Counter>, counters: Array<Counter>,
|}; |};
type Actions = {||};
type PersistedState = {||};
const Icon = styled(Glyph)({ const Icon = styled(Glyph)({
marginTop: 5, marginTop: 5,
}); });
@@ -234,7 +236,11 @@ function pad(chunk: mixed, len: number): string {
return str; return str;
} }
export default class LogTable extends SonarDevicePlugin<LogsState> { export default class LogTable extends SonarDevicePlugin<
State,
Actions,
PersistedState,
> {
static id = 'DeviceLogs'; static id = 'DeviceLogs';
static title = 'Logs'; static title = 'Logs';
static icon = 'arrow-right'; static icon = 'arrow-right';
@@ -267,7 +273,6 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
rows: [], rows: [],
entries: [], entries: [],
key2entry: {}, key2entry: {},
initialising: true,
highlightedRows: [], highlightedRows: [],
counters: this.restoreSavedCounters(), counters: this.restoreSavedCounters(),
}; };
@@ -276,20 +281,24 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
columns: TableColumns; columns: TableColumns;
columnSizes: TableColumnSizes; columnSizes: TableColumnSizes;
columnOrder: TableColumnOrder; columnOrder: TableColumnOrder;
logListener: ?Symbol;
init() { batch: Entries = [];
let batch: Entries = []; queued: boolean = false;
let queued = false; counter: number = 0;
let counter = 0;
constructor(props: PluginProps<PersistedState>) {
super(props);
const supportedColumns = this.device.supportedColumns(); const supportedColumns = this.device.supportedColumns();
this.columns = keepKeys(COLUMNS, supportedColumns); this.columns = keepKeys(COLUMNS, supportedColumns);
this.columnSizes = keepKeys(COLUMN_SIZE, supportedColumns); this.columnSizes = keepKeys(COLUMN_SIZE, supportedColumns);
this.columnOrder = INITIAL_COLUMN_ORDER.filter(obj => this.columnOrder = INITIAL_COLUMN_ORDER.filter(obj =>
supportedColumns.includes(obj.key), supportedColumns.includes(obj.key),
); );
this.logListener = this.device.addLogListener(this.processEntry);
}
this.device.addLogListener((entry: DeviceLogEntry) => { processEntry = (entry: DeviceLogEntry) => {
const {icon, style} = LOG_TYPES[(entry.type: string)] || LOG_TYPES.debug; const {icon, style} = LOG_TYPES[(entry.type: string)] || LOG_TYPES.debug;
// clean message // clean message
@@ -338,9 +347,7 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
value: <HiddenScrollText code={true}>{message}</HiddenScrollText>, value: <HiddenScrollText code={true}>{message}</HiddenScrollText>,
}, },
tag: { tag: {
value: ( value: <HiddenScrollText code={true}>{entry.tag}</HiddenScrollText>,
<HiddenScrollText code={true}>{entry.tag}</HiddenScrollText>
),
isFilterable: true, isFilterable: true,
}, },
pid: { pid: {
@@ -360,9 +367,7 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
isFilterable: true, isFilterable: true,
}, },
app: { app: {
value: ( value: <HiddenScrollText code={true}>{entry.app}</HiddenScrollText>,
<HiddenScrollText code={true}>{entry.app}</HiddenScrollText>
),
isFilterable: true, isFilterable: true,
}, },
}, },
@@ -370,22 +375,22 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
style, style,
type: entry.type, type: entry.type,
filterValue: entry.message, filterValue: entry.message,
key: String(counter++), key: String(this.counter++),
}, },
}; };
// batch up logs to be processed every 250ms, if we have lots of log // batch up logs to be processed every 250ms, if we have lots of log
// messages coming in, then calling an setState 200+ times is actually // messages coming in, then calling an setState 200+ times is actually
// pretty expensive // pretty expensive
batch.push(item); this.batch.push(item);
if (!queued) { if (!this.queued) {
queued = true; this.queued = true;
this.batchTimer = setTimeout(() => { this.batchTimer = setTimeout(() => {
const thisBatch = batch; const thisBatch = this.batch;
batch = []; this.batch = [];
queued = false; this.queued = false;
// update rows/entries // update rows/entries
this.setState(state => { this.setState(state => {
@@ -417,22 +422,16 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
}); });
}, 100); }, 100);
} }
}); };
this.initTimer = setTimeout(() => {
this.setState({
initialising: false,
});
}, 2000);
}
componentWillUnmount() { componentWillUnmount() {
if (this.initTimer) {
clearTimeout(this.initTimer);
}
if (this.batchTimer) { if (this.batchTimer) {
clearTimeout(this.batchTimer); clearTimeout(this.batchTimer);
} }
if (this.logListener) {
this.device.removeLogListener(this.logListener);
}
} }
addRowIfNeeded( addRowIfNeeded(
@@ -536,7 +535,7 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
}); });
render() { render() {
const {initialising, rows} = this.state; const {rows} = this.state;
const contextMenuItems = [ const contextMenuItems = [
{ {
@@ -547,11 +546,7 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
click: this.clearLogs, click: this.clearLogs,
}, },
]; ];
return initialising ? ( return (
<FlexCenter fill={true}>
<LoadingIndicator />
</FlexCenter>
) : (
<LogTable.ContextMenu items={contextMenuItems} component={FlexColumn}> <LogTable.ContextMenu items={contextMenuItems} component={FlexColumn}>
<SearchableTable <SearchableTable
innerRef={this.setTableRef} innerRef={this.setTableRef}

View File

@@ -5,7 +5,7 @@
* @format * @format
*/ */
import type {DeviceType, DeviceShell, DeviceLogListener} from './BaseDevice.js'; import type {DeviceType, DeviceShell} from './BaseDevice.js';
import {Priority} from 'adbkit-logcat-fb'; import {Priority} from 'adbkit-logcat-fb';
import child_process from 'child_process'; import child_process from 'child_process';
@@ -25,27 +25,10 @@ export default class AndroidDevice extends BaseDevice {
if (deviceType == 'physical') { if (deviceType == 'physical') {
this.supportedPlugins.push('DeviceCPU'); this.supportedPlugins.push('DeviceCPU');
} }
}
supportedPlugins = [
'DeviceLogs',
'DeviceShell',
'DeviceFiles',
'DeviceScreen',
];
icon = 'icons/android.svg';
os = 'Android';
adb: ADBClient;
pidAppMapping: {[key: number]: string} = {};
logReader: any;
supportedColumns(): Array<string> {
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
}
addLogListener(callback: DeviceLogListener) {
this.adb.openLogcat(this.serial).then(reader => { this.adb.openLogcat(this.serial).then(reader => {
reader.on('entry', async entry => { reader.on('entry', entry => {
if (this.logListeners.size > 0) {
let type = 'unknown'; let type = 'unknown';
if (entry.priority === Priority.VERBOSE) { if (entry.priority === Priority.VERBOSE) {
type = 'verbose'; type = 'verbose';
@@ -66,7 +49,7 @@ export default class AndroidDevice extends BaseDevice {
type = 'fatal'; type = 'fatal';
} }
callback({ this.notifyLogListeners({
tag: entry.tag, tag: entry.tag,
pid: entry.pid, pid: entry.pid,
tid: entry.tid, tid: entry.tid,
@@ -74,10 +57,27 @@ export default class AndroidDevice extends BaseDevice {
date: entry.date, date: entry.date,
type, type,
}); });
}
}); });
}); });
} }
supportedPlugins = [
'DeviceLogs',
'DeviceShell',
'DeviceFiles',
'DeviceScreen',
];
icon = 'icons/android.svg';
os = 'Android';
adb: ADBClient;
pidAppMapping: {[key: number]: string} = {};
logReader: any;
supportedColumns(): Array<string> {
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
}
reverse(): Promise<void> { reverse(): Promise<void> {
if (this.deviceType === 'physical') { if (this.deviceType === 'physical') {
return this.adb return this.adb

View File

@@ -62,6 +62,9 @@ export default class BaseDevice {
// possible src of icon to display next to the device title // possible src of icon to display next to the device title
icon: ?string; icon: ?string;
logListeners: Map<Symbol, DeviceLogListener> = new Map();
logEntries: Array<DeviceLogEntry> = [];
supportsOS(os: string) { supportsOS(os: string) {
return os.toLowerCase() === this.os.toLowerCase(); return os.toLowerCase() === this.os.toLowerCase();
} }
@@ -80,8 +83,22 @@ export default class BaseDevice {
throw new Error('unimplemented'); throw new Error('unimplemented');
} }
addLogListener(listener: DeviceLogListener) { addLogListener(callback: DeviceLogListener): Symbol {
throw new Error('unimplemented'); const id = Symbol();
this.logListeners.set(id, callback);
this.logEntries.map(callback);
return id;
}
notifyLogListeners(entry: DeviceLogEntry) {
this.logEntries.push(entry);
if (this.logListeners.size > 0) {
this.logListeners.forEach(listener => listener(entry));
}
}
removeLogListener(id: Symbol) {
this.logListeners.delete(id);
} }
spawnShell(): DeviceShell { spawnShell(): DeviceShell {

View File

@@ -5,12 +5,7 @@
* @format * @format
*/ */
import type { import type {DeviceType, LogLevel, DeviceLogEntry} from './BaseDevice.js';
DeviceType,
LogLevel,
DeviceLogEntry,
DeviceLogListener,
} from './BaseDevice.js';
import child_process from 'child_process'; import child_process from 'child_process';
import BaseDevice from './BaseDevice.js'; import BaseDevice from './BaseDevice.js';
import JSONStream from 'JSONStream'; import JSONStream from 'JSONStream';
@@ -47,7 +42,7 @@ export default class IOSDevice extends BaseDevice {
super(serial, deviceType, title); super(serial, deviceType, title);
this.buffer = ''; this.buffer = '';
this.log = null; this.log = this.startLogListener();
} }
teardown() { teardown() {
@@ -60,7 +55,7 @@ export default class IOSDevice extends BaseDevice {
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time']; return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
} }
addLogListener(callback: DeviceLogListener, retries: number = 3) { startLogListener(retries: number = 3) {
if (retries === 0) { if (retries === 0) {
console.error('Attaching iOS log listener continuously failed.'); console.error('Attaching iOS log listener continuously failed.');
return; return;
@@ -102,14 +97,15 @@ export default class IOSDevice extends BaseDevice {
.pipe(new StripLogPrefix()) .pipe(new StripLogPrefix())
.pipe(JSONStream.parse('*')) .pipe(JSONStream.parse('*'))
.on('data', (data: RawLogEntry) => { .on('data', (data: RawLogEntry) => {
callback(IOSDevice.parseLogEntry(data)); const entry = IOSDevice.parseLogEntry(data);
this.notifyLogListeners(entry);
}); });
} catch (e) { } catch (e) {
console.error('Could not parse iOS log stream.', e); console.error('Could not parse iOS log stream.', e);
// restart log stream // restart log stream
this.log.kill(); this.log.kill();
this.log = null; this.log = null;
this.addLogListener(callback, retries - 1); this.startLogListener(retries - 1);
} }
} }

View File

@@ -5,11 +5,7 @@
* @format * @format
*/ */
import type { import type {DeviceType, DeviceLogEntry} from './BaseDevice.js';
DeviceType,
DeviceLogEntry,
DeviceLogListener,
} from './BaseDevice.js';
import fs from 'fs-extra'; import fs from 'fs-extra';
import os from 'os'; import os from 'os';
@@ -40,6 +36,8 @@ export default class OculusDevice extends BaseDevice {
this.watcher = null; this.watcher = null;
this.processedFileMap = {}; this.processedFileMap = {};
this.setupListener();
} }
teardown() { teardown() {
@@ -69,63 +67,63 @@ export default class OculusDevice extends BaseDevice {
} }
} }
processText(text: Buffer, callback: DeviceLogListener) { processText(text: Buffer) {
text text
.toString() .toString()
.split('\r\n') .split('\r\n')
.forEach(line => { .forEach(line => {
const regex = /(.*){(\S+)}\s*\[([\w :.\\]+)\](.*)/; const regex = /(.*){(\S+)}\s*\[([\w :.\\]+)\](.*)/;
const match = regex.exec(line); const match = regex.exec(line);
let entry;
if (match && match.length === 5) { if (match && match.length === 5) {
callback({ entry = {
tid: 0, tid: 0,
pid: 0, pid: 0,
date: new Date(Date.parse(match[1])), date: new Date(Date.parse(match[1])),
type: this.mapLogLevel(match[2]), type: this.mapLogLevel(match[2]),
tag: match[3], tag: match[3],
message: match[4], message: match[4],
}); };
} else if (line.trim() === '') { } else if (line.trim() === '') {
// skip // skip
} else { } else {
callback({ entry = {
tid: 0, tid: 0,
pid: 0, pid: 0,
date: new Date(), date: new Date(),
type: 'verbose', type: 'verbose',
tag: 'failed-parse', tag: 'failed-parse',
message: line, message: line,
});
}
});
}
addLogListener = (callback: DeviceLogListener) => {
this.setupListener(callback);
}; };
}
if (entry) {
this.notifyLogListeners(entry);
}
});
}
async setupListener(callback: DeviceLogListener) { async setupListener() {
const files = await fs.readdir(getLogsPath()); const files = await fs.readdir(getLogsPath());
this.watchedFile = files this.watchedFile = files
.filter(file => file.startsWith('Service_')) .filter(file => file.startsWith('Service_'))
.sort() .sort()
.pop(); .pop();
this.watch(callback); this.watch();
this.timer = setTimeout(() => this.checkForNewLog(callback), 5000); this.timer = setTimeout(() => this.checkForNewLog(), 5000);
} }
watch(callback: DeviceLogListener) { watch() {
const filePath = getLogsPath(this.watchedFile); const filePath = getLogsPath(this.watchedFile);
fs.watchFile(filePath, async (current, previous) => { fs.watchFile(filePath, async (current, previous) => {
const readLen = current.size - previous.size; const readLen = current.size - previous.size;
const buffer = new Buffer(readLen); const buffer = new Buffer(readLen);
const fd = await fs.open(filePath, 'r'); const fd = await fs.open(filePath, 'r');
await fs.read(fd, buffer, 0, readLen, previous.size); await fs.read(fd, buffer, 0, readLen, previous.size);
this.processText(buffer, callback); this.processText(buffer);
}); });
} }
async checkForNewLog(callback: DeviceLogListener) { async checkForNewLog() {
const files = await fs.readdir(getLogsPath()); const files = await fs.readdir(getLogsPath());
const latestLog = files const latestLog = files
.filter(file => file.startsWith('Service_')) .filter(file => file.startsWith('Service_'))
@@ -135,8 +133,8 @@ export default class OculusDevice extends BaseDevice {
const oldFilePath = getLogsPath(this.watchedFile); const oldFilePath = getLogsPath(this.watchedFile);
fs.unwatchFile(oldFilePath); fs.unwatchFile(oldFilePath);
this.watchedFile = latestLog; this.watchedFile = latestLog;
this.watch(callback); this.watch();
} }
this.timer = setTimeout(() => this.checkForNewLog(callback), 5000); this.timer = setTimeout(() => this.checkForNewLog(), 5000);
} }
} }

View File

@@ -5,7 +5,6 @@
* @format * @format
*/ */
import type {DeviceLogListener} from './BaseDevice.js';
import BaseDevice from './BaseDevice.js'; import BaseDevice from './BaseDevice.js';
export default class WindowsDevice extends BaseDevice { export default class WindowsDevice extends BaseDevice {
@@ -22,6 +21,4 @@ export default class WindowsDevice extends BaseDevice {
supportedColumns(): Array<string> { supportedColumns(): Array<string> {
return []; return [];
} }
addLogListener(_callback: DeviceLogListener) {}
} }