Commit 54899c76 authored by Tim Neumann's avatar Tim Neumann Committed by Chris Bracken

Support multi-line log messages on iOS devices (#17327)

parent d27cd520
......@@ -415,7 +415,10 @@ String decodeSyslog(String line) {
}
class _IOSDeviceLogReader extends DeviceLogReader {
RegExp _lineRegex;
// Matches a syslog line from the runner.
RegExp _runnerLineRegex;
// Matches a syslog line from any app.
RegExp _anyLineRegex;
_IOSDeviceLogReader(this.device, ApplicationPackage app) {
_linesController = new StreamController<String>.broadcast(
......@@ -428,7 +431,11 @@ class _IOSDeviceLogReader extends DeviceLogReader {
// iOS 9 format: Runner[297] <Notice>:
// iOS 10 format: Runner(Flutter)[297] <Notice>:
final String appName = app == null ? '' : app.name.replaceAll('.app', '');
_lineRegex = new RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: ');
_runnerLineRegex = new RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: ');
// Similar to above, but allows ~arbitrary components instead of "Runner"
// and "Flutter". The regex tries to strike a balance between not producing
// false positives and not producing false negatives.
_anyLineRegex = new RegExp(r'\w+(\([^)]*\))?\[\d+\] <[A-Za-z]+>: ');
}
final IOSDevice device;
......@@ -445,8 +452,8 @@ class _IOSDeviceLogReader extends DeviceLogReader {
void _start() {
iMobileDevice.startLogger().then<Null>((Process process) {
_process = process;
_process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(_onLine);
_process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(_onLine);
_process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(_newLineHandler());
_process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(_newLineHandler());
_process.exitCode.whenComplete(() {
if (_linesController.hasListener)
_linesController.close();
......@@ -454,14 +461,35 @@ class _IOSDeviceLogReader extends DeviceLogReader {
});
}
void _onLine(String line) {
final Match match = _lineRegex.firstMatch(line);
// Returns a stateful line handler to properly capture multi-line output.
//
// For multi-line log messages, any line after the first is logged without
// any specific prefix. To properly capture those, we enter "printing" mode
// after matching a log line from the runner. When in printing mode, we print
// all lines until we find the start of another log message (from any app).
Function _newLineHandler() {
bool printing = false;
return (String line) {
if (printing) {
if (!_anyLineRegex.hasMatch(line)) {
_linesController.add(decodeSyslog(line));
return;
}
if (match != null) {
final String logLine = line.substring(match.end);
// Only display the log line after the initial device and executable information.
_linesController.add(decodeSyslog(logLine));
}
printing = false;
}
final Match match = _runnerLineRegex.firstMatch(line);
if (match != null) {
final String logLine = line.substring(match.end);
// Only display the log line after the initial device and executable information.
_linesController.add(decodeSyslog(logLine));
printing = true;
}
};
}
void _stop() {
......
......@@ -125,5 +125,40 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
}, overrides: <Type, Generator>{
IMobileDevice: () => mockIMobileDevice,
});
testUsingContext('includes multi-line Flutter logs in the output', () async {
when(mockIMobileDevice.startLogger()).thenAnswer((Invocation invocation) {
final Process mockProcess = new MockProcess();
when(mockProcess.stdout).thenAnswer((Invocation invocation) =>
new Stream<List<int>>.fromIterable(<List<int>>['''
Runner(Flutter)[297] <Notice>: This is a multi-line message,
with another Flutter message following it.
Runner(Flutter)[297] <Notice>: This is a multi-line message,
with a non-Flutter log message following it.
Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
'''.codeUnits]));
when(mockProcess.stderr)
.thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
// Delay return of exitCode until after stdout stream data, since it terminates the logger.
when(mockProcess.exitCode)
.thenAnswer((Invocation invocation) => new Future<int>.delayed(Duration.zero, () => 0));
return new Future<Process>.value(mockProcess);
});
final IOSDevice device = new IOSDevice('123456');
final DeviceLogReader logReader = device.getLogReader(
app: new BuildableIOSApp(projectBundleId: 'bundleId'),
);
final List<String> lines = await logReader.logLines.toList();
expect(lines, <String>[
'This is a multi-line message,',
' with another Flutter message following it.',
'This is a multi-line message,',
' with a non-Flutter log message following it.',
]);
}, overrides: <Type, Generator>{
IMobileDevice: () => mockIMobileDevice,
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment