Adds initial support for hot reload for Fuchsia to flutter_tool. (#8764)

diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index b9bde1e..0f6f5f1 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -32,6 +32,7 @@
 import 'src/commands/doctor.dart';
 import 'src/commands/drive.dart';
 import 'src/commands/format.dart';
+import 'src/commands/fuchsia_reload.dart';
 import 'src/commands/install.dart';
 import 'src/commands/logs.dart';
 import 'src/commands/packages.dart';
@@ -74,6 +75,7 @@
     new DoctorCommand(),
     new DriveCommand(),
     new FormatCommand(),
+    new FuchsiaReloadCommand(),
     new InstallCommand(),
     new LogsCommand(),
     new PackagesCommand(),
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index 33918cc..11c5347 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -261,6 +261,7 @@
     case TargetPlatform.darwin_x64:
     case TargetPlatform.linux_x64:
     case TargetPlatform.windows_x64:
+    case TargetPlatform.fuchsia:
       return null;
   }
   assert(platform != null);
@@ -286,6 +287,7 @@
       case TargetPlatform.darwin_x64:
       case TargetPlatform.linux_x64:
       case TargetPlatform.windows_x64:
+      case TargetPlatform.fuchsia:
         return null;
     }
     return null;
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index d0dd1fd..c3b3e79 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -89,6 +89,7 @@
       case TargetPlatform.darwin_x64:
       case TargetPlatform.linux_x64:
       case TargetPlatform.windows_x64:
+      case TargetPlatform.fuchsia:
         return _getHostArtifactPath(artifact, platform);
     }
     assert(false, 'Invalid platform $platform.');
@@ -170,6 +171,7 @@
       case TargetPlatform.linux_x64:
       case TargetPlatform.darwin_x64:
       case TargetPlatform.windows_x64:
+      case TargetPlatform.fuchsia:
         assert(mode == null, 'Platform $platform does not support different build modes.');
         return fs.path.join(engineDir, platformName);
       case TargetPlatform.ios:
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 8c0bcc5..52dc5cf 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -67,7 +67,8 @@
   ios,
   darwin_x64,
   linux_x64,
-  windows_x64
+  windows_x64,
+  fuchsia,
 }
 
 String getNameForTargetPlatform(TargetPlatform platform) {
@@ -86,6 +87,8 @@
       return 'linux-x64';
     case TargetPlatform.windows_x64:
       return 'windows-x64';
+    case TargetPlatform.fuchsia:
+      return 'fuchsia';
   }
   assert(false);
   return null;
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index e2bbeb0..778a323 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -175,6 +175,7 @@
     case TargetPlatform.darwin_x64:
     case TargetPlatform.linux_x64:
     case TargetPlatform.windows_x64:
+    case TargetPlatform.fuchsia:
       assert(false);
   }
 
@@ -232,6 +233,7 @@
     case TargetPlatform.darwin_x64:
     case TargetPlatform.linux_x64:
     case TargetPlatform.windows_x64:
