Add a control pipe that can trigger reloads / restarts (#5282)

diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart
index ccd227b..729f1d4 100644
--- a/packages/flutter_tools/lib/src/base/os.dart
+++ b/packages/flutter_tools/lib/src/base/os.dart
@@ -40,6 +40,9 @@
   /// if `which` was not able to locate the binary.
   File which(String execName);
 
+  /// Return the File representing a new pipe.
+  File makePipe(String path);
+
   void unzip(File file, Directory targetDirectory);
 }
 
@@ -67,6 +70,12 @@
   void unzip(File file, Directory targetDirectory) {
     runSync(<String>['unzip', '-o', '-q', file.path, '-d', targetDirectory.path]);
   }
+
+  @override
+  File makePipe(String path) {
+    runSync(<String>['mkfifo', path]);
+    return new File(path);
+  }
 }
 
 class _WindowsUtils extends OperatingSystemUtils {
@@ -101,6 +110,11 @@
       destFile.writeAsBytesSync(archiveFile.content);
     }
   }
+
+  @override
+  File makePipe(String path) {
+    throw new UnsupportedError('makePipe is not implemented on Windows.');
+  }
 }
 
 Future<int> findAvailablePort() async {
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index c14f40a..fed2bb0 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -20,6 +20,7 @@
 import 'build_apk.dart';
 import 'install.dart';
 import 'trace.dart';
+import '../base/os.dart';
 
 abstract class RunCommandBase extends FlutterCommand {
   RunCommandBase() {
@@ -63,6 +64,12 @@
                       defaultsTo: false,
                       help: 'Run with support for hot reloading.');
 
+    // Option to enable control over a named pipe.
+    argParser.addOption('control-pipe',
+                        hide: true,
+                        help: 'Specify a named pipe to receive commands on.');
+
+
     // Hidden option to enable a benchmarking mode. This will run the given
     // application, measure the startup time and the app restart time, write the
     // results out to 'refresh_benchmark.json', and exit. This flag is intended
@@ -118,6 +125,7 @@
 
     // Do some early error checks for hot mode.
     bool hotMode = argResults['hot'];
+
     if (hotMode) {
       if (getBuildMode() != BuildMode.debug) {
         printError('Hot mode only works with debug builds.');
@@ -127,6 +135,21 @@
         printError('Hot mode is not supported by this device.');
         return 1;
       }
+    } else {
+      if (argResults['control-pipe']) {
+        printError('--control-pipe requires --hot');
+        return 1;
+      }
+    }
+
+    String pipePath = argResults['control-pipe'];
+    File pipe;
+    if (pipePath != null) {
+      try {
+        // Attempt to create the pipe.
+        os.makePipe(pipePath);
+      } catch (_) { /* ignore */ }
+      pipe = new File(pipePath);
     }
 
     ResidentRunner runner;
@@ -135,7 +158,8 @@
       runner = new HotRunner(
         deviceForCommand,
         target: targetFile,
-        debuggingOptions: options
+        debuggingOptions: options,
+        pipe: pipe
       );
     } else {
       runner = new RunAndStayResident(
diff --git a/packages/flutter_tools/lib/src/hot.dart b/packages/flutter_tools/lib/src/hot.dart
index 89ba133..60cc768 100644
--- a/packages/flutter_tools/lib/src/hot.dart
+++ b/packages/flutter_tools/lib/src/hot.dart
@@ -34,7 +34,8 @@
     Device device, {
     String target,
     DebuggingOptions debuggingOptions,
-    bool usesTerminalUI: true
+    bool usesTerminalUI: true,
+    this.pipe
   }) : super(device,
              target: target,
              debuggingOptions: debuggingOptions,
@@ -46,6 +47,33 @@
   String _mainPath;
   String _projectRootPath;
   final AssetBundle bundle = new AssetBundle();
+  final File pipe;
+
+  Future<String> _readFromControlPipe() async {
+    final Stream<List<int>> stream = pipe.openRead();
+    final List<int> bytes = await stream.first;
+    final String string = new String.fromCharCodes(bytes).trim();
+    return string;
+  }
+
+  Future<Null> _startReadingFromControlPipe() async {
+    if (pipe == null)
+      return;
+
+    while (true) {
+      // This loop will only exit if _readFromControlPipe throws an exception.
+      // If no exception is thrown this will keep the flutter command running
+      // until it is explicitly stopped via some other mechanism, for example,
+      // ctrl+c or sending "q" to the control pipe.
+      String result = await _readFromControlPipe();
+      printStatus('Control pipe received "$result"');
+      await processTerminalInput(result);
+      if (result.toLowerCase() == 'q') {
+        printStatus("Finished reading from control pipe");
+        break;
+      }
+    }
+  }
 
   @override
   Future<int> run({ Completer<int> observatoryPortCompleter, String route }) {
@@ -166,6 +194,8 @@
       await _launchFromDevFS(_package, _mainPath);
     }
 
+    _startReadingFromControlPipe();
+
     printStatus('Application running.');
 
     setupTerminal();
@@ -176,16 +206,16 @@
   }
 
   @override
-  void handleTerminalCommand(String code) {
+  Future<Null> handleTerminalCommand(String code) async {
     final String lower = code.toLowerCase();
-    if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
+    if ((lower == 'r') || (code == AnsiTerminal.KEY_F5)) {
       // F5, restart
-      if (code == 'r') {
+      if ((code == 'r') || (code == AnsiTerminal.KEY_F5)) {
         // lower-case 'r'
-        _reloadSources();
+        await _reloadSources();
       } else {
         // upper-case 'R'.
-        _restartFromSources();
+        await _restartFromSources();
       }
     }
   }
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 3bbbcbf..0d405d6 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -40,12 +40,12 @@
     return stopApp();
   }
 
-  void _debugDumpApp() {
-    serviceProtocol.flutterDebugDumpApp(serviceProtocol.firstIsolateId);
+  Future<Null> _debugDumpApp() async {
+    await serviceProtocol.flutterDebugDumpApp(serviceProtocol.firstIsolateId);
   }
 
-  void _debugDumpRenderTree() {
-    serviceProtocol.flutterDebugDumpRenderTree(serviceProtocol.firstIsolateId);
+  Future<Null> _debugDumpRenderTree() async {
+    await serviceProtocol.flutterDebugDumpRenderTree(serviceProtocol.firstIsolateId);
   }
 
   void registerSignalHandlers() {
@@ -99,7 +99,7 @@
   }
 
   /// Returns [true] if the input has been handled by this function.
-  bool _commonTerminalInputHandler(String character) {
+  Future<bool> _commonTerminalInputHandler(String character) async {
     final String lower = character.toLowerCase();
 
     printStatus(''); // the key the user tapped might be on this line
@@ -109,20 +109,26 @@
       printHelp();
       return true;
     } else if (lower == 'w') {
-      _debugDumpApp();
+      await _debugDumpApp();
       return true;
     } else if (lower == 't') {
-      _debugDumpRenderTree();
+      await _debugDumpRenderTree();
       return true;
     } else if (lower == 'q' || character == AnsiTerminal.KEY_F10) {
       // F10, exit
-      stopApp();
+      await stopApp();
       return true;
     }
 
     return false;
   }
 
+  Future<Null> processTerminalInput(String command) async {
+    bool handled = await _commonTerminalInputHandler(command);
+    if (!handled)
+      await handleTerminalCommand(command);
+  }
+
   void appFinished() {
     if (_finished.isCompleted)
       return;
@@ -143,9 +149,7 @@
 
       terminal.singleCharMode = true;
       terminal.onCharInput.listen((String code) {
-        if (!_commonTerminalInputHandler(code)) {
-          handleTerminalCommand(code);
-        }
+        processTerminalInput(code);
       });
     }
   }
@@ -174,7 +178,7 @@
   /// Called to print help to the terminal.
   void printHelp();
   /// Called when the runner should handle a terminal command.
-  void handleTerminalCommand(String code);
+  Future<Null> handleTerminalCommand(String code);
 }
 
 /// Given the value of the --target option, return the path of the Dart file
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
index 9455918..d921b71 100644
--- a/packages/flutter_tools/lib/src/run.dart
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -208,12 +208,12 @@
   }
 
   @override
-  void handleTerminalCommand(String code) {
+  Future<Null> handleTerminalCommand(String code) async {
     String lower = code.toLowerCase();
     if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
       if (device.supportsRestart) {
         // F5, restart
-        restart();
+        await restart();
       }
     }
   }