Support physical device logging
Summary: Changelog: Logs plugin now supports physical iOS devices As reported in https://github.com/facebook/flipper/issues/262 and linked papercut. This diff adds support for iOS device logs through idb. Since idb doesn't respect `--json` flag at the moment, we perform the parsing in Flipper itself. Reviewed By: passy Differential Revision: D27346262 fbshipit-source-id: 3b314716f48bb9a7fe709370303396a51893359c
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d22e893169
commit
d5fbe9a5b9
@@ -20,6 +20,7 @@ import {promisify} from 'util';
|
||||
import {exec} from 'child_process';
|
||||
import {default as promiseTimeout} from '../utils/promiseTimeout';
|
||||
import {IOSBridge} from '../utils/IOSBridge';
|
||||
import split2 from 'split2';
|
||||
|
||||
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
|
||||
|
||||
@@ -40,6 +41,10 @@ type RawLogEntry = {
|
||||
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;
|
||||
|
||||
export default class IOSDevice extends BaseDevice {
|
||||
log?: child_process.ChildProcessWithoutNullStreams;
|
||||
buffer: string;
|
||||
@@ -97,8 +102,13 @@ export default class IOSDevice extends BaseDevice {
|
||||
}
|
||||
|
||||
const logListener = iOSBridge.startLogListener;
|
||||
if (!this.log && logListener) {
|
||||
this.log = logListener(this.serial);
|
||||
if (
|
||||
!this.log &&
|
||||
logListener &&
|
||||
(this.deviceType === 'emulator' ||
|
||||
(this.deviceType === 'physical' && iOSBridge.idbAvailable))
|
||||
) {
|
||||
this.log = logListener(this.serial, this.deviceType);
|
||||
this.log.on('error', (err: Error) => {
|
||||
console.error('iOS log tailer error', err);
|
||||
});
|
||||
@@ -112,13 +122,24 @@ export default class IOSDevice extends BaseDevice {
|
||||
});
|
||||
|
||||
try {
|
||||
this.log.stdout
|
||||
.pipe(new StripLogPrefix())
|
||||
.pipe(JSONStream.parse('*'))
|
||||
.on('data', (data: RawLogEntry) => {
|
||||
const entry = IOSDevice.parseLogEntry(data);
|
||||
this.addLogEntry(entry);
|
||||
if (this.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
|
||||
@@ -129,15 +150,42 @@ export default class IOSDevice extends BaseDevice {
|
||||
}
|
||||
}
|
||||
|
||||
static parseLogEntry(entry: RawLogEntry): DeviceLogEntry {
|
||||
const LOG_MAPPING: Map<IOSLogLevel, LogLevel> = new Map([
|
||||
['Default' as IOSLogLevel, 'debug' as LogLevel],
|
||||
['Info' as IOSLogLevel, 'info' as LogLevel],
|
||||
['Debug' as IOSLogLevel, 'debug' as LogLevel],
|
||||
['Error' as IOSLogLevel, 'error' as LogLevel],
|
||||
['Fault' as IOSLogLevel, 'fatal' as LogLevel],
|
||||
]);
|
||||
let type: LogLevel = LOG_MAPPING.get(entry.messageType) || 'unknown';
|
||||
static getLogLevel(level: string): LogLevel {
|
||||
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: LogLevel = IOSDevice.getLogLevel(entry.messageType);
|
||||
|
||||
// when Apple log levels are not used, log messages can be prefixed with
|
||||
// their loglevel.
|
||||
|
||||
BIN
desktop/app/src/devices/__tests__/iOSLogs.node.tsx
Normal file
BIN
desktop/app/src/devices/__tests__/iOSLogs.node.tsx
Normal file
Binary file not shown.
@@ -64,7 +64,9 @@ test('test getAllPromisesForQueryingDevices when xcode detected', () => {
|
||||
const promises = getAllPromisesForQueryingDevices(
|
||||
mockStore,
|
||||
logger,
|
||||
{},
|
||||
{
|
||||
idbAvailable: false,
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(promises.length).toEqual(3);
|
||||
@@ -74,7 +76,9 @@ test('test getAllPromisesForQueryingDevices when xcode is not detected', () => {
|
||||
const promises = getAllPromisesForQueryingDevices(
|
||||
mockStore,
|
||||
logger,
|
||||
{},
|
||||
{
|
||||
idbAvailable: true,
|
||||
},
|
||||
false,
|
||||
);
|
||||
expect(promises.length).toEqual(1);
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import child_process from 'child_process';
|
||||
import {DeviceType} from 'flipper-plugin-lib';
|
||||
|
||||
export interface IOSBridge {
|
||||
idbAvailable: boolean;
|
||||
startLogListener?: (
|
||||
udid: string,
|
||||
deviceType: DeviceType,
|
||||
) => child_process.ChildProcessWithoutNullStreams;
|
||||
}
|
||||
|
||||
@@ -26,27 +29,36 @@ async function isAvailable(idbPath: string): Promise<boolean> {
|
||||
.catch((_) => false);
|
||||
}
|
||||
|
||||
const LOG_EXTRA_ARGS = [
|
||||
'--style',
|
||||
'json',
|
||||
'--predicate',
|
||||
'senderImagePath contains "Containers"',
|
||||
'--debug',
|
||||
'--info',
|
||||
];
|
||||
function getLogExtraArgs(deviceType: DeviceType) {
|
||||
if (deviceType === 'physical') {
|
||||
return [
|
||||
// idb has a --json option, but that doesn't actually work!
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'--style',
|
||||
'json',
|
||||
'--predicate',
|
||||
'senderImagePath contains "Containers"',
|
||||
'--debug',
|
||||
'--info',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export function idbStartLogListener(
|
||||
idbPath: string,
|
||||
udid: string,
|
||||
deviceType: DeviceType,
|
||||
): child_process.ChildProcessWithoutNullStreams {
|
||||
return child_process.spawn(
|
||||
idbPath,
|
||||
['log', '--udid', udid, '--', ...LOG_EXTRA_ARGS],
|
||||
['log', '--udid', udid, '--', ...getLogExtraArgs(deviceType)],
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
export function xcrunStartLogListener(udid: string) {
|
||||
export function xcrunStartLogListener(udid: string, deviceType: DeviceType) {
|
||||
const deviceSetPath = process.env.DEVICE_SET_PATH
|
||||
? ['--set', process.env.DEVICE_SET_PATH]
|
||||
: [];
|
||||
@@ -59,7 +71,7 @@ export function xcrunStartLogListener(udid: string) {
|
||||
udid,
|
||||
'log',
|
||||
'stream',
|
||||
...LOG_EXTRA_ARGS,
|
||||
...getLogExtraArgs(deviceType),
|
||||
],
|
||||
{},
|
||||
);
|
||||
@@ -70,18 +82,23 @@ export async function makeIOSBridge(
|
||||
isXcodeDetected: boolean,
|
||||
isAvailableFn: (idbPath: string) => Promise<boolean> = isAvailable,
|
||||
): Promise<IOSBridge> {
|
||||
if (!isXcodeDetected) {
|
||||
// iOS Physical Device can still get detected without Xcode. In this case there is no way to setup log listener yet.
|
||||
// This will not be the case, idb team is working on making idb log work without XCode atleast for physical device.
|
||||
return {};
|
||||
}
|
||||
// prefer idb
|
||||
if (await isAvailableFn(idbPath)) {
|
||||
return {
|
||||
idbAvailable: true,
|
||||
startLogListener: idbStartLogListener.bind(null, idbPath),
|
||||
};
|
||||
}
|
||||
|
||||
// no idb, if it's a simulator and xcode is available, we can use xcrun
|
||||
if (isXcodeDetected) {
|
||||
return {
|
||||
idbAvailable: false,
|
||||
startLogListener: xcrunStartLogListener,
|
||||
};
|
||||
}
|
||||
// no idb, and not a simulator, we can't log this device
|
||||
return {
|
||||
startLogListener: xcrunStartLogListener,
|
||||
idbAvailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ test('uses xcrun with no idb when xcode is detected', async () => {
|
||||
const ib = await makeIOSBridge('', true);
|
||||
expect(ib.startLogListener).toBeDefined();
|
||||
|
||||
ib.startLogListener!('deadbeef');
|
||||
ib.startLogListener!('deadbeef', 'emulator');
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'xcrun',
|
||||
@@ -43,7 +43,7 @@ test('uses idb when present and xcode detected', async () => {
|
||||
const ib = await makeIOSBridge('/usr/local/bin/idb', true, async (_) => true);
|
||||
expect(ib.startLogListener).toBeDefined();
|
||||
|
||||
ib.startLogListener!('deadbeef');
|
||||
ib.startLogListener!('deadbeef', 'emulator');
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'/usr/local/bin/idb',
|
||||
@@ -63,6 +63,31 @@ test('uses idb when present and xcode detected', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('uses idb when present and xcode detected and physical device connected', async () => {
|
||||
const ib = await makeIOSBridge('/usr/local/bin/idb', true, async (_) => true);
|
||||
expect(ib.startLogListener).toBeDefined();
|
||||
|
||||
ib.startLogListener!('deadbeef', 'physical');
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'/usr/local/bin/idb',
|
||||
[
|
||||
'log',
|
||||
'--udid',
|
||||
'deadbeef',
|
||||
'--',
|
||||
// no further args; not supported by idb atm
|
||||
],
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
test("without idb physical devices can't log", async () => {
|
||||
const ib = await makeIOSBridge('', true);
|
||||
expect(ib.idbAvailable).toBeFalsy();
|
||||
expect(ib.startLogListener).toBeDefined(); // since we have xcode
|
||||
});
|
||||
|
||||
test('uses no log listener when xcode is not detected', async () => {
|
||||
const ib = await makeIOSBridge('', false);
|
||||
expect(ib.startLogListener).toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user