Allow to start only one instance of log listener and crash watcher

Summary:
Changelog: Allow only a single crash watcher and a single log listener per device. Start log listener and crash watcher for every device upon connection. Remove commands to start/stop them externally.

Monitored CPU load for a physical Android device with the log listener on and off. Did not notice any real difference.

Resolved crashing adbkit-logcat by forcing the usage of 2.0.1. A proper fix would be to unify babel transforms for browser flipper and electron flipper, but we might re-think how we distribute flipper in the next half, so a simple hot fix might be a better use of time and resources.

Reviewed By: mweststrate

Differential Revision: D33132506

fbshipit-source-id: 39d422682a10a64830ac516e30f43f32f416819d
This commit is contained in:
Andrey Goncharov
2021-12-20 11:37:25 -08:00
committed by Facebook GitHub Bot
parent 731749b41f
commit debf872806
19 changed files with 838 additions and 424 deletions

View File

@@ -7,43 +7,13 @@
* @format
*/
import {
DeviceLogLevel,
DeviceLogEntry,
DeviceType,
timeout,
} from 'flipper-common';
import {DeviceType, timeout} from 'flipper-common';
import child_process, {ChildProcess} from 'child_process';
import JSONStream from 'JSONStream';
import {Transform} from 'stream';
import {ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB, IOSBridge} from './IOSBridge';
import split2 from 'split2';
import {IOSBridge} from './IOSBridge';
import {ServerDevice} from '../ServerDevice';
import {FlipperServerImpl} from '../../FlipperServerImpl';
import {addFileWatcherForiOSCrashLogs} from './iOSCrashUtils';
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
type RawLogEntry = {
eventMessage: string;
machTimestamp: number;
messageType: IOSLogLevel;
processID: number;
processImagePath: string;
processImageUUID: string;
processUniqueID: number;
senderImagePath: string;
senderImageUUID: string;
senderProgramCounter: number;
threadID: number;
timestamp: string;
timezoneName: string;
traceID: string;
};
// https://regex101.com/r/rrl03T/1
// Mar 25 17:06:38 iPhone symptomsd(SymptomEvaluator)[125] <Notice>: Stuff
const logRegex = /(^.{15}) ([^ ]+?) ([^\[]+?)\[(\d+?)\] <(\w+?)>: (.*)$/s;
import {iOSCrashWatcher} from './iOSCrashUtils';
import {iOSLogListener} from './iOSLogListener';
export default class IOSDevice extends ServerDevice {
log?: child_process.ChildProcessWithoutNullStreams;
@@ -51,6 +21,8 @@ export default class IOSDevice extends ServerDevice {
private recordingProcess?: ChildProcess;
private recordingLocation?: string;
private iOSBridge: IOSBridge;
readonly logListener: iOSLogListener;
readonly crashWatcher: iOSCrashWatcher;
constructor(
flipperServer: FlipperServerImpl,
@@ -68,6 +40,27 @@ export default class IOSDevice extends ServerDevice {
});
this.buffer = '';
this.iOSBridge = iOSBridge;
this.logListener = new iOSLogListener(
() => this.connected,
(logEntry) => this.addLogEntry(logEntry),
this.iOSBridge,
this.serial,
this.info.deviceType,
);
// It is OK not to await the start of the log listener. We just spawn it and handle errors internally.
this.logListener
.start()
.catch((e) =>
console.error('IOSDevice.logListener.start -> unexpected error', e),
);
this.crashWatcher = new iOSCrashWatcher(this);
// It is OK not to await the start of the crash watcher. We just spawn it and handle errors internally.
this.crashWatcher
.start()
.catch((e) =>
console.error('IOSDevice.crashWatcher.start -> unexpected error', e),
);
}
async screenshot(): Promise<Buffer> {
@@ -84,146 +77,6 @@ export default class IOSDevice extends ServerDevice {
});
}
startLogging() {
this.startLogListener(this.iOSBridge);
}
stopLogging() {
this.log?.kill();
}
startLogListener(iOSBridge: IOSBridge, retries: number = 3) {
if (retries === 0) {
console.warn('Attaching iOS log listener continuously failed.');
return;
}
if (!this.log) {
try {
this.log = iOSBridge.startLogListener(
this.serial,
this.info.deviceType,
);
} catch (e) {
if (e.message === ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB) {
console.warn(e);
} else {
console.error('Failed to initialise device logs:', e);
this.startLogListener(iOSBridge, retries - 1);
}
return;
}
this.log.on('error', (err: Error) => {
console.error('iOS log tailer error', err);
});
this.log.stderr.on('data', (data: Buffer) => {
console.warn('iOS log tailer stderr: ', data.toString());
});
this.log.on('exit', () => {
this.log = undefined;
});
try {
if (this.info.deviceType === 'physical') {
this.log.stdout.pipe(split2('\0')).on('data', (line: string) => {
const parsed = IOSDevice.parseLogLine(line);
if (parsed) {
this.addLogEntry(parsed);
} else {
console.warn('Failed to parse iOS log line: ', line);
}
});
} else {
this.log.stdout
.pipe(new StripLogPrefix())
.pipe(JSONStream.parse('*'))
.on('data', (data: RawLogEntry) => {
const entry = IOSDevice.parseJsonLogEntry(data);
this.addLogEntry(entry);
});
}
} catch (e) {
console.error('Could not parse iOS log stream.', e);
// restart log stream
this.log.kill();
this.log = undefined;
this.startLogListener(iOSBridge, retries - 1);
}
}
}
protected startCrashWatcherImpl(): () => void {
return addFileWatcherForiOSCrashLogs(this);
}
static getLogLevel(level: string): DeviceLogLevel {
switch (level) {
case 'Default':
return 'debug';
case 'Info':
return 'info';
case 'Debug':
return 'debug';
case 'Error':
return 'error';
case 'Notice':
return 'verbose';
case 'Fault':
return 'fatal';
default:
return 'unknown';
}
}
static parseLogLine(line: string): DeviceLogEntry | undefined {
const matches = line.match(logRegex);
if (matches) {
return {
date: new Date(Date.parse(matches[1])),
tag: matches[3],
tid: 0,
pid: parseInt(matches[4], 10),
type: IOSDevice.getLogLevel(matches[5]),
message: matches[6],
};
}
return undefined;
}
static parseJsonLogEntry(entry: RawLogEntry): DeviceLogEntry {
let type: DeviceLogLevel = IOSDevice.getLogLevel(entry.messageType);
// when Apple log levels are not used, log messages can be prefixed with
// their loglevel.
if (entry.eventMessage.startsWith('[debug]')) {
type = 'debug';
} else if (entry.eventMessage.startsWith('[info]')) {
type = 'info';
} else if (entry.eventMessage.startsWith('[warn]')) {
type = 'warn';
} else if (entry.eventMessage.startsWith('[error]')) {
type = 'error';
}
// remove type from mesage
entry.eventMessage = entry.eventMessage.replace(
/^\[(debug|info|warn|error)\]/,
'',
);
const tag = entry.processImagePath.split('/').pop() || '';
return {
date: new Date(entry.timestamp),
pid: entry.processID,
tid: entry.threadID,
tag,
message: entry.eventMessage,
type,
};
}
async screenCaptureAvailable() {
return this.info.deviceType === 'emulator' && this.connected;
}
@@ -275,27 +128,3 @@ export default class IOSDevice extends ServerDevice {
super.disconnect();
}
}
// Used to strip the initial output of the logging utility where it prints out settings.
// We know the log stream is json so it starts with an open brace.
class StripLogPrefix extends Transform {
passedPrefix = false;
_transform(
data: any,
_encoding: string,
callback: (err?: Error, data?: any) => void,
) {
if (this.passedPrefix) {
this.push(data);
} else {
const dataString = data.toString();
const index = dataString.indexOf('[');
if (index >= 0) {
this.push(dataString.substring(index));
this.passedPrefix = true;
}
}
callback();
}
}