+    case TargetPlatform.fuchsia:
       assert(false);
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
new file mode 100644
index 0000000..31a8268
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
@@ -0,0 +1,227 @@
+// Copyright 2017 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:math';
+
+import '../base/common.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/platform.dart';
+import '../device.dart';
+import '../flx.dart' as flx;
+import '../fuchsia/fuchsia_device.dart';
+import '../globals.dart';
+import '../run_hot.dart';
+import '../runner/flutter_command.dart';
+
+// Usage:
+// With e.g. flutter_gallery already running, a HotRunner can be attached to it
+// with:
+// $ flutter fuchsia_reload -f ~/fuchsia -a 192.168.1.39 \
+//       -g //lib/flutter/examples/flutter_gallery:flutter_gallery
+
+class FuchsiaReloadCommand extends FlutterCommand {
+  String _fuchsiaRoot;
+  String _projectRoot;
+  String _projectName;
+  String _fuchsiaProjectPath;
+  String _target;
+  String _address;
+  String _dotPackagesPath;
+
+  @override
+  final String name = 'fuchsia_reload';
+
+  @override
+  final String description = 'Hot reload on Fuchsia.';
+
+  FuchsiaReloadCommand() {
+    addBuildModeFlags(defaultToRelease: false);
+    argParser.addOption('address',
+      abbr: 'a',
+      help: 'Fuchsia device network name or address.');
+    argParser.addOption('build-type',
+      abbr: 'b',
+      defaultsTo: 'release-x86-64',
+      help: 'Fuchsia build type, e.g. release-x86-64.');
+    argParser.addOption('fuchsia-root',
+      abbr: 'f',
+      defaultsTo: platform.environment['FUCHSIA_ROOT'],
+      help: 'Path to Fuchsia source tree.');
+    argParser.addOption('gn-target',
+      abbr: 'g',
+      help: 'GN target of the application, e.g //path/to/app:app');
+    argParser.addOption('target',
+      abbr: 't',
+      defaultsTo: flx.defaultMainPath,
+      help: 'Target app path / main entry-point file. '
+            'Relative to --gn-target path, e.g. lib/main.dart');
+  }
+
+  @override
+  Future<Null> runCommand() async {
+    _validateArguments();
+
+    // Find the network ports used on the device by VM service instances.
+    final List<int> servicePorts = await _getServicePorts();
+    if (servicePorts.length == 0) {
+      throwToolExit("Couldn't find any running Observatory instances.");
+    }
+    for (int port in servicePorts) {
+      printStatus("Fuchsia service port: $port");
+    }
+
+    // TODO(zra): Check that there are running VM services on the returned
+    // ports, and find the Isolates that are running the target app.
+
+    // Set up a device and hot runner and attach the hot runner to the first
+    // vm service we found.
+    final int firstPort = servicePorts[0];
+    final FuchsiaDevice device = new FuchsiaDevice("$_address:$firstPort");
+    final HotRunner hotRunner = new HotRunner(
+        device,
+        debuggingOptions: new DebuggingOptions.enabled(getBuildMode()),
+        target: _target,
+        projectRootPath: _fuchsiaProjectPath,
+        packagesFilePath: _dotPackagesPath);
+    final Uri observatoryUri = Uri.parse("http://$_address:$firstPort");
+    await hotRunner.attach(observatoryUri);
+  }
+
+  void _validateArguments() {
+    _fuchsiaRoot = argResults['fuchsia-root'];
+    if (_fuchsiaRoot == null) {
+      throwToolExit(
+          "Please give the location of the Fuchsia tree with --fuchsia-root");
+    }
+    if (!_directoryExists(_fuchsiaRoot)) {
+      throwToolExit("Specified --fuchsia-root '$_fuchsiaRoot' does not exist");
+    }
+
+    _address = argResults['address'];
+    if (_address == null) {
+      throwToolExit(
+          "Give the address of the device running Fuchsia with --address");
+    }
+
+    final List<String> gnTarget = _extractPathAndName(argResults['gn-target']);
+    _projectRoot = gnTarget[0];
+    _projectName = gnTarget[1];
+    _fuchsiaProjectPath = "$_fuchsiaRoot/$_projectRoot";
+    if (!_directoryExists(_fuchsiaProjectPath)) {
+      throwToolExit(
+          "Target does not exist in the Fuchsia tree: $_fuchsiaProjectPath");
+    }
+
+    final String relativeTarget = argResults['target'];
+    if (relativeTarget == null) {
+      throwToolExit('Give the application entry point with --target');
+    }
+    _target = "$_fuchsiaProjectPath/$relativeTarget";
+    if (!_fileExists(_target)) {
+      throwToolExit("Couldn't find application entry point at $_target");
+    }
+
+    final String buildType = argResults['build-type'];
+    if (buildType == null) {
+      throwToolExit("Give the build type with --build-type");
+    }
+    final String packagesFileName = "${_projectName}_dart_package.packages";
+    _dotPackagesPath =
+        "$_fuchsiaRoot/out/$buildType/gen/$_projectRoot/$packagesFileName";
+    if (!_fileExists(_dotPackagesPath)) {
+      throwToolExit("Couldn't find .packages file at $_dotPackagesPath");
+    }
+  }
+
+  List<String> _extractPathAndName(String gnTarget) {
+    final String errorMessage =
+        "fuchsia_reload --target '$gnTarget' should have the form: "
+        "'//path/to/app:name'";
+    // Separate strings like //path/to/target:app into [path/to/target, app]
+    final int lastColon = gnTarget.lastIndexOf(':');
+    if (lastColon < 0) {
+      throwToolExit(errorMessage);
+    }
+    final String name = gnTarget.substring(lastColon + 1);
+    // Skip '//' and chop off after :
+    if ((gnTarget.length < 3) || (gnTarget[0] != '/') || (gnTarget[1] != '/')) {
+      throwToolExit(errorMessage);
+    }
+    final String path = gnTarget.substring(2, lastColon);
+    return <String>[path, name];
+  }
+
+  Future<List<int>> _getServicePorts() async {
+    final FuchsiaDeviceCommandRunner runner =
+        new FuchsiaDeviceCommandRunner(_fuchsiaRoot);
+    final List<String> lsOutput = await runner.run("ls /tmp/dart.services");
+    final List<int> ports = new List<int>();
+    for (String s in lsOutput) {
+      final String trimmed = s.trim();
+      final int lastSpace = trimmed.lastIndexOf(' ');
+      final String lastWord = trimmed.substring(lastSpace + 1);
+      if ((lastWord != '.') && (lastWord != '..')) {
+        final int value = int.parse(lastWord, onError: (_) => null);
+        if (value != null) {
+          ports.add(value);
+        }
+      }
+    }
+    return ports;
+  }
+
+  bool _directoryExists(String path) {
+    final Directory d = fs.directory(path);
+    return d.existsSync();
+  }
+
+  bool _fileExists(String path) {
+    final File f = fs.file(path);
+    return f.existsSync();
+  }
+}
+
+
+// TODO(zra): When Fuchsia has ssh, this should be changed to use that instead.
+class FuchsiaDeviceCommandRunner {
+  final String _fuchsiaRoot;
+  final Random _rng = new Random(new DateTime.now().millisecondsSinceEpoch);
+
+  FuchsiaDeviceCommandRunner(this._fuchsiaRoot);
+
+  Future<List<String>> run(String command) async {
+    final int tag = _rng.nextInt(999999);
+    const String kNetRunCommand = "out/build-magenta/tools/netruncmd";
+    final String netruncmd = fs.path.join(_fuchsiaRoot, kNetRunCommand);
+    const String kNetCP = "out/build-magenta/tools/netcp";
+    final String netcp = fs.path.join(_fuchsiaRoot, kNetCP);
+    final String remoteStdout = "/tmp/netruncmd.$tag";
+    final String localStdout = "${fs.systemTempDirectory.path}/netruncmd.$tag";
+    final String redirectedCommand = "$command > $remoteStdout";
+    // Run the command with output directed to a tmp file.
+    ProcessResult result =
+        await Process.run(netruncmd, <String>[":", redirectedCommand]);
+    if (result.exitCode != 0) {
+      return null;
+    }
+    // Copy that file to the local filesystem.
+    result = await Process.run(netcp, <String>[":$remoteStdout", localStdout]);
+    // Try to delete the remote file. Don't care about the result;
+    Process.run(netruncmd, <String>[":", "rm $remoteStdout"]);
+    if (result.exitCode != 0) {
+      return null;
+    }
+    // Read the local file.
+    final File f = fs.file(localStdout);
+    List<String> lines;
+    try {
+      lines = await f.readAsLines();
+    } finally {
+      f.delete();
+    }
+    return lines;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
new file mode 100644
index 0000000..10fff76
--- /dev/null
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -0,0 +1,102 @@
+// Copyright 2017 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 '../application_package.dart';
+import '../build_info.dart';
+import '../devfs.dart';
+import '../device.dart';
+
+/// Read the log for a particular device.
+class _FuchsiaLogReader extends DeviceLogReader {
+  FuchsiaDevice _device;
+
+  _FuchsiaLogReader(this._device);
+
+  @override String get name => _device.name;
+
+  Stream<String> _logLines;
+  @override
+  Stream<String> get logLines {
+    _logLines ??= new Stream<String>.empty();
+    return _logLines;
+  }
+
+  @override
+  String toString() => name;
+}
+
+class FuchsiaDevice extends Device {
+  FuchsiaDevice(String id, { this.name }) : super(id);
+
+  @override
+  bool get supportsHotMode => true;
+
+  @override
+  final String name;
+
+  @override
+  bool get isLocalEmulator => false;
+
+  @override
+  bool get supportsStartPaused => false;
+
+  @override
+  bool isAppInstalled(ApplicationPackage app) => false;
+
+  @override
+  bool isLatestBuildInstalled(ApplicationPackage app) => false;
+
+  @override
+  bool installApp(ApplicationPackage app) => false;
+
+  @override
+  bool uninstallApp(ApplicationPackage app) => false;
+
+  @override
+  bool isSupported() => true;
+
+  @override
+  Future<LaunchResult> startApp(
+    ApplicationPackage app,
+    BuildMode mode, {
+    String mainPath,
+    String route,
+    DebuggingOptions debuggingOptions,
+    Map<String, dynamic> platformArgs,
+    bool prebuiltApplication: false,
+    DevFSContent kernelContent,
+    bool applicationNeedsRebuild: false,
+  }) => new Future<Null>.error('unimplemented');
+
+  @override
+  Future<bool> stopApp(ApplicationPackage app) async {
+    // Currently we don't have a way to stop an app running on Fuchsia.
+    return false;
+  }
+
+  @override
+  TargetPlatform get targetPlatform => TargetPlatform.fuchsia;
+
+  @override
+  String get sdkNameAndVersion => 'Fuchsia';
+
+  _FuchsiaLogReader _logReader;
+  @override
+  DeviceLogReader getLogReader({ApplicationPackage app}) {
+    _logReader ??= new _FuchsiaLogReader(this);
+    return _logReader;
+  }
+
+  @override
+  DevicePortForwarder get portForwarder => null;
+
+  @override
+  void clearLogs() {
+  }
+
+  @override
+  bool get supportsScreenshot => false;
+}
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 78e109d..6224f56 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -89,6 +89,12 @@
     return stopApp();
   }
 
+  Future<Null> detach() async {
+    await stopEchoingDeviceLog();
+    await preStop();
+    appFinished();
+  }
+
   Future<Null> _debugDumpApp() async {
     if (vmService != null)
       await vmService.vm.refreshViews();
@@ -273,6 +279,9 @@
       // F10, exit
       await stop();
       return true;
+    } else if (lower == 'd') {
+      await detach();
+      return true;
     }
 
     return false;
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 4245355..99a1bc0 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -87,6 +87,74 @@
     return true;
   }
 
