Summary:
Logs were not collected in headless mode, because there was no subscriber listening to the logs. Now they are always stored, even if there is no subscriber. Actually this makes more sense even for the desktop UI, as subscribers could subscribe later.
The only reason this was working on the desktop app was because the log plugin automatically subscribed on launch.
This brings us to the actual question: If a message is logged in a forest and no one is around to read it, is it actually logged? 🤯
Reviewed By: passy
Differential Revision: D14149691
fbshipit-source-id: 212f1b0a69bd0cc8ae0ba3592f29ca90b7a5a475
182 lines
4.4 KiB
JavaScript
182 lines
4.4 KiB
JavaScript
/**
|
|
* Copyright 2018-present Facebook.
|
|
* 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 {DeviceType, LogLevel, DeviceLogEntry} from './BaseDevice.js';
|
|
import child_process from 'child_process';
|
|
import BaseDevice from './BaseDevice.js';
|
|
import JSONStream from 'JSONStream';
|
|
import {Transform} from 'stream';
|
|
|
|
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,
|
|
|};
|
|
|
|
export default class IOSDevice extends BaseDevice {
|
|
icon = 'icons/ios.svg';
|
|
os = 'iOS';
|
|
|
|
log: any;
|
|
buffer: string;
|
|
|
|
constructor(serial: string, deviceType: DeviceType, title: string) {
|
|
super(serial, deviceType, title);
|
|
|
|
this.buffer = '';
|
|
this.log = this.startLogListener();
|
|
}
|
|
|
|
teardown() {
|
|
if (this.log) {
|
|
this.log.kill();
|
|
}
|
|
}
|
|
|
|
supportedColumns(): Array<string> {
|
|
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
|
|
}
|
|
|
|
startLogListener(retries: number = 3) {
|
|
if (this.deviceType === 'physical') {
|
|
return;
|
|
}
|
|
if (retries === 0) {
|
|
console.error('Attaching iOS log listener continuously failed.');
|
|
return;
|
|
}
|
|
if (!this.log) {
|
|
const deviceSetPath = process.env.DEVICE_SET_PATH
|
|
? ['--set', process.env.DEVICE_SET_PATH]
|
|
: [];
|
|
|
|
this.log = child_process.spawn(
|
|
'xcrun',
|
|
[
|
|
'simctl',
|
|
...deviceSetPath,
|
|
'spawn',
|
|
'booted',
|
|
'log',
|
|
'stream',
|
|
'--style',
|
|
'json',
|
|
'--predicate',
|
|
'senderImagePath contains "Containers"',
|
|
'--info',
|
|
'--debug',
|
|
],
|
|
{},
|
|
);
|
|
|
|
this.log.on('error', err => {
|
|
console.error(err);
|
|
});
|
|
|
|
this.log.stderr.on('data', data => {
|
|
console.error(data.toString());
|
|
});
|
|
|
|
this.log.on('exit', () => {
|
|
this.log = null;
|
|
});
|
|
}
|
|
|
|
try {
|
|
this.log.stdout
|
|
.pipe(new StripLogPrefix())
|
|
.pipe(JSONStream.parse('*'))
|
|
.on('data', (data: RawLogEntry) => {
|
|
const entry = IOSDevice.parseLogEntry(data);
|
|
this.addLogEntry(entry);
|
|
});
|
|
} catch (e) {
|
|
console.error('Could not parse iOS log stream.', e);
|
|
// restart log stream
|
|
this.log.kill();
|
|
this.log = null;
|
|
this.startLogListener(retries - 1);
|
|
}
|
|
}
|
|
|
|
static parseLogEntry(entry: RawLogEntry): DeviceLogEntry {
|
|
const LOG_MAPPING: Map<IOSLogLevel, LogLevel> = new Map([
|
|
['Default', 'debug'],
|
|
['Info', 'info'],
|
|
['Debug', 'debug'],
|
|
['Error', 'error'],
|
|
['Fault', 'fatal'],
|
|
]);
|
|
let type: LogLevel = LOG_MAPPING.get(entry.messageType) || 'unknown';
|
|
|
|
// 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,
|
|
};
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|