Commit b0dca796 authored by Devon Carew's avatar Devon Carew

Flutter run (#3553)

* rework flutter run

* fix npe with --debug-port

* connect to obs and exit when that conneciton closes

* update todos
parent cbe650a7
...@@ -12,7 +12,6 @@ import 'package:web_socket_channel/io.dart'; ...@@ -12,7 +12,6 @@ import 'package:web_socket_channel/io.dart';
import '../android/android_sdk.dart'; import '../android/android_sdk.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/utils.dart'; import '../base/utils.dart';
...@@ -189,49 +188,38 @@ class AndroidDevice extends Device { ...@@ -189,49 +188,38 @@ class AndroidDevice extends Device {
} }
Future<Null> _forwardPort(String service, int devicePort, int port) async { Future<Null> _forwardPort(String service, int devicePort, int port) async {
bool portWasZero = (port == null) || (port == 0);
try { try {
// Set up port forwarding for observatory. // Set up port forwarding for observatory.
port = await portForwarder.forward(devicePort, port = await portForwarder.forward(devicePort, hostPort: port);
hostPort: port);
if (portWasZero)
printStatus('$service listening on http://127.0.0.1:$port'); printStatus('$service listening on http://127.0.0.1:$port');
} catch (e) { } catch (e) {
printError('Unable to forward port $port: $e'); printError('Unable to forward port $port: $e');
} }
} }
Future<bool> startBundle(AndroidApk apk, String bundlePath, { Future<LaunchResult> startBundle(AndroidApk apk, String bundlePath, {
bool checked: true,
bool traceStartup: false, bool traceStartup: false,
String route, String route,
bool clearLogs: false, DebuggingOptions options
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort
}) async { }) async {
printTrace('$this startBundle'); printTrace('$this startBundle');
if (!FileSystemEntity.isFileSync(bundlePath)) { if (!FileSystemEntity.isFileSync(bundlePath)) {
printError('Cannot find $bundlePath'); printError('Cannot find $bundlePath');
return false; return new LaunchResult.failed();
} }
if (clearLogs)
this.clearLogs();
runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath])); runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath]));
ServiceProtocolDiscovery observatoryDiscovery = ServiceProtocolDiscovery observatoryDiscovery;
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService); ServiceProtocolDiscovery diagnosticDiscovery;
ServiceProtocolDiscovery diagnosticDiscovery =
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kDiagnosticService);
// We take this future here but do not wait for completion until *after* we if (options.debuggingEnabled) {
// start the bundle. observatoryDiscovery = new ServiceProtocolDiscovery(
Future<List<int>> scrapeServicePorts = Future.wait( logReader, ServiceProtocolDiscovery.kObservatoryService);
<Future<int>>[observatoryDiscovery.nextPort(), diagnosticDiscovery.nextPort()]); diagnosticDiscovery = new ServiceProtocolDiscovery(
logReader, ServiceProtocolDiscovery.kDiagnosticService);
}
List<String> cmd = adbCommandForDevice(<String>[ List<String> cmd = adbCommandForDevice(<String>[
'shell', 'am', 'start', 'shell', 'am', 'start',
...@@ -240,61 +228,76 @@ class AndroidDevice extends Device { ...@@ -240,61 +228,76 @@ class AndroidDevice extends Device {
'-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
'--ez', 'enable-background-compilation', 'true', '--ez', 'enable-background-compilation', 'true',
]); ]);
if (checked)
cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
if (traceStartup) if (traceStartup)
cmd.addAll(<String>['--ez', 'trace-startup', 'true']); cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
if (startPaused)
cmd.addAll(<String>['--ez', 'start-paused', 'true']);
if (route != null) if (route != null)
cmd.addAll(<String>['--es', 'route', route]); cmd.addAll(<String>['--es', 'route', route]);
if (options.debuggingEnabled) {
if (options.checked)
cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
if (options.startPaused)
cmd.addAll(<String>['--ez', 'start-paused', 'true']);
}
cmd.add(apk.launchActivity); cmd.add(apk.launchActivity);
String result = runCheckedSync(cmd); String result = runCheckedSync(cmd);
// This invocation returns 0 even when it fails. // This invocation returns 0 even when it fails.
if (result.contains('Error: ')) { if (result.contains('Error: ')) {
printError(result.trim()); printError(result.trim());
return false; return new LaunchResult.failed();
} }
if (!options.debuggingEnabled) {
return new LaunchResult.succeeded();
} else {
// Wait for the service protocol port here. This will complete once the // Wait for the service protocol port here. This will complete once the
// device has printed "Observatory is listening on...". // device has printed "Observatory is listening on...".
printTrace('Waiting for observatory port to be available...'); printTrace('Waiting for observatory port to be available...');
try { try {
List<int> devicePorts = await scrapeServicePorts.timeout(new Duration(seconds: 12)); Future<List<int>> scrapeServicePorts = Future.wait(
<Future<int>>[observatoryDiscovery.nextPort(), diagnosticDiscovery.nextPort()]
);
List<int> devicePorts = await scrapeServicePorts.timeout(new Duration(seconds: 20));
int observatoryDevicePort = devicePorts[0]; int observatoryDevicePort = devicePorts[0];
int diagnosticDevicePort = devicePorts[1];
printTrace('observatory port = $observatoryDevicePort'); printTrace('observatory port = $observatoryDevicePort');
int observatoryLocalPort = await options.findBestObservatoryPort();
// TODO(devoncarew): Remember the forwarding information (so we can later remove the
// port forwarding).
await _forwardPort(ServiceProtocolDiscovery.kObservatoryService, await _forwardPort(ServiceProtocolDiscovery.kObservatoryService,
observatoryDevicePort, observatoryPort); observatoryDevicePort, observatoryLocalPort);
int diagnosticDevicePort = devicePorts[1];
printTrace('diagnostic port = $diagnosticDevicePort');
int diagnosticLocalPort = await options.findBestDiagnosticPort();
await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService, await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService,
diagnosticDevicePort, diagnosticPort); diagnosticDevicePort, diagnosticLocalPort);
return true; return new LaunchResult.succeeded(
observatoryPort: observatoryLocalPort,
diagnosticPort: diagnosticLocalPort
);
} catch (error) { } catch (error) {
if (error is TimeoutException) if (error is TimeoutException)
printError('Timed out while waiting for a debug connection.'); printError('Timed out while waiting for a debug connection.');
else else
printError('Error waiting for a debug connection: $error'); printError('Error waiting for a debug connection: $error');
return new LaunchResult.failed();
return false; } finally {
observatoryDiscovery.cancel();
diagnosticDiscovery.cancel();
}
} }
} }
@override @override
Future<bool> startApp( Future<LaunchResult> startApp(
ApplicationPackage package, ApplicationPackage package,
Toolchain toolchain, { Toolchain toolchain, {
String mainPath, String mainPath,
String route, String route,
bool checked: true, DebuggingOptions debuggingOptions,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}) async { }) async {
if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion()) if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
return false; return new LaunchResult.failed();
String localBundlePath = await flx.buildFlx( String localBundlePath = await flx.buildFlx(
toolchain, toolchain,
...@@ -304,21 +307,13 @@ class AndroidDevice extends Device { ...@@ -304,21 +307,13 @@ class AndroidDevice extends Device {
printTrace('Starting bundle for $this.'); printTrace('Starting bundle for $this.');
if (await startBundle( return startBundle(
package, package,
localBundlePath, localBundlePath,
checked: checked,
traceStartup: platformArgs['trace-startup'], traceStartup: platformArgs['trace-startup'],
route: route, route: route,
clearLogs: clearLogs, options: debuggingOptions
startPaused: startPaused, );
observatoryPort: observatoryPort,
diagnosticPort: diagnosticPort
)) {
return true;
} else {
return false;
}
} }
@override @override
...@@ -468,7 +463,8 @@ List<AndroidDevice> getAdbDevices() { ...@@ -468,7 +463,8 @@ List<AndroidDevice> getAdbDevices() {
// 0149947A0D01500C device usb:340787200X // 0149947A0D01500C device usb:340787200X
// emulator-5612 host features:shell_2 // emulator-5612 host features:shell_2
RegExp deviceRegExShort = new RegExp(r'^(\S+)\s+(\S+)\s+\S+$'); // emulator-5554 offline
RegExp deviceRegExShort = new RegExp(r'^(\S+)\s+(\S+)(\s+\S+)?$');
for (String line in output) { for (String line in output) {
// Skip lines like: * daemon started successfully * // Skip lines like: * daemon started successfully *
...@@ -522,34 +518,25 @@ List<AndroidDevice> getAdbDevices() { ...@@ -522,34 +518,25 @@ List<AndroidDevice> getAdbDevices() {
/// A log reader that logs from `adb logcat`. /// A log reader that logs from `adb logcat`.
class _AdbLogReader extends DeviceLogReader { class _AdbLogReader extends DeviceLogReader {
_AdbLogReader(this.device); _AdbLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: _start,
onCancel: _stop
);
}
final AndroidDevice device; final AndroidDevice device;
final StreamController<String> _linesStreamController = StreamController<String> _linesController;
new StreamController<String>.broadcast();
Process _process; Process _process;
StreamSubscription<String> _stdoutSubscription;
StreamSubscription<String> _stderrSubscription;
@override @override
Stream<String> get lines => _linesStreamController.stream; Stream<String> get logLines => _linesController.stream;
@override @override
String get name => device.name; String get name => device.name;
@override void _start() {
bool get isReading => _process != null;
@override
Future<int> get finished => _process != null ? _process.exitCode : new Future<int>.value(0);
@override
Future<Null> start() async {
if (_process != null)
throw new StateError('_AdbLogReader must be stopped before it can be started.');
// Start the adb logcat process. // Start the adb logcat process.
List<String> args = <String>['logcat', '-v', 'tag']; List<String> args = <String>['logcat', '-v', 'tag'];
String lastTimestamp = device.lastLogcatTimestamp; String lastTimestamp = device.lastLogcatTimestamp;
...@@ -558,55 +545,29 @@ class _AdbLogReader extends DeviceLogReader { ...@@ -558,55 +545,29 @@ class _AdbLogReader extends DeviceLogReader {
args.addAll(<String>[ args.addAll(<String>[
'-s', 'flutter:V', 'FlutterMain:V', 'FlutterView:V', 'AndroidRuntime:W', 'ActivityManager:W', 'System.err:W', '*:F' '-s', 'flutter:V', 'FlutterMain:V', 'FlutterView:V', 'AndroidRuntime:W', 'ActivityManager:W', 'System.err:W', '*:F'
]); ]);
_process = await runCommand(device.adbCommandForDevice(args)); runCommand(device.adbCommandForDevice(args)).then((Process process) {
_stdoutSubscription = _process = process;
_process.stdout.transform(UTF8.decoder) _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
.transform(const LineSplitter()).listen(_onLine); _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
_stderrSubscription =
_process.stderr.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onLine);
_process.exitCode.then(_onExit);
}
@override
Future<Null> 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) { _process.exitCode.then((int code) {
_stdoutSubscription?.cancel(); if (_linesController.hasListener)
_stdoutSubscription = null; _linesController.close();
_stderrSubscription?.cancel(); });
_stderrSubscription = null; });
_process = null;
} }
void _onLine(String line) { void _onLine(String line) {
// Filter out some noisy ActivityManager notifications. // Filter out some noisy ActivityManager notifications.
if (line.startsWith('W/ActivityManager: getRunningAppProcesses')) if (line.startsWith('W/ActivityManager: getRunningAppProcesses'))
return; return;
_linesController.add(line);
_linesStreamController.add(line);
} }
@override void _stop() {
int get hashCode => name.hashCode; // TODO(devoncarew): We should remove adb port forwarding here.
@override _process?.kill();
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _AdbLogReader)
return false;
return other.device.id == device.id;
} }
} }
......
...@@ -2,5 +2,6 @@ ...@@ -2,5 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
const int observatoryDefaultPort = 8181; const int defaultObservatoryPort = 8100;
const int diagnosticDefaultPort = 8182; const int defaultDiagnosticPort = 8101;
const int defaultDrivePort = 8183;
...@@ -109,3 +109,30 @@ Future<int> findAvailablePort() async { ...@@ -109,3 +109,30 @@ Future<int> findAvailablePort() async {
await socket.close(); await socket.close();
return port; return port;
} }
const int _kMaxSearchIterations = 5;
/// This method will attempt to return a port close to or the same as
/// [defaultPort]. Failing that, it will return any available port.
Future<int> findPreferredPort(int defaultPort, { int searchStep: 2 }) async {
int iterationCount = 0;
while (iterationCount < _kMaxSearchIterations) {
int port = defaultPort + iterationCount * searchStep;
if (await _isPortAvailable(port))
return port;
iterationCount++;
}
return findAvailablePort();
}
Future<bool> _isPortAvailable(int port) async {
try {
ServerSocket socket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, port);
await socket.close();
return true;
} catch (error) {
return false;
}
}
...@@ -267,8 +267,7 @@ class AppDomain extends Domain { ...@@ -267,8 +267,7 @@ class AppDomain extends Domain {
command.toolchain, command.toolchain,
stop: true, stop: true,
target: args['target'], target: args['target'],
route: args['route'], route: args['route']
checked: args['checked'] ?? true
); );
if (result != 0) if (result != 0)
......
...@@ -11,6 +11,7 @@ import 'package:test/src/executable.dart' as executable; // ignore: implementati ...@@ -11,6 +11,7 @@ import 'package:test/src/executable.dart' as executable; // ignore: implementati
import '../android/android_device.dart' show AndroidDevice; import '../android/android_device.dart' show AndroidDevice;
import '../application_package.dart'; import '../application_package.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/common.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
...@@ -60,8 +61,9 @@ class DriveCommand extends RunCommandBase { ...@@ -60,8 +61,9 @@ class DriveCommand extends RunCommandBase {
); );
argParser.addOption('debug-port', argParser.addOption('debug-port',
defaultsTo: '8183', defaultsTo: defaultDrivePort.toString(),
help: 'Listen to the given port for a debug connection.'); help: 'Listen to the given port for a debug connection.'
);
} }
@override @override
...@@ -261,25 +263,22 @@ Future<int> startApp(DriveCommand command) async { ...@@ -261,25 +263,22 @@ Future<int> startApp(DriveCommand command) async {
await command.device.installApp(package); await command.device.installApp(package);
printTrace('Starting application.'); printTrace('Starting application.');
bool started = await command.device.startApp( LaunchResult result = await command.device.startApp(
package, package,
command.toolchain, command.toolchain,
mainPath: mainPath, mainPath: mainPath,
route: command.route, route: command.route,
debuggingOptions: new DebuggingOptions.enabled(
checked: command.checked, checked: command.checked,
clearLogs: true,
startPaused: true, startPaused: true,
observatoryPort: command.debugPort, observatoryPort: command.debugPort
),
platformArgs: <String, dynamic>{ platformArgs: <String, dynamic>{
'trace-startup': command.traceStartup, 'trace-startup': command.traceStartup,
} }
); );
if (started && command.device.supportsStartPaused) { return result.started ? 0 : 2;
await delayUntilObservatoryAvailable('localhost', command.debugPort);
}
return started ? 0 : 2;
} }
/// Runs driver tests. /// Runs driver tests.
......
...@@ -7,6 +7,7 @@ import 'dart:io'; ...@@ -7,6 +7,7 @@ import 'dart:io';
import '../base/os.dart'; import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import 'run.dart'; import 'run.dart';
...@@ -62,7 +63,7 @@ class ListenCommand extends RunCommandBase { ...@@ -62,7 +63,7 @@ class ListenCommand extends RunCommandBase {
target: target, target: target,
install: firstTime, install: firstTime,
stop: true, stop: true,
checked: checked, debuggingOptions: new DebuggingOptions.enabled(checked: checked),
traceStartup: traceStartup, traceStartup: traceStartup,
route: route route: route
); );
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:io';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
...@@ -40,17 +41,33 @@ class LogsCommand extends FlutterCommand { ...@@ -40,17 +41,33 @@ class LogsCommand extends FlutterCommand {
printStatus('Showing $logReader logs:'); printStatus('Showing $logReader logs:');
Completer<int> exitCompleter = new Completer<int>();
// Start reading. // Start reading.
if (!logReader.isReading) StreamSubscription<String> subscription = logReader.logLines.listen(
await logReader.start(); printStatus,
onDone: () {
exitCompleter.complete(0);
},
onError: (dynamic error) {
exitCompleter.complete(error is int ? error : 1);
}
);
StreamSubscription<String> subscription = logReader.lines.listen(printStatus); // When terminating, close down the log reader.
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
subscription.cancel();
printStatus('');
exitCompleter.complete(0);
});
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
subscription.cancel();
exitCompleter.complete(0);
});
// Wait for the log reader to be finished. // Wait for the log reader to be finished.
int result = await logReader.finished; int result = await exitCompleter.future;
subscription.cancel(); subscription.cancel();
if (result != 0) if (result != 0)
printError('Error listening to $logReader logs.'); printError('Error listening to $logReader logs.');
return result; return result;
......
...@@ -39,7 +39,8 @@ class RefreshCommand extends FlutterCommand { ...@@ -39,7 +39,8 @@ class RefreshCommand extends FlutterCommand {
String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin'); String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin');
int result = await toolchain.compiler.createSnapshot( int result = await toolchain.compiler.createSnapshot(
mainPath: argResults['target'], snapshotPath: snapshotPath mainPath: argResults['target'],
snapshotPath: snapshotPath
); );
if (result != 0) { if (result != 0) {
printError('Failed to run the Flutter compiler. Exit code: $result'); printError('Failed to run the Flutter compiler. Exit code: $result');
......
...@@ -17,19 +17,6 @@ import '../toolchain.dart'; ...@@ -17,19 +17,6 @@ import '../toolchain.dart';
import 'build_apk.dart'; import 'build_apk.dart';
import 'install.dart'; import 'install.dart';
/// Given the value of the --target option, return the path of the Dart file
/// where the app's main function should be.
String findMainDartFile([String target]) {
if (target == null)
target = '';
String targetPath = path.absolute(target);
if (FileSystemEntity.isDirectorySync(targetPath)) {
return path.join(targetPath, 'lib', 'main.dart');
} else {
return targetPath;
}
}
abstract class RunCommandBase extends FlutterCommand { abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() { RunCommandBase() {
argParser.addFlag('checked', argParser.addFlag('checked',
...@@ -66,17 +53,21 @@ class RunCommand extends RunCommandBase { ...@@ -66,17 +53,21 @@ class RunCommand extends RunCommandBase {
argParser.addFlag('full-restart', argParser.addFlag('full-restart',
defaultsTo: true, defaultsTo: true,
help: 'Stop any currently running application process before running the app.'); help: 'Stop any currently running application process before running the app.');
argParser.addFlag('clear-logs',
defaultsTo: true,
help: 'Clear log history before running the app.');
argParser.addFlag('start-paused', argParser.addFlag('start-paused',
defaultsTo: false, defaultsTo: false,
negatable: false, negatable: false,
help: 'Start in a paused mode and wait for a debugger to connect.'); help: 'Start in a paused mode and wait for a debugger to connect.');
argParser.addOption('debug-port', argParser.addOption('debug-port',
defaultsTo: observatoryDefaultPort.toString(), help: 'Listen to the given port for a debug connection (defaults to $defaultObservatoryPort).');
help: 'Listen to the given port for a debug connection.');
usesPubOption(); usesPubOption();
// A temporary, hidden flag to experiment with a different run style.
// TODO(devoncarew): Remove this.
argParser.addFlag('resident',
defaultsTo: false,
negatable: false,
hide: true,
help: 'Stay resident after running the app.');
} }
@override @override
...@@ -95,45 +86,54 @@ class RunCommand extends RunCommandBase { ...@@ -95,45 +86,54 @@ class RunCommand extends RunCommandBase {
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
bool clearLogs = argResults['clear-logs'];
int debugPort; int debugPort;
if (argResults['debug-port'] != null) {
try { try {
debugPort = int.parse(argResults['debug-port']); debugPort = int.parse(argResults['debug-port']);
} catch (error) { } catch (error) {
printError('Invalid port for `--debug-port`: $error'); printError('Invalid port for `--debug-port`: $error');
return 1; return 1;
} }
}
int result;
DebuggingOptions options;
if (getBuildMode() != BuildMode.debug) {
options = new DebuggingOptions.disabled();
} else {
options = new DebuggingOptions.enabled(
checked: checked,
startPaused: argResults['start-paused'],
observatoryPort: debugPort
);
}
int result = await startApp( if (argResults['resident']) {
result = await startAppStayResident(
deviceForCommand,
toolchain,
target: target,
debuggingOptions: options,
traceStartup: traceStartup,
buildMode: getBuildMode()
);
} else {
result = await startApp(
deviceForCommand, deviceForCommand,
toolchain, toolchain,
target: target, target: target,
enginePath: runner.enginePath,
install: true,
stop: argResults['full-restart'], stop: argResults['full-restart'],
checked: checked, install: true,
debuggingOptions: options,
traceStartup: traceStartup, traceStartup: traceStartup,
route: route, route: route,
clearLogs: clearLogs,
startPaused: argResults['start-paused'],
debugPort: debugPort,
buildMode: getBuildMode() buildMode: getBuildMode()
); );
return result;
} }
}
String _getMissingPackageHintForPlatform(TargetPlatform platform) { return result;
switch (platform) {
case TargetPlatform.android_arm:
return 'Is your project missing an android/AndroidManifest.xml?';
case TargetPlatform.ios:
return 'Is your project missing an ios/Info.plist?';
default:
return null;
} }
} }
...@@ -141,15 +141,11 @@ Future<int> startApp( ...@@ -141,15 +141,11 @@ Future<int> startApp(
Device device, Device device,
Toolchain toolchain, { Toolchain toolchain, {
String target, String target,
String enginePath,
bool stop: true, bool stop: true,
bool install: true, bool install: true,
bool checked: true, DebuggingOptions debuggingOptions,
bool traceStartup: false, bool traceStartup: false,
String route, String route,
bool clearLogs: false,
bool startPaused: false,
int debugPort: observatoryDefaultPort,
BuildMode buildMode: BuildMode.debug BuildMode buildMode: BuildMode.debug
}) async { }) async {
String mainPath = findMainDartFile(target); String mainPath = findMainDartFile(target);
...@@ -216,22 +212,164 @@ Future<int> startApp( ...@@ -216,22 +212,164 @@ Future<int> startApp(
printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...'); printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...');
bool result = await device.startApp( LaunchResult result = await device.startApp(
package, package,
toolchain, toolchain,
mainPath: mainPath, mainPath: mainPath,
route: route, route: route,
checked: checked, debuggingOptions: debuggingOptions,
clearLogs: clearLogs, platformArgs: platformArgs
startPaused: startPaused, );
observatoryPort: debugPort,
if (!result.started)
printError('Error running application on ${device.name}.');
return result.started ? 0 : 2;
}
// start logging
// start the app
// scrape obs. port
// connect via obs.
// stay alive as long as obs. is alive
// intercept SIG_QUIT; kill the launched app
Future<int> startAppStayResident(
Device device,
Toolchain toolchain, {
String target,
DebuggingOptions debuggingOptions,
bool traceStartup: false,
BuildMode buildMode: BuildMode.debug
}) async {
String mainPath = findMainDartFile(target);
if (!FileSystemEntity.isFileSync(mainPath)) {
String message = 'Tried to run $mainPath, but that file does not exist.';
if (target == null)
message += '\nConsider using the -t option to specify the Dart file to start.';
printError(message);
return 1;
}
ApplicationPackage package = getApplicationPackageForPlatform(device.platform);
if (package == null) {
String message = 'No application found for ${device.platform}.';
String hint = _getMissingPackageHintForPlatform(device.platform);
if (hint != null)
message += '\n$hint';
printError(message);
return 1;
}
// TODO(devoncarew): We shouldn't have to do type checks here.
if (device is AndroidDevice) {
printTrace('Running build command.');
int result = await buildApk(
device.platform,
toolchain,
target: target,
buildMode: buildMode
);
if (result != 0)
return result;
}
// TODO(devoncarew): Move this into the device.startApp() impls.
if (package != null) {
printTrace("Stopping app '${package.name}' on ${device.name}.");
// We don't wait for the stop command to complete.
device.stopApp(package);
}
// Allow any stop commands from above to start work.
await new Future<Duration>.delayed(Duration.ZERO);
printTrace('Running install command.');
// TODO(devoncarew): This fails for ios devices - we haven't built yet.
await installApp(device, package);
Map<String, dynamic> platformArgs;
if (traceStartup != null)
platformArgs = <String, dynamic>{ 'trace-startup': traceStartup };
printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...');
StreamSubscription<String> loggingSubscription = device.logReader.logLines.listen((String line) {
if (!line.contains('Observatory listening on http') && !line.contains('Diagnostic server listening on http'))
printStatus(line);
});
LaunchResult result = await device.startApp(
package,
toolchain,
mainPath: mainPath,
debuggingOptions: debuggingOptions,
platformArgs: platformArgs platformArgs: platformArgs
); );
if (!result) if (!result.started) {
printError('Error running application on ${device.name}.'); printError('Error running application on ${device.name}.');
await loggingSubscription.cancel();
return 2;
}
Completer<int> exitCompleter = new Completer<int>();
void complete(int exitCode) {
if (!exitCompleter.isCompleted)
exitCompleter.complete(0);
};
// Connect to observatory.
WebSocket observatoryConnection;
if (debuggingOptions.debuggingEnabled) {
final String localhost = InternetAddress.LOOPBACK_IP_V4.address;
final String url = 'ws://$localhost:${result.observatoryPort}/ws';
observatoryConnection = await WebSocket.connect(url);
printTrace('Connected to observatory port: ${result.observatoryPort}.');
return result ? 0 : 2; // Listen for observatory connection close.
observatoryConnection.listen((dynamic data) {
// Ignore observatory messages.
}, onDone: () {
loggingSubscription.cancel();
printStatus('Application finished.');
complete(0);
});
}
printStatus('Application running.');
// When terminating, close down the log reader.
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
loggingSubscription.cancel();
printStatus('');
complete(0);
});
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
loggingSubscription.cancel();
complete(0);
});
return exitCompleter.future;
}
/// Given the value of the --target option, return the path of the Dart file
/// where the app's main function should be.
String findMainDartFile([String target]) {
if (target == null)
target = '';
String targetPath = path.absolute(target);
if (FileSystemEntity.isDirectorySync(targetPath))
return path.join(targetPath, 'lib', 'main.dart');
else
return targetPath;
} }
/// Delay until the Observatory / service protocol is available. /// Delay until the Observatory / service protocol is available.
...@@ -243,10 +381,9 @@ Future<Null> delayUntilObservatoryAvailable(String host, int port, { ...@@ -243,10 +381,9 @@ Future<Null> delayUntilObservatoryAvailable(String host, int port, {
}) async { }) async {
printTrace('Waiting until Observatory is available (port $port).'); printTrace('Waiting until Observatory is available (port $port).');
Stopwatch stopwatch = new Stopwatch()..start();
final String url = 'ws://$host:$port/ws'; final String url = 'ws://$host:$port/ws';
printTrace('Looking for the observatory at $url.'); printTrace('Looking for the observatory at $url.');
Stopwatch stopwatch = new Stopwatch()..start();
while (stopwatch.elapsed <= timeout) { while (stopwatch.elapsed <= timeout) {
try { try {
...@@ -262,11 +399,21 @@ Future<Null> delayUntilObservatoryAvailable(String host, int port, { ...@@ -262,11 +399,21 @@ Future<Null> delayUntilObservatoryAvailable(String host, int port, {
printTrace('Unable to connect to the observatory.'); printTrace('Unable to connect to the observatory.');
} }
String _getMissingPackageHintForPlatform(TargetPlatform platform) {
switch (platform) {
case TargetPlatform.android_arm:
case TargetPlatform.android_x64:
return 'Is your project missing an android/AndroidManifest.xml?';
case TargetPlatform.ios:
return 'Is your project missing an ios/Info.plist?';
default:
return null;
}
}
/// Return a relative path if [fullPath] is contained by the cwd, else return an /// Return a relative path if [fullPath] is contained by the cwd, else return an
/// absolute path. /// absolute path.
String _getDisplayPath(String fullPath) { String _getDisplayPath(String fullPath) {
String cwd = Directory.current.path + Platform.pathSeparator; String cwd = Directory.current.path + Platform.pathSeparator;
if (fullPath.startsWith(cwd)) return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath;
return fullPath.substring(cwd.length);
return fullPath;
} }
...@@ -16,7 +16,7 @@ class SkiaCommand extends FlutterCommand { ...@@ -16,7 +16,7 @@ class SkiaCommand extends FlutterCommand {
argParser.addOption('output-file', help: 'Write the Skia picture file to this path.'); argParser.addOption('output-file', help: 'Write the Skia picture file to this path.');
argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.'); argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.');
argParser.addOption('diagnostic-port', argParser.addOption('diagnostic-port',
defaultsTo: diagnosticDefaultPort.toString(), defaultsTo: defaultDiagnosticPort.toString(),
help: 'Local port where the diagnostic server is listening.'); help: 'Local port where the diagnostic server is listening.');
} }
......
...@@ -18,7 +18,7 @@ class TraceCommand extends FlutterCommand { ...@@ -18,7 +18,7 @@ class TraceCommand extends FlutterCommand {
argParser.addOption('duration', argParser.addOption('duration',
defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.'); defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.');
argParser.addOption('debug-port', argParser.addOption('debug-port',
defaultsTo: observatoryDefaultPort.toString(), defaultsTo: defaultObservatoryPort.toString(),
help: 'Local port where the observatory is listening.'); help: 'Local port where the observatory is listening.');
} }
......
...@@ -10,6 +10,7 @@ import 'android/android_device.dart'; ...@@ -10,6 +10,7 @@ import 'android/android_device.dart';
import 'application_package.dart'; import 'application_package.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'base/os.dart';
import 'build_configuration.dart'; import 'build_configuration.dart';
import 'globals.dart'; import 'globals.dart';
import 'ios/devices.dart'; import 'ios/devices.dart';
...@@ -176,16 +177,12 @@ abstract class Device { ...@@ -176,16 +177,12 @@ abstract class Device {
/// ///
/// [platformArgs] allows callers to pass platform-specific arguments to the /// [platformArgs] allows callers to pass platform-specific arguments to the
/// start call. /// start call.
Future<bool> startApp( Future<LaunchResult> startApp(
ApplicationPackage package, ApplicationPackage package,
Toolchain toolchain, { Toolchain toolchain, {
String mainPath, String mainPath,
String route, String route,
bool checked: true, DebuggingOptions debuggingOptions,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}); });
...@@ -229,6 +226,68 @@ abstract class Device { ...@@ -229,6 +226,68 @@ abstract class Device {
} }
} }
class DebuggingOptions {
DebuggingOptions.enabled({
this.checked: true,
this.startPaused: false,
this.observatoryPort,
this.diagnosticPort
}) : debuggingEnabled = true;
DebuggingOptions.disabled() :
debuggingEnabled = false,
checked = false,
startPaused = false,
observatoryPort = null,
diagnosticPort = null;
final bool debuggingEnabled;
final bool checked;
final bool startPaused;
final int observatoryPort;
final int diagnosticPort;
bool get hasObservatoryPort => observatoryPort != null;
/// Return the user specified observatory port. If that isn't available,
/// return [defaultObservatoryPort], or a port close to that one.
Future<int> findBestObservatoryPort() {
if (hasObservatoryPort)
return new Future<int>.value(observatoryPort);
return findPreferredPort(observatoryPort ?? defaultObservatoryPort);
}
bool get hasDiagnosticPort => diagnosticPort != null;
/// Return the user specified diagnostic port. If that isn't available,
/// return [defaultObservatoryPort], or a port close to that one.
Future<int> findBestDiagnosticPort() {
return findPreferredPort(diagnosticPort ?? defaultDiagnosticPort);
}
}
class LaunchResult {
LaunchResult.succeeded({ this.observatoryPort, this.diagnosticPort }) : started = true;
LaunchResult.failed() : started = false, observatoryPort = null, diagnosticPort = null;
bool get hasObservatory => observatoryPort != null;
final bool started;
final int observatoryPort;
final int diagnosticPort;
@override
String toString() {
StringBuffer buf = new StringBuffer('started=$started');
if (observatoryPort != null)
buf.write(', observatory=$observatoryPort');
if (diagnosticPort != null)
buf.write(', diagnostic=$diagnosticPort');
return buf.toString();
}
}
class ForwardedPort { class ForwardedPort {
ForwardedPort(this.hostPort, this.devicePort); ForwardedPort(this.hostPort, this.devicePort);
...@@ -248,40 +307,18 @@ abstract class DevicePortForwarder { ...@@ -248,40 +307,18 @@ abstract class DevicePortForwarder {
/// Forward [hostPort] on the host to [devicePort] on the device. /// Forward [hostPort] on the host to [devicePort] on the device.
/// If [hostPort] is null, will auto select a host port. /// If [hostPort] is null, will auto select a host port.
/// Returns a Future that completes with the host port. /// Returns a Future that completes with the host port.
Future<int> forward(int devicePort, {int hostPort: null}); Future<int> forward(int devicePort, { int hostPort: null });
/// Stops forwarding [forwardedPort]. /// Stops forwarding [forwardedPort].
Future<Null> unforward(ForwardedPort forwardedPort); Future<Null> unforward(ForwardedPort forwardedPort);
} }
/// Read the log for a particular device. Subclasses must implement `hashCode` /// Read the log for a particular device.
/// and `operator ==` so that log readers that read from the same location can be
/// de-duped. For example, two Android devices will both try and log using
/// `adb logcat`; we don't want to display two identical log streams.
abstract class DeviceLogReader { abstract class DeviceLogReader {
String get name; String get name;
/// A broadcast stream where each element in the string is a line of log /// A broadcast stream where each element in the string is a line of log output.
/// output. Stream<String> get logLines;
Stream<String> get lines;
/// Start reading logs from the device.
Future<Null> start();
/// Actively reading lines from the log?
bool get isReading;
/// Actively stop reading logs from the device.
Future<Null> stop();
/// Completes when the log is finished.
Future<int> get finished;
@override
int get hashCode;
@override
bool operator ==(dynamic other);
@override @override
String toString() => name; String toString() => name;
......
...@@ -9,7 +9,6 @@ import 'dart:io'; ...@@ -9,7 +9,6 @@ import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_configuration.dart'; import '../build_configuration.dart';
...@@ -154,19 +153,15 @@ class IOSDevice extends Device { ...@@ -154,19 +153,15 @@ class IOSDevice extends Device {
} }
@override @override
Future<bool> startApp( Future<LaunchResult> startApp(
ApplicationPackage app, ApplicationPackage app,
Toolchain toolchain, { Toolchain toolchain, {
String mainPath, String mainPath,
String route, String route,
bool checked: true, DebuggingOptions debuggingOptions,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}) async { }) async {
// TODO(chinmaygarde): Use checked, mainPath, route, clearLogs. // TODO(chinmaygarde): Use checked, mainPath, route.
// TODO(devoncarew): Handle startPaused, debugPort. // TODO(devoncarew): Handle startPaused, debugPort.
printTrace('Building ${app.name} for $id'); printTrace('Building ${app.name} for $id');
...@@ -174,7 +169,7 @@ class IOSDevice extends Device { ...@@ -174,7 +169,7 @@ class IOSDevice extends Device {
bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true); bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true);
if (!buildResult) { if (!buildResult) {
printError('Could not build the precompiled application for the device.'); printError('Could not build the precompiled application for the device.');
return false; return new LaunchResult.failed();
} }
// Step 2: Check that the application exists at the specified path. // Step 2: Check that the application exists at the specified path.
...@@ -182,7 +177,7 @@ class IOSDevice extends Device { ...@@ -182,7 +177,7 @@ class IOSDevice extends Device {
bool bundleExists = bundle.existsSync(); bool bundleExists = bundle.existsSync();
if (!bundleExists) { if (!bundleExists) {
printError('Could not find the built application bundle at ${bundle.path}.'); printError('Could not find the built application bundle at ${bundle.path}.');
return false; return new LaunchResult.failed();
} }
// Step 3: Attempt to install the application on the device. // Step 3: Attempt to install the application on the device.
...@@ -197,10 +192,10 @@ class IOSDevice extends Device { ...@@ -197,10 +192,10 @@ class IOSDevice extends Device {
if (installationResult != 0) { if (installationResult != 0) {
printError('Could not install ${bundle.path} on $id.'); printError('Could not install ${bundle.path} on $id.');
return false; return new LaunchResult.failed();
} }
return true; return new LaunchResult.succeeded();
} }
@override @override
...@@ -266,90 +261,46 @@ class IOSDevice extends Device { ...@@ -266,90 +261,46 @@ class IOSDevice extends Device {
} }
class _IOSDeviceLogReader extends DeviceLogReader { class _IOSDeviceLogReader extends DeviceLogReader {
_IOSDeviceLogReader(this.device); _IOSDeviceLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: _start,
onCancel: _stop
);
}
final IOSDevice device; final IOSDevice device;
final StreamController<String> _linesStreamController = StreamController<String> _linesController;
new StreamController<String>.broadcast();
Process _process; Process _process;
StreamSubscription<String> _stdoutSubscription;
StreamSubscription<String> _stderrSubscription;
@override @override
Stream<String> get lines => _linesStreamController.stream; Stream<String> get logLines => _linesController.stream;
@override @override
String get name => device.name; String get name => device.name;
@override void _start() {
bool get isReading => _process != null; runCommand(<String>[device.loggerPath]).then((Process process) {
_process = process;
_process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
_process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
@override _process.exitCode.then((int code) {
Future<int> get finished { if (_linesController.hasListener)
return _process != null ? _process.exitCode : new Future<int>.value(0); _linesController.close();
} });
});
@override
Future<Null> 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);
}
@override
Future<Null> 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'); static final RegExp _runnerRegex = new RegExp(r'FlutterRunner');
void _onLine(String line) { void _onLine(String line) {
if (!_runnerRegex.hasMatch(line)) if (_runnerRegex.hasMatch(line))
return; _linesController.add(line);
_linesStreamController.add(line);
} }
@override void _stop() {
int get hashCode => name.hashCode; _process?.kill();
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _IOSDeviceLogReader)
return false;
return other.name == name;
} }
} }
......
...@@ -9,7 +9,6 @@ import 'dart:io'; ...@@ -9,7 +9,6 @@ import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_configuration.dart'; import '../build_configuration.dart';
...@@ -437,32 +436,25 @@ class IOSSimulator extends Device { ...@@ -437,32 +436,25 @@ class IOSSimulator extends Device {
} }
@override @override
Future<bool> startApp( Future<LaunchResult> startApp(
ApplicationPackage app, ApplicationPackage app,
Toolchain toolchain, { Toolchain toolchain, {
String mainPath, String mainPath,
String route, String route,
bool checked: true, DebuggingOptions debuggingOptions,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
Map<String, dynamic> platformArgs Map<String, dynamic> platformArgs
}) async { }) async {
printTrace('Building ${app.name} for $id.'); printTrace('Building ${app.name} for $id.');
if (clearLogs)
this.clearLogs();
if (!(await _setupUpdatedApplicationBundle(app, toolchain))) if (!(await _setupUpdatedApplicationBundle(app, toolchain)))
return false; return new LaunchResult.failed();
ServiceProtocolDiscovery serviceProtocolDiscovery = ServiceProtocolDiscovery observatoryDiscovery;
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService);
// We take this future here but do not wait for completion until *after* we if (debuggingOptions.debuggingEnabled) {
// start the application. observatoryDiscovery = new ServiceProtocolDiscovery(
Future<int> scrapeServicePort = serviceProtocolDiscovery.nextPort(); logReader, ServiceProtocolDiscovery.kObservatoryService);
}
// Prepare launch arguments. // Prepare launch arguments.
List<String> args = <String>[ List<String> args = <String>[
...@@ -471,39 +463,47 @@ class IOSSimulator extends Device { ...@@ -471,39 +463,47 @@ class IOSSimulator extends Device {
"--packages=${path.absolute('.packages')}", "--packages=${path.absolute('.packages')}",
]; ];
if (checked) if (debuggingOptions.debuggingEnabled) {
if (debuggingOptions.checked)
args.add("--enable-checked-mode"); args.add("--enable-checked-mode");
if (debuggingOptions.startPaused)
if (startPaused)
args.add("--start-paused"); args.add("--start-paused");
if (observatoryPort != observatoryDefaultPort) int observatoryPort = await debuggingOptions.findBestObservatoryPort();
args.add("--observatory-port=$observatoryPort"); args.add("--observatory-port=$observatoryPort");
}
// Launch the updated application in the simulator. // Launch the updated application in the simulator.
try { try {
SimControl.instance.launch(id, app.id, args); SimControl.instance.launch(id, app.id, args);
} catch (error) { } catch (error) {
printError('$error'); printError('$error');
return false; return new LaunchResult.failed();
} }
if (!debuggingOptions.debuggingEnabled) {
return new LaunchResult.succeeded();
} else {
// Wait for the service protocol port here. This will complete once the // Wait for the service protocol port here. This will complete once the
// device has printed "Observatory is listening on..." // device has printed "Observatory is listening on..."
printTrace('Waiting for observatory port to be available...'); printTrace('Waiting for observatory port to be available...');
try { try {
int devicePort = await scrapeServicePort.timeout(new Duration(seconds: 12)); int devicePort = await observatoryDiscovery
.nextPort()
.timeout(new Duration(seconds: 20));
printTrace('service protocol port = $devicePort'); printTrace('service protocol port = $devicePort');
printStatus('Observatory listening on http://127.0.0.1:$devicePort'); printStatus('Observatory listening on http://127.0.0.1:$devicePort');
return true; return new LaunchResult.succeeded(observatoryPort: devicePort);
} catch (error) { } catch (error) {
if (error is TimeoutException) if (error is TimeoutException)
printError('Timed out while waiting for a debug connection.'); printError('Timed out while waiting for a debug connection.');
else else
printError('Error waiting for a debug connection: $error'); printError('Error waiting for a debug connection: $error');
return new LaunchResult.failed();
return false; } finally {
observatoryDiscovery.cancel();
}
} }
} }
...@@ -662,118 +662,57 @@ class IOSSimulator extends Device { ...@@ -662,118 +662,57 @@ class IOSSimulator extends Device {
} }
class _IOSSimulatorLogReader extends DeviceLogReader { class _IOSSimulatorLogReader extends DeviceLogReader {
_IOSSimulatorLogReader(this.device); _IOSSimulatorLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: () {
_start();
},
onCancel: _stop
);
}
final IOSSimulator device; final IOSSimulator device;
final StreamController<String> _linesStreamController = StreamController<String> _linesController;
new StreamController<String>.broadcast();
bool _lastWasFiltered = false; bool _lastWasFiltered = false;
// We log from two logs: the device and the system log. // We log from two files: the device and the system log.
Process _deviceProcess; Process _deviceProcess;
StreamSubscription<String> _deviceStdoutSubscription;
StreamSubscription<String> _deviceStderrSubscription;
Process _systemProcess; Process _systemProcess;
StreamSubscription<String> _systemStdoutSubscription;
StreamSubscription<String> _systemStderrSubscription;
@override @override
Stream<String> get lines => _linesStreamController.stream; Stream<String> get logLines => _linesController.stream;
@override @override
String get name => device.name; String get name => device.name;
@override Future<Null> _start() async {
bool get isReading => (_deviceProcess != null) && (_systemProcess != null);
@override
Future<int> get finished {
return (_deviceProcess != null) ? _deviceProcess.exitCode : new Future<int>.value(0);
}
@override
Future<Null> 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 log.
device.ensureLogsExists(); device.ensureLogsExists();
_deviceProcess = await runCommand( _deviceProcess = await runCommand(<String>['tail', '-n', '0', '-F', device.logFilePath]);
<String>['tail', '-n', '+0', '-F', device.logFilePath]); _deviceProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine);
_deviceStdoutSubscription = _deviceProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine);
_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. // Track system.log crashes.
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]... // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
_systemProcess = await runCommand( _systemProcess = await runCommand(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
<String>['tail', '-F', '/private/var/log/system.log']); _systemProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine);
_systemStdoutSubscription = _systemProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine);
_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);
}
@override _deviceProcess.exitCode.then((int code) {
Future<Null> stop() async { if (_linesController.hasListener)
if (!isReading) { _linesController.close();
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): // Match the log prefix (in order to shorten it):
// 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...' // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
final RegExp _mapRegex = static final RegExp _mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
// Jan 31 19:23:28 --- last message repeated 1 time --- // Jan 31 19:23:28 --- last message repeated 1 time ---
final RegExp _lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$'); static final RegExp _lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] '); static final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] ');
String _filterDeviceLine(String string) { String _filterDeviceLine(String string) {
Match match = _mapRegex.matchAsPrefix(string); Match match = _mapRegex.matchAsPrefix(string);
...@@ -808,8 +747,7 @@ class _IOSSimulatorLogReader extends DeviceLogReader { ...@@ -808,8 +747,7 @@ class _IOSSimulatorLogReader extends DeviceLogReader {
String filteredLine = _filterDeviceLine(line); String filteredLine = _filterDeviceLine(line);
if (filteredLine == null) if (filteredLine == null)
return; return;
_linesController.add(filteredLine);
_linesStreamController.add(filteredLine);
} }
String _filterSystemLog(String string) { String _filterSystemLog(String string) {
...@@ -825,19 +763,12 @@ class _IOSSimulatorLogReader extends DeviceLogReader { ...@@ -825,19 +763,12 @@ class _IOSSimulatorLogReader extends DeviceLogReader {
if (filteredLine == null) if (filteredLine == null)
return; return;
_linesStreamController.add(filteredLine); _linesController.add(filteredLine);
} }
@override void _stop() {
int get hashCode => device.logFilePath.hashCode; _deviceProcess?.kill();
_systemProcess?.kill();
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _IOSSimulatorLogReader)
return false;
return other.device.logFilePath == device.logFilePath;
} }
} }
......
...@@ -8,28 +8,30 @@ import 'device.dart'; ...@@ -8,28 +8,30 @@ import 'device.dart';
/// Discover service protocol ports on devices. /// Discover service protocol ports on devices.
class ServiceProtocolDiscovery { class ServiceProtocolDiscovery {
static const String kObservatoryService = 'Observatory'; /// [logReader] - a [DeviceLogReader] to look for service messages in.
static const String kDiagnosticService = 'Diagnostic server';
/// [logReader] A [DeviceLogReader] to look for service messages in.
ServiceProtocolDiscovery(DeviceLogReader logReader, String serviceName) ServiceProtocolDiscovery(DeviceLogReader logReader, String serviceName)
: _logReader = logReader, : _logReader = logReader, _serviceName = serviceName {
_serviceName = serviceName {
assert(_logReader != null); assert(_logReader != null);
if (!_logReader.isReading) _subscription = _logReader.logLines.listen(_onLine);
_logReader.start();
_logReader.lines.listen(_onLine);
} }
static const String kObservatoryService = 'Observatory';
static const String kDiagnosticService = 'Diagnostic server';
final DeviceLogReader _logReader; final DeviceLogReader _logReader;
final String _serviceName; final String _serviceName;
Completer<int> _completer = new Completer<int>(); Completer<int> _completer = new Completer<int>();
StreamSubscription<String> _subscription;
/// The [Future] returned by this function will complete when the next service /// The [Future] returned by this function will complete when the next service
/// protocol port is found. /// protocol port is found.
Future<int> nextPort() => _completer.future; Future<int> nextPort() => _completer.future;
void cancel() {
_subscription.cancel();
}
void _onLine(String line) { void _onLine(String line) {
int portNumber = 0; int portNumber = 0;
if (line.contains('$_serviceName listening on http://')) { if (line.contains('$_serviceName listening on http://')) {
...@@ -48,6 +50,7 @@ class ServiceProtocolDiscovery { ...@@ -48,6 +50,7 @@ class ServiceProtocolDiscovery {
void _located(int port) { void _located(int port) {
assert(_completer != null); assert(_completer != null);
assert(!_completer.isCompleted); assert(!_completer.isCompleted);
_completer.complete(port); _completer.complete(port);
_completer = new Completer<int>(); _completer = new Completer<int>();
} }
......
...@@ -26,6 +26,9 @@ class SnapshotCompiler { ...@@ -26,6 +26,9 @@ class SnapshotCompiler {
String depfilePath, String depfilePath,
String buildOutputPath String buildOutputPath
}) { }) {
assert(mainPath != null);
assert(snapshotPath != null);
final List<String> args = [ final List<String> args = [
_path, _path,
mainPath, mainPath,
......
...@@ -15,36 +15,39 @@ void main() { ...@@ -15,36 +15,39 @@ void main() {
MockDeviceLogReader logReader = new MockDeviceLogReader(); MockDeviceLogReader logReader = new MockDeviceLogReader();
ServiceProtocolDiscovery discoverer = ServiceProtocolDiscovery discoverer =
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService); new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService);
// Get next port future. // Get next port future.
Future<int> nextPort = discoverer.nextPort(); Future<int> nextPort = discoverer.nextPort();
expect(nextPort, isNotNull); expect(nextPort, isNotNull);
// Inject some lines. // Inject some lines.
logReader.addLine('HELLO WORLD'); logReader.addLine('HELLO WORLD');
logReader.addLine( logReader.addLine('Observatory listening on http://127.0.0.1:9999');
'Observatory listening on http://127.0.0.1:9999');
// Await the port. // Await the port.
expect(await nextPort, 9999); expect(await nextPort, 9999);
// Get next port future. // Get next port future.
nextPort = discoverer.nextPort(); nextPort = discoverer.nextPort();
logReader.addLine( logReader.addLine('Observatory listening on http://127.0.0.1:3333');
'Observatory listening on http://127.0.0.1:3333');
expect(await nextPort, 3333); expect(await nextPort, 3333);
// Get next port future. // Get next port future.
nextPort = discoverer.nextPort(); nextPort = discoverer.nextPort();
// Inject some bad lines. // Inject some bad lines.
logReader.addLine('Observatory listening on http://127.0.0.1'); logReader.addLine('Observatory listening on http://127.0.0.1');
logReader.addLine('Observatory listening on http://127.0.0.1:'); logReader.addLine('Observatory listening on http://127.0.0.1:');
logReader.addLine( logReader.addLine('Observatory listening on http://127.0.0.1:apple');
'Observatory listening on http://127.0.0.1:apple');
int port = await nextPort.timeout( int port = await nextPort.timeout(
const Duration(milliseconds: 100), onTimeout: () => 77); const Duration(milliseconds: 100), onTimeout: () => 77);
// Expect the timeout port. // Expect the timeout port.
expect(port, 77); expect(port, 77);
// Get next port future. // Get next port future.
nextPort = discoverer.nextPort(); nextPort = discoverer.nextPort();
logReader.addLine( logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:52584');
'I/flutter : Observatory listening on http://127.0.0.1:52584');
expect(await nextPort, 52584); expect(await nextPort, 52584);
discoverer.cancel();
}); });
}); });
} }
...@@ -63,38 +63,12 @@ class MockDeviceLogReader extends DeviceLogReader { ...@@ -63,38 +63,12 @@ class MockDeviceLogReader extends DeviceLogReader {
@override @override
String get name => 'MockLogReader'; String get name => 'MockLogReader';
final StreamController<String> _linesStreamController = final StreamController<String> _linesController = new StreamController<String>.broadcast();
new StreamController<String>.broadcast();
final Completer<int> _finishedCompleter = new Completer<int>();
@override
Stream<String> get lines => _linesStreamController.stream;
void addLine(String line) {
_linesStreamController.add(line);
}
bool _started = false;
@override @override
Future<Null> start() async { Stream<String> get logLines => _linesController.stream;
assert(!_started);
_started = true;
}
@override void addLine(String line) => _linesController.add(line);
bool get isReading => _started;
@override
Future<Null> stop() {
assert(_started);
_started = false;
return new Future<Null>.value();
}
@override
Future<int> get finished => _finishedCompleter.future;
} }
void applyMocksToCommand(FlutterCommand command) { void applyMocksToCommand(FlutterCommand command) {
......
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