Flutter run (#3553) * rework flutter run * fix npe with --debug-port * connect to obs and exit when that conneciton closes * update todos
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 31694edf..0286e19 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -12,7 +12,6 @@ import '../android/android_sdk.dart'; import '../application_package.dart'; -import '../base/common.dart'; import '../base/os.dart'; import '../base/process.dart'; import '../base/utils.dart'; @@ -189,49 +188,38 @@ } Future<Null> _forwardPort(String service, int devicePort, int port) async { - bool portWasZero = (port == null) || (port == 0); - try { // Set up port forwarding for observatory. - port = await portForwarder.forward(devicePort, - hostPort: port); - if (portWasZero) - printStatus('$service listening on http://127.0.0.1:$port'); + port = await portForwarder.forward(devicePort, hostPort: port); + printStatus('$service listening on http://127.0.0.1:$port'); } catch (e) { printError('Unable to forward port $port: $e'); } } - Future<bool> startBundle(AndroidApk apk, String bundlePath, { - bool checked: true, + Future<LaunchResult> startBundle(AndroidApk apk, String bundlePath, { bool traceStartup: false, String route, - bool clearLogs: false, - bool startPaused: false, - int observatoryPort: observatoryDefaultPort, - int diagnosticPort: diagnosticDefaultPort + DebuggingOptions options }) async { printTrace('$this startBundle'); if (!FileSystemEntity.isFileSync(bundlePath)) { printError('Cannot find $bundlePath'); - return false; + return new LaunchResult.failed(); } - if (clearLogs) - this.clearLogs(); - runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath])); - ServiceProtocolDiscovery observatoryDiscovery = - new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService); - ServiceProtocolDiscovery diagnosticDiscovery = - new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kDiagnosticService); + ServiceProtocolDiscovery observatoryDiscovery; + ServiceProtocolDiscovery diagnosticDiscovery; - // We take this future here but do not wait for completion until *after* we - // start the bundle. - Future<List<int>> scrapeServicePorts = Future.wait( - <Future<int>>[observatoryDiscovery.nextPort(), diagnosticDiscovery.nextPort()]); + if (options.debuggingEnabled) { + observatoryDiscovery = new ServiceProtocolDiscovery( + logReader, ServiceProtocolDiscovery.kObservatoryService); + diagnosticDiscovery = new ServiceProtocolDiscovery( + logReader, ServiceProtocolDiscovery.kDiagnosticService); + } List<String> cmd = adbCommandForDevice(<String>[ 'shell', 'am', 'start', @@ -240,61 +228,76 @@ '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP '--ez', 'enable-background-compilation', 'true', ]); - if (checked) - cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']); if (traceStartup) cmd.addAll(<String>['--ez', 'trace-startup', 'true']); - if (startPaused) - cmd.addAll(<String>['--ez', 'start-paused', 'true']); if (route != null) 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); String result = runCheckedSync(cmd); // This invocation returns 0 even when it fails. if (result.contains('Error: ')) { printError(result.trim()); - return false; + return new LaunchResult.failed(); } - // Wait for the service protocol port here. This will complete once the - // device has printed "Observatory is listening on...". - printTrace('Waiting for observatory port to be available...'); + if (!options.debuggingEnabled) { + return new LaunchResult.succeeded(); + } else { + // Wait for the service protocol port here. This will complete once the + // device has printed "Observatory is listening on...". + printTrace('Waiting for observatory port to be available...'); - try { - List<int> devicePorts = await scrapeServicePorts.timeout(new Duration(seconds: 12)); - int observatoryDevicePort = devicePorts[0]; - int diagnosticDevicePort = devicePorts[1]; - printTrace('observatory port = $observatoryDevicePort'); - await _forwardPort(ServiceProtocolDiscovery.kObservatoryService, - observatoryDevicePort, observatoryPort); - await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService, - diagnosticDevicePort, diagnosticPort); - return true; - } catch (error) { - if (error is TimeoutException) - printError('Timed out while waiting for a debug connection.'); - else - printError('Error waiting for a debug connection: $error'); - - return false; + try { + 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]; + 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, + observatoryDevicePort, observatoryLocalPort); + int diagnosticDevicePort = devicePorts[1]; + printTrace('diagnostic port = $diagnosticDevicePort'); + int diagnosticLocalPort = await options.findBestDiagnosticPort(); + await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService, + diagnosticDevicePort, diagnosticLocalPort); + return new LaunchResult.succeeded( + observatoryPort: observatoryLocalPort, + diagnosticPort: diagnosticLocalPort + ); + } catch (error) { + if (error is TimeoutException) + printError('Timed out while waiting for a debug connection.'); + else + printError('Error waiting for a debug connection: $error'); + return new LaunchResult.failed(); + } finally { + observatoryDiscovery.cancel(); + diagnosticDiscovery.cancel(); + } } } @override - Future<bool> startApp( + Future<LaunchResult> startApp( ApplicationPackage package, Toolchain toolchain, { String mainPath, String route, - bool checked: true, - bool clearLogs: false, - bool startPaused: false, - int observatoryPort: observatoryDefaultPort, - int diagnosticPort: diagnosticDefaultPort, + DebuggingOptions debuggingOptions, Map<String, dynamic> platformArgs }) async { if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion()) - return false; + return new LaunchResult.failed(); String localBundlePath = await flx.buildFlx( toolchain, @@ -304,21 +307,13 @@ printTrace('Starting bundle for $this.'); - if (await startBundle( + return startBundle( package, localBundlePath, - checked: checked, traceStartup: platformArgs['trace-startup'], route: route, - clearLogs: clearLogs, - startPaused: startPaused, - observatoryPort: observatoryPort, - diagnosticPort: diagnosticPort - )) { - return true; - } else { - return false; - } + options: debuggingOptions + ); } @override @@ -468,7 +463,8 @@ // 0149947A0D01500C device usb:340787200X // 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) { // Skip lines like: * daemon started successfully * @@ -522,34 +518,25 @@ /// A log reader that logs from `adb logcat`. class _AdbLogReader extends DeviceLogReader { - _AdbLogReader(this.device); + _AdbLogReader(this.device) { + _linesController = new StreamController<String>.broadcast( + onListen: _start, + onCancel: _stop + ); + } final AndroidDevice device; - final StreamController<String> _linesStreamController = - new StreamController<String>.broadcast(); - + StreamController<String> _linesController; Process _process; - StreamSubscription<String> _stdoutSubscription; - StreamSubscription<String> _stderrSubscription; @override - Stream<String> get lines => _linesStreamController.stream; + Stream<String> get logLines => _linesController.stream; @override String get name => device.name; - @override - 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.'); - + void _start() { // Start the adb logcat process. List<String> args = <String>['logcat', '-v', 'tag']; String lastTimestamp = device.lastLogcatTimestamp; @@ -558,55 +545,29 @@ args.addAll(<String>[ '-s', 'flutter:V', 'FlutterMain:V', 'FlutterView:V', 'AndroidRuntime:W', 'ActivityManager:W', 'System.err:W', '*:F' ]); - _process = await runCommand(device.adbCommandForDevice(args)); - _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); - } + runCommand(device.adbCommandForDevice(args)).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 - 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) { - _stdoutSubscription?.cancel(); - _stdoutSubscription = null; - _stderrSubscription?.cancel(); - _stderrSubscription = null; - _process = null; + _process.exitCode.then((int code) { + if (_linesController.hasListener) + _linesController.close(); + }); + }); } void _onLine(String line) { // Filter out some noisy ActivityManager notifications. if (line.startsWith('W/ActivityManager: getRunningAppProcesses')) return; - - _linesStreamController.add(line); + _linesController.add(line); } - @override - int get hashCode => name.hashCode; + void _stop() { + // TODO(devoncarew): We should remove adb port forwarding here. - @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other is! _AdbLogReader) - return false; - return other.device.id == device.id; + _process?.kill(); } }
diff --git a/packages/flutter_tools/lib/src/base/common.dart b/packages/flutter_tools/lib/src/base/common.dart index 4481953..509f69f 100644 --- a/packages/flutter_tools/lib/src/base/common.dart +++ b/packages/flutter_tools/lib/src/base/common.dart
@@ -2,5 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -const int observatoryDefaultPort = 8181; -const int diagnosticDefaultPort = 8182; +const int defaultObservatoryPort = 8100; +const int defaultDiagnosticPort = 8101; +const int defaultDrivePort = 8183;
diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart index 7bdb35e..e268b6c 100644 --- a/packages/flutter_tools/lib/src/base/os.dart +++ b/packages/flutter_tools/lib/src/base/os.dart
@@ -109,3 +109,30 @@ await socket.close(); 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; + } +}
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 24d8993..ce6e9dc 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -267,8 +267,7 @@ command.toolchain, stop: true, target: args['target'], - route: args['route'], - checked: args['checked'] ?? true + route: args['route'] ); if (result != 0)
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index b4cc893..4a0a995 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -11,6 +11,7 @@ import '../android/android_device.dart' show AndroidDevice; import '../application_package.dart'; import '../base/file_system.dart'; +import '../base/common.dart'; import '../base/os.dart'; import '../device.dart'; import '../globals.dart'; @@ -60,8 +61,9 @@ ); argParser.addOption('debug-port', - defaultsTo: '8183', - help: 'Listen to the given port for a debug connection.'); + defaultsTo: defaultDrivePort.toString(), + help: 'Listen to the given port for a debug connection.' + ); } @override @@ -261,25 +263,22 @@ await command.device.installApp(package); printTrace('Starting application.'); - bool started = await command.device.startApp( + LaunchResult result = await command.device.startApp( package, command.toolchain, mainPath: mainPath, route: command.route, - checked: command.checked, - clearLogs: true, - startPaused: true, - observatoryPort: command.debugPort, + debuggingOptions: new DebuggingOptions.enabled( + checked: command.checked, + startPaused: true, + observatoryPort: command.debugPort + ), platformArgs: <String, dynamic>{ 'trace-startup': command.traceStartup, } ); - if (started && command.device.supportsStartPaused) { - await delayUntilObservatoryAvailable('localhost', command.debugPort); - } - - return started ? 0 : 2; + return result.started ? 0 : 2; } /// Runs driver tests.
diff --git a/packages/flutter_tools/lib/src/commands/listen.dart b/packages/flutter_tools/lib/src/commands/listen.dart index 352bd38..382d219 100644 --- a/packages/flutter_tools/lib/src/commands/listen.dart +++ b/packages/flutter_tools/lib/src/commands/listen.dart
@@ -7,6 +7,7 @@ import '../base/os.dart'; import '../base/process.dart'; +import '../device.dart'; import '../globals.dart'; import 'run.dart'; @@ -62,7 +63,7 @@ target: target, install: firstTime, stop: true, - checked: checked, + debuggingOptions: new DebuggingOptions.enabled(checked: checked), traceStartup: traceStartup, route: route );
diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart index ab82161..a769d97 100644 --- a/packages/flutter_tools/lib/src/commands/logs.dart +++ b/packages/flutter_tools/lib/src/commands/logs.dart
@@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import '../device.dart'; import '../globals.dart'; @@ -40,17 +41,33 @@ printStatus('Showing $logReader logs:'); - // Start reading. - if (!logReader.isReading) - await logReader.start(); + Completer<int> exitCompleter = new Completer<int>(); - StreamSubscription<String> subscription = logReader.lines.listen(printStatus); + // Start reading. + StreamSubscription<String> subscription = logReader.logLines.listen( + printStatus, + onDone: () { + exitCompleter.complete(0); + }, + onError: (dynamic error) { + exitCompleter.complete(error is int ? error : 1); + } + ); + + // 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. - int result = await logReader.finished; - + int result = await exitCompleter.future; subscription.cancel(); - if (result != 0) printError('Error listening to $logReader logs.'); return result;
diff --git a/packages/flutter_tools/lib/src/commands/refresh.dart b/packages/flutter_tools/lib/src/commands/refresh.dart index cef5a1b..d1cfbfe 100644 --- a/packages/flutter_tools/lib/src/commands/refresh.dart +++ b/packages/flutter_tools/lib/src/commands/refresh.dart
@@ -39,7 +39,8 @@ String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin'); int result = await toolchain.compiler.createSnapshot( - mainPath: argResults['target'], snapshotPath: snapshotPath + mainPath: argResults['target'], + snapshotPath: snapshotPath ); if (result != 0) { printError('Failed to run the Flutter compiler. Exit code: $result');
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 7911eb8..419be74 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -17,19 +17,6 @@ import 'build_apk.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 { RunCommandBase() { argParser.addFlag('checked', @@ -66,17 +53,21 @@ argParser.addFlag('full-restart', defaultsTo: true, 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', defaultsTo: false, negatable: false, help: 'Start in a paused mode and wait for a debugger to connect.'); argParser.addOption('debug-port', - defaultsTo: observatoryDefaultPort.toString(), - help: 'Listen to the given port for a debug connection.'); + help: 'Listen to the given port for a debug connection (defaults to $defaultObservatoryPort).'); 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 @@ -95,61 +86,66 @@ @override Future<int> runInProject() async { - bool clearLogs = argResults['clear-logs']; - int debugPort; - try { - debugPort = int.parse(argResults['debug-port']); - } catch (error) { - printError('Invalid port for `--debug-port`: $error'); - return 1; + if (argResults['debug-port'] != null) { + try { + debugPort = int.parse(argResults['debug-port']); + } catch (error) { + printError('Invalid port for `--debug-port`: $error'); + return 1; + } } - int result = await startApp( - deviceForCommand, - toolchain, - target: target, - enginePath: runner.enginePath, - install: true, - stop: argResults['full-restart'], - checked: checked, - traceStartup: traceStartup, - route: route, - clearLogs: clearLogs, - startPaused: argResults['start-paused'], - debugPort: debugPort, - buildMode: getBuildMode() - ); + 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 + ); + } + + if (argResults['resident']) { + result = await startAppStayResident( + deviceForCommand, + toolchain, + target: target, + debuggingOptions: options, + traceStartup: traceStartup, + buildMode: getBuildMode() + ); + } else { + result = await startApp( + deviceForCommand, + toolchain, + target: target, + stop: argResults['full-restart'], + install: true, + debuggingOptions: options, + traceStartup: traceStartup, + route: route, + buildMode: getBuildMode() + ); + } return result; } } -String _getMissingPackageHintForPlatform(TargetPlatform platform) { - 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; - } -} - Future<int> startApp( Device device, Toolchain toolchain, { String target, - String enginePath, bool stop: true, bool install: true, - bool checked: true, + DebuggingOptions debuggingOptions, bool traceStartup: false, String route, - bool clearLogs: false, - bool startPaused: false, - int debugPort: observatoryDefaultPort, BuildMode buildMode: BuildMode.debug }) async { String mainPath = findMainDartFile(target); @@ -216,22 +212,164 @@ printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...'); - bool result = await device.startApp( + LaunchResult result = await device.startApp( package, toolchain, mainPath: mainPath, route: route, - checked: checked, - clearLogs: clearLogs, - startPaused: startPaused, - observatoryPort: debugPort, + debuggingOptions: debuggingOptions, platformArgs: platformArgs ); - if (!result) + if (!result.started) printError('Error running application on ${device.name}.'); - return result ? 0 : 2; + 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 + ); + + if (!result.started) { + 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}.'); + + // 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. @@ -243,10 +381,9 @@ }) async { printTrace('Waiting until Observatory is available (port $port).'); - Stopwatch stopwatch = new Stopwatch()..start(); - final String url = 'ws://$host:$port/ws'; printTrace('Looking for the observatory at $url.'); + Stopwatch stopwatch = new Stopwatch()..start(); while (stopwatch.elapsed <= timeout) { try { @@ -262,11 +399,21 @@ 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 /// absolute path. String _getDisplayPath(String fullPath) { String cwd = Directory.current.path + Platform.pathSeparator; - if (fullPath.startsWith(cwd)) - return fullPath.substring(cwd.length); - return fullPath; + return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath; }
diff --git a/packages/flutter_tools/lib/src/commands/skia.dart b/packages/flutter_tools/lib/src/commands/skia.dart index 1f1bb8c..e5a6431 100644 --- a/packages/flutter_tools/lib/src/commands/skia.dart +++ b/packages/flutter_tools/lib/src/commands/skia.dart
@@ -16,7 +16,7 @@ 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('diagnostic-port', - defaultsTo: diagnosticDefaultPort.toString(), + defaultsTo: defaultDiagnosticPort.toString(), help: 'Local port where the diagnostic server is listening.'); }
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart index 792e064..73203a1 100644 --- a/packages/flutter_tools/lib/src/commands/trace.dart +++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -18,7 +18,7 @@ argParser.addOption('duration', defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.'); argParser.addOption('debug-port', - defaultsTo: observatoryDefaultPort.toString(), + defaultsTo: defaultObservatoryPort.toString(), help: 'Local port where the observatory is listening.'); }
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index e2ba8d9..575472e 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart
@@ -10,6 +10,7 @@ import 'application_package.dart'; import 'base/common.dart'; import 'base/utils.dart'; +import 'base/os.dart'; import 'build_configuration.dart'; import 'globals.dart'; import 'ios/devices.dart'; @@ -176,16 +177,12 @@ /// /// [platformArgs] allows callers to pass platform-specific arguments to the /// start call. - Future<bool> startApp( + Future<LaunchResult> startApp( ApplicationPackage package, Toolchain toolchain, { String mainPath, String route, - bool checked: true, - bool clearLogs: false, - bool startPaused: false, - int observatoryPort: observatoryDefaultPort, - int diagnosticPort: diagnosticDefaultPort, + DebuggingOptions debuggingOptions, Map<String, dynamic> platformArgs }); @@ -229,6 +226,68 @@ } } +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 { ForwardedPort(this.hostPort, this.devicePort); @@ -248,40 +307,18 @@ /// Forward [hostPort] on the host to [devicePort] on the device. /// If [hostPort] is null, will auto select a 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]. Future<Null> unforward(ForwardedPort forwardedPort); } -/// Read the log for a particular device. Subclasses must implement `hashCode` -/// 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. +/// Read the log for a particular device. abstract class DeviceLogReader { String get name; - /// 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<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); + /// A broadcast stream where each element in the string is a line of log output. + Stream<String> get logLines; @override String toString() => name;
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index f15cad6..00b9d6a 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -9,7 +9,6 @@ import 'package:path/path.dart' as path; import '../application_package.dart'; -import '../base/common.dart'; import '../base/os.dart'; import '../base/process.dart'; import '../build_configuration.dart'; @@ -154,19 +153,15 @@ } @override - Future<bool> startApp( + Future<LaunchResult> startApp( ApplicationPackage app, Toolchain toolchain, { String mainPath, String route, - bool checked: true, - bool clearLogs: false, - bool startPaused: false, - int observatoryPort: observatoryDefaultPort, - int diagnosticPort: diagnosticDefaultPort, + DebuggingOptions debuggingOptions, Map<String, dynamic> platformArgs }) async { - // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs. + // TODO(chinmaygarde): Use checked, mainPath, route. // TODO(devoncarew): Handle startPaused, debugPort. printTrace('Building ${app.name} for $id'); @@ -174,7 +169,7 @@ bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true); if (!buildResult) { 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. @@ -182,7 +177,7 @@ bool bundleExists = bundle.existsSync(); if (!bundleExists) { 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. @@ -197,10 +192,10 @@ if (installationResult != 0) { printError('Could not install ${bundle.path} on $id.'); - return false; + return new LaunchResult.failed(); } - return true; + return new LaunchResult.succeeded(); } @override @@ -266,90 +261,46 @@ } class _IOSDeviceLogReader extends DeviceLogReader { - _IOSDeviceLogReader(this.device); + _IOSDeviceLogReader(this.device) { + _linesController = new StreamController<String>.broadcast( + onListen: _start, + onCancel: _stop + ); + } final IOSDevice device; - final StreamController<String> _linesStreamController = - new StreamController<String>.broadcast(); - + StreamController<String> _linesController; Process _process; - StreamSubscription<String> _stdoutSubscription; - StreamSubscription<String> _stderrSubscription; @override - Stream<String> get lines => _linesStreamController.stream; + Stream<String> get logLines => _linesController.stream; @override String get name => device.name; - @override - bool get isReading => _process != null; + void _start() { + 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 - Future<int> get finished { - return _process != null ? _process.exitCode : new Future<int>.value(0); + _process.exitCode.then((int code) { + if (_linesController.hasListener) + _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) { - if (!_runnerRegex.hasMatch(line)) - return; - - _linesStreamController.add(line); + if (_runnerRegex.hasMatch(line)) + _linesController.add(line); } - @override - int get hashCode => name.hashCode; - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other is! _IOSDeviceLogReader) - return false; - return other.name == name; + void _stop() { + _process?.kill(); } }
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 043bc59..a55a74a 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -9,7 +9,6 @@ import 'package:path/path.dart' as path; import '../application_package.dart'; -import '../base/common.dart'; import '../base/context.dart'; import '../base/process.dart'; import '../build_configuration.dart'; @@ -437,32 +436,25 @@ } @override - Future<bool> startApp( + Future<LaunchResult> startApp( ApplicationPackage app, Toolchain toolchain, { String mainPath, String route, - bool checked: true, - bool clearLogs: false, - bool startPaused: false, - int observatoryPort: observatoryDefaultPort, - int diagnosticPort: diagnosticDefaultPort, + DebuggingOptions debuggingOptions, Map<String, dynamic> platformArgs }) async { printTrace('Building ${app.name} for $id.'); - if (clearLogs) - this.clearLogs(); - if (!(await _setupUpdatedApplicationBundle(app, toolchain))) - return false; + return new LaunchResult.failed(); - ServiceProtocolDiscovery serviceProtocolDiscovery = - new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService); + ServiceProtocolDiscovery observatoryDiscovery; - // We take this future here but do not wait for completion until *after* we - // start the application. - Future<int> scrapeServicePort = serviceProtocolDiscovery.nextPort(); + if (debuggingOptions.debuggingEnabled) { + observatoryDiscovery = new ServiceProtocolDiscovery( + logReader, ServiceProtocolDiscovery.kObservatoryService); + } // Prepare launch arguments. List<String> args = <String>[ @@ -471,39 +463,47 @@ "--packages=${path.absolute('.packages')}", ]; - if (checked) - args.add("--enable-checked-mode"); + if (debuggingOptions.debuggingEnabled) { + if (debuggingOptions.checked) + args.add("--enable-checked-mode"); + if (debuggingOptions.startPaused) + args.add("--start-paused"); - if (startPaused) - args.add("--start-paused"); - - if (observatoryPort != observatoryDefaultPort) + int observatoryPort = await debuggingOptions.findBestObservatoryPort(); args.add("--observatory-port=$observatoryPort"); + } // Launch the updated application in the simulator. try { SimControl.instance.launch(id, app.id, args); } catch (error) { printError('$error'); - return false; + return new LaunchResult.failed(); } - // Wait for the service protocol port here. This will complete once the - // device has printed "Observatory is listening on..." - printTrace('Waiting for observatory port to be available...'); + if (!debuggingOptions.debuggingEnabled) { + return new LaunchResult.succeeded(); + } else { + // Wait for the service protocol port here. This will complete once the + // device has printed "Observatory is listening on..." + printTrace('Waiting for observatory port to be available...'); - try { - int devicePort = await scrapeServicePort.timeout(new Duration(seconds: 12)); - printTrace('service protocol port = $devicePort'); - printStatus('Observatory listening on http://127.0.0.1:$devicePort'); - return true; - } catch (error) { - if (error is TimeoutException) - printError('Timed out while waiting for a debug connection.'); - else - printError('Error waiting for a debug connection: $error'); - - return false; + try { + int devicePort = await observatoryDiscovery + .nextPort() + .timeout(new Duration(seconds: 20)); + printTrace('service protocol port = $devicePort'); + printStatus('Observatory listening on http://127.0.0.1:$devicePort'); + return new LaunchResult.succeeded(observatoryPort: devicePort); + } catch (error) { + if (error is TimeoutException) + printError('Timed out while waiting for a debug connection.'); + else + printError('Error waiting for a debug connection: $error'); + return new LaunchResult.failed(); + } finally { + observatoryDiscovery.cancel(); + } } } @@ -662,118 +662,57 @@ } class _IOSSimulatorLogReader extends DeviceLogReader { - _IOSSimulatorLogReader(this.device); + _IOSSimulatorLogReader(this.device) { + _linesController = new StreamController<String>.broadcast( + onListen: () { + _start(); + }, + onCancel: _stop + ); + } final IOSSimulator device; - final StreamController<String> _linesStreamController = - new StreamController<String>.broadcast(); - + StreamController<String> _linesController; 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; - StreamSubscription<String> _deviceStdoutSubscription; - StreamSubscription<String> _deviceStderrSubscription; - Process _systemProcess; - StreamSubscription<String> _systemStdoutSubscription; - StreamSubscription<String> _systemStderrSubscription; @override - Stream<String> get lines => _linesStreamController.stream; + Stream<String> get logLines => _linesController.stream; @override String get name => device.name; - @override - 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. - + Future<Null> _start() async { // Device log. device.ensureLogsExists(); - _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); + _deviceProcess = await runCommand(<String>['tail', '-n', '0', '-F', device.logFilePath]); + _deviceProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine); + _deviceProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine); // Track system.log crashes. // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]... - _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); - } + _systemProcess = await runCommand(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']); + _systemProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine); + _systemProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine); - @override - Future<Null> 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; + _deviceProcess.exitCode.then((int code) { + if (_linesController.hasListener) + _linesController.close(); + }); } // 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+\]\)?: (.*)$'); + static 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+ --- (.*) ---$'); + 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) { Match match = _mapRegex.matchAsPrefix(string); @@ -808,8 +747,7 @@ String filteredLine = _filterDeviceLine(line); if (filteredLine == null) return; - - _linesStreamController.add(filteredLine); + _linesController.add(filteredLine); } String _filterSystemLog(String string) { @@ -825,19 +763,12 @@ if (filteredLine == null) return; - _linesStreamController.add(filteredLine); + _linesController.add(filteredLine); } - @override - int get hashCode => device.logFilePath.hashCode; - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other is! _IOSSimulatorLogReader) - return false; - return other.device.logFilePath == device.logFilePath; + void _stop() { + _deviceProcess?.kill(); + _systemProcess?.kill(); } }
diff --git a/packages/flutter_tools/lib/src/service_protocol.dart b/packages/flutter_tools/lib/src/service_protocol.dart index 7800dc4..1ce825a 100644 --- a/packages/flutter_tools/lib/src/service_protocol.dart +++ b/packages/flutter_tools/lib/src/service_protocol.dart
@@ -8,28 +8,30 @@ /// Discover service protocol ports on devices. class ServiceProtocolDiscovery { + /// [logReader] - a [DeviceLogReader] to look for service messages in. + ServiceProtocolDiscovery(DeviceLogReader logReader, String serviceName) + : _logReader = logReader, _serviceName = serviceName { + assert(_logReader != null); + _subscription = _logReader.logLines.listen(_onLine); + } + static const String kObservatoryService = 'Observatory'; static const String kDiagnosticService = 'Diagnostic server'; - /// [logReader] A [DeviceLogReader] to look for service messages in. - ServiceProtocolDiscovery(DeviceLogReader logReader, String serviceName) - : _logReader = logReader, - _serviceName = serviceName { - assert(_logReader != null); - if (!_logReader.isReading) - _logReader.start(); - - _logReader.lines.listen(_onLine); - } - final DeviceLogReader _logReader; final String _serviceName; + Completer<int> _completer = new Completer<int>(); + StreamSubscription<String> _subscription; /// The [Future] returned by this function will complete when the next service /// protocol port is found. Future<int> nextPort() => _completer.future; + void cancel() { + _subscription.cancel(); + } + void _onLine(String line) { int portNumber = 0; if (line.contains('$_serviceName listening on http://')) { @@ -48,6 +50,7 @@ void _located(int port) { assert(_completer != null); assert(!_completer.isCompleted); + _completer.complete(port); _completer = new Completer<int>(); }
diff --git a/packages/flutter_tools/lib/src/toolchain.dart b/packages/flutter_tools/lib/src/toolchain.dart index 0e25ef0..2a7a557 100644 --- a/packages/flutter_tools/lib/src/toolchain.dart +++ b/packages/flutter_tools/lib/src/toolchain.dart
@@ -26,6 +26,9 @@ String depfilePath, String buildOutputPath }) { + assert(mainPath != null); + assert(snapshotPath != null); + final List<String> args = [ _path, mainPath,