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,