Refactor DeviceLogReader
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index a364ee4..0189014 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
@@ -49,6 +50,8 @@
bool get isLocalEmulator => false;
+ _AdbLogReader _logReader;
+
List<String> adbCommandForDevice(List<String> args) {
return <String>[androidSdk.adbPath, '-s', id]..addAll(args);
}
@@ -283,7 +286,12 @@
runSync(adbCommandForDevice(<String>['-s', id, 'logcat', '-c']));
}
- DeviceLogReader createLogReader() => new _AdbLogReader(this);
+ DeviceLogReader get logReader {
+ if (_logReader == null)
+ _logReader = new _AdbLogReader(this);
+
+ return _logReader;
+ }
void startTracing(AndroidApk apk) {
runCheckedSync(adbCommandForDevice(<String>[
@@ -460,26 +468,76 @@
final AndroidDevice device;
+ final StreamController<String> _linesStreamController =
+ new StreamController<String>.broadcast();
+
+ Process _process;
+ StreamSubscription _stdoutSubscription;
+ StreamSubscription _stderrSubscription;
+
+ Stream<String> get lines => _linesStreamController.stream;
+
String get name => device.name;
- Future<int> logs({ bool clear: false, bool showPrefix: false }) async {
- if (clear)
- device.clearLogs();
+ bool get isReading => _process != null;
- return await runCommandAndStreamOutput(device.adbCommandForDevice(<String>[
- '-s',
- device.id,
- 'logcat',
- '-v',
- 'tag', // Only log the tag and the message
- '-T',
- device.lastLogcatTimestamp,
- '-s',
- 'flutter:V',
- 'ActivityManager:W',
- 'System.err:W',
- '*:F',
- ]), prefix: showPrefix ? '[$name] ' : '');
+ Future get finished =>
+ _process != null ? _process.exitCode : new Future.value(0);
+
+ Future start() async {
+ if (_process != null) {
+ throw new StateError(
+ '_AdbLogReader must be stopped before it can be started.');
+ }
+
+ // Start the adb logcat process.
+ _process = await runCommand(device.adbCommandForDevice(
+ <String>[
+ '-s',
+ device.id,
+ 'logcat',
+ '-v',
+ 'tag', // Only log the tag and the message
+ '-T',
+ device.lastLogcatTimestamp,
+ '-s',
+ 'flutter:V',
+ 'ActivityManager:W',
+ 'System.err:W',
+ '*:F',
+ ]));
+ _stdoutSubscription =
+ _process.stdout.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onLine);
+ _stderrSubscription =
+ _process.stderr.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onLine);
+ _process.exitCode.then(_onExit);
+ }
+
+ Future stop() async {
+ if (_process == null) {
+ throw new StateError(
+ '_AdbLogReader must be started before it can be stopped.');
+ }
+ _stdoutSubscription?.cancel();
+ _stdoutSubscription = null;
+ _stderrSubscription?.cancel();
+ _stderrSubscription = null;
+ await _process.kill();
+ _process = null;
+ }
+
+ void _onExit(int exitCode) {
+ _stdoutSubscription?.cancel();
+ _stdoutSubscription = null;
+ _stderrSubscription?.cancel();
+ _stderrSubscription = null;
+ _process = null;
+ }
+
+ void _onLine(String line) {
+ _linesStreamController.add(line);
}
int get hashCode => name.hashCode;
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index f30c9b2..89ae5a3 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -10,20 +10,30 @@
typedef String StringConverter(String string);
+/// This runs the command in the background from the specified working
+/// directory. Completes when the process has been started.
+Future<Process> runCommand(List<String> cmd, {String workingDirectory}) async {
+ printTrace(cmd.join(' '));
+ String executable = cmd[0];
+ List<String> arguments = cmd.length > 1 ? cmd.sublist(1) : [];
+ Process process = await Process.start(
+ executable,
+ arguments,
+ workingDirectory: workingDirectory
+ );
+ return process;
+}
+
/// This runs the command and streams stdout/stderr from the child process to
-/// this process' stdout/stderr.
+/// this process' stdout/stderr. Completes with the process's exit code.
Future<int> runCommandAndStreamOutput(List<String> cmd, {
String workingDirectory,
String prefix: '',
RegExp filter,
StringConverter mapFunction
}) async {
- printTrace(cmd.join(' '));
- Process process = await Process.start(
- cmd[0],
- cmd.sublist(1),
- workingDirectory: workingDirectory
- );
+ Process process = await runCommand(cmd,
+ workingDirectory: workingDirectory);
process.stdout
.transform(UTF8.decoder)
.transform(const LineSplitter())
diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart
index 9f7d114..75261ca 100644
--- a/packages/flutter_tools/lib/src/commands/logs.dart
+++ b/packages/flutter_tools/lib/src/commands/logs.dart
@@ -39,13 +39,30 @@
List<DeviceLogReader> readers = new List<DeviceLogReader>();
for (Device device in devices) {
- readers.add(device.createLogReader());
+ if (clear)
+ device.clearLogs();
+
+ readers.add(device.logReader);
}
printStatus('Showing ${readers.join(', ')} logs:');
List<int> results = await Future.wait(readers.map((DeviceLogReader reader) async {
- int result = await reader.logs(clear: clear, showPrefix: devices.length > 1);
+ if (!reader.isReading) {
+ // Start reading.
+ await reader.start();
+ }
+ StreamSubscription subscription = reader.lines.listen((String line) {
+ if (devices.length > 1) {
+ // Prefix with the name of the device.
+ print('[${reader.name}] $line');
+ } else {
+ print(line);
+ }
+ });
+ // Wait for the log reader to be finished.
+ int result = await reader.finished;
+ subscription.cancel();
if (result != 0)
printError('Error listening to $reader logs.');
return result;
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 985bc6b..822b8bb 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -148,7 +148,11 @@
TargetPlatform get platform;
- DeviceLogReader createLogReader();
+ /// Get the log reader for this device.
+ DeviceLogReader get logReader;
+
+ /// Clear the device's logs.
+ void clearLogs();
/// Start an app package on the current device.
///
@@ -189,7 +193,21 @@
abstract class DeviceLogReader {
String get name;
- Future<int> logs({ bool clear: false, bool showPrefix: false });
+ /// A broadcast stream where each element in the string is a line of log
+ /// output.
+ Stream<String> get lines;
+
+ /// Start reading logs from the device.
+ Future start();
+
+ /// Actively reading lines from the log?
+ bool get isReading;
+
+ /// Actively stop reading logs from the device.
+ Future stop();
+
+ /// Completes when the log is finished.
+ Future get finished;
int get hashCode;
bool operator ==(dynamic other);
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 3f57bd2..8af09f4 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
@@ -62,6 +63,8 @@
final String name;
+ _IOSDeviceLogReader _logReader;
+
bool get isLocalEmulator => false;
bool get supportsStartPaused => false;
@@ -220,7 +223,15 @@
@override
TargetPlatform get platform => TargetPlatform.iOS;
- DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this);
+ DeviceLogReader get logReader {
+ if (_logReader == null)
+ _logReader = new _IOSDeviceLogReader(this);
+
+ return _logReader;
+ }
+
+ void clearLogs() {
+ }
}
class _IOSDeviceLogReader extends DeviceLogReader {
@@ -228,15 +239,65 @@
final IOSDevice device;
+ final StreamController<String> _linesStreamController =
+ new StreamController<String>.broadcast();
+
+ Process _process;
+ StreamSubscription _stdoutSubscription;
+ StreamSubscription _stderrSubscription;
+
+ Stream<String> get lines => _linesStreamController.stream;
+
String get name => device.name;
- // TODO(devoncarew): Support [clear].
- Future<int> logs({ bool clear: false, bool showPrefix: false }) async {
- return await runCommandAndStreamOutput(
- <String>[device.loggerPath],
- prefix: showPrefix ? '[$name] ' : '',
- filter: new RegExp(r'Runner')
- );
+ bool get isReading => _process != null;
+
+ Future get finished =>
+ _process != null ? _process.exitCode : new Future.value(0);
+
+ Future start() async {
+ if (_process != null) {
+ throw new StateError(
+ '_IOSDeviceLogReader must be stopped before it can be started.');
+ }
+ _process = await runCommand(<String>[device.loggerPath]);
+ _stdoutSubscription =
+ _process.stdout.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onLine);
+ _stderrSubscription =
+ _process.stderr.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onLine);
+ _process.exitCode.then(_onExit);
+ }
+
+ Future stop() async {
+ if (_process == null) {
+ throw new StateError(
+ '_IOSDeviceLogReader must be started before it can be stopped.');
+ }
+ _stdoutSubscription?.cancel();
+ _stdoutSubscription = null;
+ _stderrSubscription?.cancel();
+ _stderrSubscription = null;
+ await _process.kill();
+ _process = null;
+ }
+
+ void _onExit(int exitCode) {
+ _stdoutSubscription?.cancel();
+ _stdoutSubscription = null;
+ _stderrSubscription?.cancel();
+ _stderrSubscription = null;
+ _process = null;
+ }
+
+ RegExp _runnerRegex = new RegExp(r'Runner');
+
+ void _onLine(String line) {
+ if (!_runnerRegex.hasMatch(line))
+ return;
+
+ _linesStreamController.add(line);
}
int get hashCode => name.hashCode;
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 0cb5e54..cb9ab5d 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
-import 'dart:convert' show JSON;
+import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
@@ -213,6 +213,8 @@
bool get isLocalEmulator => true;
+ _IOSSimulatorLogReader _logReader;
+
String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
String _getSimulatorPath() {
@@ -428,7 +430,12 @@
@override
TargetPlatform get platform => TargetPlatform.iOSSimulator;
- DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this);
+ DeviceLogReader get logReader {
+ if (_logReader == null)
+ _logReader = new _IOSSimulatorLogReader(this);
+
+ return _logReader;
+ }
void clearLogs() {
File logFile = new File(logFilePath);
@@ -451,71 +458,157 @@
final IOSSimulator device;
+ final StreamController<String> _linesStreamController =
+ new StreamController<String>.broadcast();
+
bool _lastWasFiltered = false;
+ // We log from two logs: the device and the system log.
+ Process _deviceProcess;
+ StreamSubscription _deviceStdoutSubscription;
+ StreamSubscription _deviceStderrSubscription;
+
+ Process _systemProcess;
+ StreamSubscription _systemStdoutSubscription;
+ StreamSubscription _systemStderrSubscription;
+
+ Stream<String> get lines => _linesStreamController.stream;
+
String get name => device.name;
- Future<int> logs({ bool clear: false, bool showPrefix: false }) async {
- if (clear)
- device.clearLogs();
+ bool get isReading => (_deviceProcess != null) && (_systemProcess != null);
+ Future get finished =>
+ (_deviceProcess != null) ? _deviceProcess.exitCode : new Future.value(0);
+
+ Future start() async {
+ if (isReading) {
+ throw new StateError(
+ '_IOSSimulatorLogReader must be stopped before it can be started.');
+ }
+
+ // TODO(johnmccutchan): Add a ProcessSet abstraction that handles running
+ // N processes and merging their output.
+
+ // Device log.
device.ensureLogsExists();
-
- // Match the log prefix (in order to shorten it):
- // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
- RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
- // Jan 31 19:23:28 --- last message repeated 1 time ---
- RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
-
- // This filter matches many Flutter lines in the log:
- // new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses
- // a fair number, including ones that would be useful in diagnosing crashes.
- // For now, we're not filtering the log file (but do clear it with each run).
-
- Future<int> result = runCommandAndStreamOutput(
- <String>['tail', '-n', '+0', '-F', device.logFilePath],
- prefix: showPrefix ? '[$name] ' : '',
- mapFunction: (String string) {
- Match match = mapRegex.matchAsPrefix(string);
- if (match != null) {
- _lastWasFiltered = true;
-
- // Filter out some messages that clearly aren't related to Flutter.
- if (string.contains(': could not find icon for representation -> com.apple.'))
- return null;
- String category = match.group(1);
- String content = match.group(2);
- if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' ||
- category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' ||
- category == 'searchd')
- return null;
-
- _lastWasFiltered = false;
-
- if (category == 'Runner')
- return content;
- return '$category: $content';
- }
- match = lastMessageRegex.matchAsPrefix(string);
- if (match != null && !_lastWasFiltered)
- return '(${match.group(1)})';
- return string;
- }
- );
+ _deviceProcess = await runCommand(
+ <String>['tail', '-n', '+0', '-F', device.logFilePath]);
+ _deviceStdoutSubscription =
+ _deviceProcess.stdout.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onDeviceLine);
+ _deviceStderrSubscription =
+ _deviceProcess.stderr.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onDeviceLine);
+ _deviceProcess.exitCode.then(_onDeviceExit);
// Track system.log crashes.
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
- runCommandAndStreamOutput(
- <String>['tail', '-F', '/private/var/log/system.log'],
- prefix: showPrefix ? '[$name] ' : '',
- filter: new RegExp(r' FlutterRunner\[\d+\] '),
- mapFunction: (String string) {
- Match match = mapRegex.matchAsPrefix(string);
- return match == null ? string : '${match.group(1)}: ${match.group(2)}';
- }
- );
+ _systemProcess = await runCommand(
+ <String>['tail', '-F', '/private/var/log/system.log']);
+ _systemStdoutSubscription =
+ _systemProcess.stdout.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onSystemLine);
+ _systemStderrSubscription =
+ _systemProcess.stderr.transform(UTF8.decoder)
+ .transform(const LineSplitter()).listen(_onSystemLine);
+ _systemProcess.exitCode.then(_onSystemExit);
+ }
- return await result;
+ Future stop() async {
+ if (!isReading) {
+ throw new StateError(
+ '_IOSSimulatorLogReader must be started before it can be stopped.');
+ }
+ if (_deviceProcess != null) {
+ await _deviceProcess.kill();
+ _deviceProcess = null;
+ }
+ _onDeviceExit(0);
+ if (_systemProcess != null) {
+ await _systemProcess.kill();
+ _systemProcess = null;
+ }
+ _onSystemExit(0);
+ }
+
+ void _onDeviceExit(int exitCode) {
+ _deviceStdoutSubscription?.cancel();
+ _deviceStdoutSubscription = null;
+ _deviceStderrSubscription?.cancel();
+ _deviceStderrSubscription = null;
+ _deviceProcess = null;
+ }
+
+ void _onSystemExit(int exitCode) {
+ _systemStdoutSubscription?.cancel();
+ _systemStdoutSubscription = null;
+ _systemStderrSubscription?.cancel();
+ _systemStderrSubscription = null;
+ _systemProcess = null;
+ }
+
+ // Match the log prefix (in order to shorten it):
+ // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
+ final RegExp _mapRegex =
+ new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
+
+ // Jan 31 19:23:28 --- last message repeated 1 time ---
+ final RegExp _lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
+
+ final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] ');
+
+ String _filterDeviceLine(String string) {
+ Match match = _mapRegex.matchAsPrefix(string);
+ if (match != null) {
+ _lastWasFiltered = true;
+
+ // Filter out some messages that clearly aren't related to Flutter.
+ if (string.contains(': could not find icon for representation -> com.apple.'))
+ return null;
+
+ String category = match.group(1);
+ String content = match.group(2);
+ if (category == 'Game Center' || category == 'itunesstored' ||
+ category == 'nanoregistrylaunchd' || category == 'mstreamd' ||
+ category == 'syncdefaultsd' || category == 'companionappd' ||
+ category == 'searchd')
+ return null;
+
+ _lastWasFiltered = false;
+
+ if (category == 'Runner')
+ return content;
+ return '$category: $content';
+ }
+ match = _lastMessageRegex.matchAsPrefix(string);
+ if (match != null && !_lastWasFiltered)
+ return '(${match.group(1)})';
+ return string;
+ }
+
+ void _onDeviceLine(String line) {
+ String filteredLine = _filterDeviceLine(line);
+ if (filteredLine == null)
+ return;
+
+ _linesStreamController.add(filteredLine);
+ }
+
+ String _filterSystemLog(String string) {
+ Match match = _mapRegex.matchAsPrefix(string);
+ return match == null ? string : '${match.group(1)}: ${match.group(2)}';
+ }
+
+ void _onSystemLine(String line) {
+ if (!_flutterRunnerRegex.hasMatch(line))
+ return;
+
+ String filteredLine = _filterSystemLog(line);
+ if (filteredLine == null)
+ return;
+
+ _linesStreamController.add(filteredLine);
}
int get hashCode => device.logFilePath.hashCode;