add a restart command to the daemon protocol (#4385)

* refactor the --resident run option into a separate file

* update daemon to run --resident apps

* re-plumbing daemon start

* send app logs

* update tests

* review changes

* fix test runner

* remove PackageMap.createGlobalInstance; rely on the ctor

* review comments
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
new file mode 100644
index 0000000..7d95e0c
--- /dev/null
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -0,0 +1,338 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import 'application_package.dart';
+import 'base/logger.dart';
+import 'base/utils.dart';
+import 'build_info.dart';
+import 'commands/build_apk.dart';
+import 'commands/install.dart';
+import 'commands/trace.dart';
+import 'device.dart';
+import 'globals.dart';
+import 'observatory.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;
+}
+
+// TODO: split out the cli part of the UI from this class
+
+class RunAndStayResident {
+  RunAndStayResident(
+    this.device, {
+    this.target,
+    this.debuggingOptions,
+    this.usesTerminalUI: true
+  });
+
+  final Device device;
+  final String target;
+  final DebuggingOptions debuggingOptions;
+  final bool usesTerminalUI;
+
+  ApplicationPackage _package;
+  String _mainPath;
+  LaunchResult _result;
+
+  Completer<int> _exitCompleter = new Completer<int>();
+  StreamSubscription<String> _loggingSubscription;
+
+  Observatory observatory;
+
+  /// Start the app and keep the process running during its lifetime.
+  Future<int> run({
+    bool traceStartup: false,
+    bool benchmark: false,
+    Completer<int> observatoryPortCompleter
+  }) {
+    // Don't let uncaught errors kill the process.
+    return runZoned(() {
+      return _run(
+        traceStartup: traceStartup,
+        benchmark: benchmark,
+        observatoryPortCompleter: observatoryPortCompleter
+      );
+    }, onError: (dynamic error, StackTrace stackTrace) {
+      printError('Exception from flutter run: $error', stackTrace);
+    });
+  }
+
+  Future<bool> restart() async {
+    if (observatory == null) {
+      printError('Debugging is not enabled.');
+      return false;
+    } else {
+      Status status = logger.startProgress('Re-starting application...');
+
+      Future<Event> extensionAddedEvent = observatory.onExtensionEvent
+        .where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization')
+        .first;
+
+      bool restartResult = await device.restartApp(
+        _package,
+        _result,
+        mainPath: _mainPath,
+        observatory: observatory
+      );
+
+      status.stop(showElapsedTime: true);
+
+      if (restartResult) {
+        // TODO(devoncarew): We should restore the route here.
+        await extensionAddedEvent;
+      }
+
+      return restartResult;
+    }
+  }
+
+  Future<Null> stop() {
+    _stopLogger();
+    return _stopApp();
+  }
+
+  Future<int> _run({
+    bool traceStartup: false,
+    bool benchmark: false,
+    Completer<int> observatoryPortCompleter
+  }) async {
+    _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;
+    }
+
+    _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;
+    }
+
+    Stopwatch startTime = new Stopwatch()..start();
+
+    // 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,
+        target: target,
+        buildMode: debuggingOptions.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);
+
+    // TODO(devoncarew): This fails for ios devices - we haven't built yet.
+    if (device is AndroidDevice) {
+      printTrace('Running install command.');
+      if (!(installApp(device, _package)))
+        return 1;
+    }
+
+    Map<String, dynamic> platformArgs;
+    if (traceStartup != null)
+      platformArgs = <String, dynamic>{ 'trace-startup': traceStartup };
+
+    printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
+
+    _loggingSubscription = device.logReader.logLines.listen((String line) {
+      if (!line.contains('Observatory listening on http') && !line.contains('Diagnostic server listening on http'))
+        printStatus(line);
+    });
+
+    _result = await device.startApp(
+      _package,
+      debuggingOptions.buildMode,
+      mainPath: _mainPath,
+      debuggingOptions: debuggingOptions,
+      platformArgs: platformArgs
+    );
+
+    if (!_result.started) {
+      printError('Error running application on ${device.name}.');
+      await _loggingSubscription.cancel();
+      return 2;
+    }
+
+    startTime.stop();
+
+    if (observatoryPortCompleter != null && _result.hasObservatory)
+      observatoryPortCompleter.complete(_result.observatoryPort);
+
+    // Connect to observatory.
+    if (debuggingOptions.debuggingEnabled) {
+      observatory = await Observatory.connect(_result.observatoryPort);
+      printTrace('Connected to observatory port: ${_result.observatoryPort}.');
+
+      observatory.populateIsolateInfo();
+      observatory.onExtensionEvent.listen((Event event) {
+        printTrace(event.toString());
+      });
+      observatory.onIsolateEvent.listen((Event event) {
+        printTrace(event.toString());
+      });
+
+      if (benchmark)
+        await observatory.waitFirstIsolate;
+
+      // Listen for observatory connection close.
+      observatory.done.whenComplete(() {
+        if (!_exitCompleter.isCompleted) {
+          printStatus('Application finished.');
+          _exitCompleter.complete(0);
+        }
+      });
+    }
+
+    printStatus('Application running.');
+
+    if (observatory != null && traceStartup) {
+      printStatus('Downloading startup trace info...');
+
+      await downloadStartupTrace(observatory);
+
+      if (!_exitCompleter.isCompleted)
+        _exitCompleter.complete(0);
+    } else {
+      if (usesTerminalUI) {
+        if (!logger.quiet)
+          _printHelp();
+
+        terminal.singleCharMode = true;
+        terminal.onCharInput.listen((String code) {
+          String lower = code.toLowerCase();
+
+          if (lower == 'h' || code == AnsiTerminal.KEY_F1) {
+            // F1, help
+            _printHelp();
+          } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
+            // F5, restart
+            restart();
+          } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
+            // F10, exit
+            _stopApp();
+          }
+        });
+      }
+
+      ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
+        _resetTerminal();
+        _stopLogger();
+        _stopApp();
+      });
+      ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
+        _resetTerminal();
+        _stopLogger();
+        _stopApp();
+      });
+    }
+
+    if (benchmark) {
+      await new Future<Null>.delayed(new Duration(seconds: 4));
+
+      // Touch the file.
+      File mainFile = new File(_mainPath);
+      mainFile.writeAsBytesSync(mainFile.readAsBytesSync());
+
+      Stopwatch restartTime = new Stopwatch()..start();
+      bool restarted = await restart();
+      restartTime.stop();
+      writeRunBenchmarkFile(startTime, restarted ? restartTime : null);
+      await new Future<Null>.delayed(new Duration(seconds: 2));
+      stop();
+    }
+
+    return _exitCompleter.future.then((int exitCode) async {
+      _resetTerminal();
+      _stopLogger();
+      return exitCode;
+    });
+  }
+
+  void _printHelp() {
+    printStatus('Type "h" or F1 for help, "r" or F5 to restart the app, and "q", F10, or ctrl-c to quit.');
+  }
+
+  void _stopLogger() {
+    _loggingSubscription?.cancel();
+  }
+
+  void _resetTerminal() {
+    if (usesTerminalUI)
+      terminal.singleCharMode = false;
+  }
+
+  Future<Null> _stopApp() {
+    if (observatory != null && !observatory.isClosed) {
+      if (observatory.isolates.isNotEmpty) {
+        observatory.flutterExit(observatory.firstIsolateId);
+        return new Future<Null>.delayed(new Duration(milliseconds: 100));
+      }
+    }
+
+    if (!_exitCompleter.isCompleted)
+      _exitCompleter.complete(0);
+
+    return new Future<Null>.value();
+  }
+}
+
+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;
+  }
+}
+
+void writeRunBenchmarkFile(Stopwatch startTime, [Stopwatch restartTime]) {
+  final String benchmarkOut = 'refresh_benchmark.json';
+  Map<String, dynamic> data = <String, dynamic>{
+    'start': startTime.elapsedMilliseconds,
+    'time': (restartTime ?? startTime).elapsedMilliseconds // time and restart are the same
+  };
+  if (restartTime != null)
+    data['restart'] = restartTime.elapsedMilliseconds;
+
+  new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
+  printStatus('Run benchmark written to $benchmarkOut ($data).');
+}