+  Future<int> attach(Uri observatoryUri, {
+    Completer<DebugConnectionInfo> connectionInfoCompleter,
+    Completer<Null> appStartedCompleter,
+  }) async {
+    _observatoryUri = observatoryUri;
+    try {
+      await connectToServiceProtocol(_observatoryUri);
+    } catch (error) {
+      printError('Error connecting to the service protocol: $error');
+      return 2;
+    }
+
+    try {
+      final Uri baseUri = await _initDevFS();
+      if (connectionInfoCompleter != null) {
+        connectionInfoCompleter.complete(
+          new DebugConnectionInfo(
+            httpUri: _observatoryUri,
+            wsUri: vmService.wsAddress,
+            baseUri: baseUri.toString()
+          )
+        );
+      }
+    } catch (error) {
+      printError('Error initializing DevFS: $error');
+      return 3;
+    }
+    final bool devfsResult = await _updateDevFS();
+    if (!devfsResult) {
+      printError('Could not perform initial file synchronization.');
+      return 3;
+    }
+
+    await vmService.vm.refreshViews();
+    printTrace('Connected to ${vmService.vm.mainView}.');
+
+    if (stayResident) {
+      setupTerminal();
+      registerSignalHandlers();
+    }
+
+    appStartedCompleter?.complete();
+
+    if (benchmarkMode) {
+      // We are running in benchmark mode.
+      printStatus('Running in benchmark mode.');
+      // Measure time to perform a hot restart.
+      printStatus('Benchmarking hot restart');
+      await restart(fullRestart: true);
+      await vmService.vm.refreshViews();
+      // TODO(johnmccutchan): Modify script entry point.
+      printStatus('Benchmarking hot reload');
+      // Measure time to perform a hot reload.
+      await restart(fullRestart: false);
+      printStatus('Benchmark completed. Exiting application.');
+      await _cleanupDevFS();
+      await stopEchoingDeviceLog();
+      await stopApp();
+      final File benchmarkOutput = fs.file('hot_benchmark.json');
+      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
+    }
+
+    if (stayResident)
+      return waitForAppToFinish();
+    await cleanupAtFinish();
+    return 0;
+  }
+
   @override
   Future<int> run({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
@@ -152,68 +220,9 @@
       return 2;
     }
 
-    _observatoryUri = result.observatoryUri;
-    try {
-      await connectToServiceProtocol(_observatoryUri);
-    } catch (error) {
-      printError('Error connecting to the service protocol: $error');
-      return 2;
-    }
-
-    try {
-      final Uri baseUri = await _initDevFS();
-      if (connectionInfoCompleter != null) {
-        connectionInfoCompleter.complete(
-          new DebugConnectionInfo(
-            httpUri: _observatoryUri,
-            wsUri: vmService.wsAddress,
-            baseUri: baseUri.toString()
-          )
-        );
-      }
-    } catch (error) {
-      printError('Error initializing DevFS: $error');
-      return 3;
-    }
-    final bool devfsResult = await _updateDevFS();
-    if (!devfsResult) {
-      printError('Could not perform initial file synchronization.');
-      return 3;
-    }
-
-    await vmService.vm.refreshViews();
-    printTrace('Connected to ${vmService.vm.mainView}.');
-
-    if (stayResident) {
-      setupTerminal();
-      registerSignalHandlers();
-    }
-
-    appStartedCompleter?.complete();
-
-    if (benchmarkMode) {
-      // We are running in benchmark mode.
-      printStatus('Running in benchmark mode.');
-      // Measure time to perform a hot restart.
-      printStatus('Benchmarking hot restart');
-      await restart(fullRestart: true);
-      await vmService.vm.refreshViews();
-      // TODO(johnmccutchan): Modify script entry point.
-      printStatus('Benchmarking hot reload');
-      // Measure time to perform a hot reload.
-      await restart(fullRestart: false);
-      printStatus('Benchmark completed. Exiting application.');
-      await _cleanupDevFS();
-      await stopEchoingDeviceLog();
-      await stopApp();
-      final File benchmarkOutput = fs.file('hot_benchmark.json');
-      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
-    }
-
-    if (stayResident)
-      return waitForAppToFinish();
-    await cleanupAtFinish();
-    return 0;
+    return attach(result.observatoryUri,
+                  connectionInfoCompleter: connectionInfoCompleter,
+                  appStartedCompleter: appStartedCompleter);
   }
 
   @override