Add a flutter-attach entry point for fuchsia (#24878)

diff --git a/packages/flutter_tools/BUILD.gn b/packages/flutter_tools/BUILD.gn
index 30ca4bb..1c39717 100644
--- a/packages/flutter_tools/BUILD.gn
+++ b/packages/flutter_tools/BUILD.gn
@@ -237,19 +237,15 @@
   ]
 }
 
-dart_tool("fuchsia_tools") {
-  package_name = "fuchsia_tools"
-  main_dart = "bin/fuchsia_tools.dart"
+dart_tool("fuchsia_attach") {
+  package_name = "fuchsia_attach"
+  main_dart = "bin/fuchsia_attach.dart"
 
   # Can be left empty as analysis is disabled.
   sources = []
 
   disable_analysis = true
 
-  non_dart_deps = [
-    "//third_party/flutter/frontend_server:frontend_server_tool",
-  ]
-
   deps = [
     ":flutter_tools",
   ]
diff --git a/packages/flutter_tools/bin/fuchsia_attach.dart b/packages/flutter_tools/bin/fuchsia_attach.dart
new file mode 100644
index 0000000..ec49191
--- /dev/null
+++ b/packages/flutter_tools/bin/fuchsia_attach.dart
@@ -0,0 +1,129 @@
+// Copyright 2018 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 'package:args/args.dart';
+
+import 'package:flutter_tools/runner.dart' as runner;
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/attach.dart';
+import 'package:flutter_tools/src/commands/doctor.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
+import 'package:flutter_tools/src/run_hot.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+
+final ArgParser parser = ArgParser()
+  ..addOption('build-dir', help: 'The fuchsia build directory')
+  ..addOption('dart-sdk', help: 'The prebuilt dart SDK')
+  ..addOption('target', help: 'The GN target to attach to')
+  ..addFlag('verbose', negatable: true);
+
+// Track the original working directory so that the tool can find the
+// flutter repo in third_party.
+String originalWorkingDirectory;
+
+Future<void> main(List<String> args) async {
+  final ArgResults argResults = parser.parse(args);
+  final bool verbose = argResults['verbose'];
+  final String target = argResults['target'];
+  final List<String> targetParts = _extractPathAndName(target);
+  final String path = targetParts[0];
+  final String name = targetParts[1];
+  final File dartSdk = fs.file(argResults['dart-sdk']);
+  final String buildDirectory = argResults['build-dir'];
+  final File frontendServer = fs.file('$buildDirectory/host_x64/gen/third_party/flutter/frontend_server/frontend_server_tool.snapshot');
+  final File sshConfig = fs.file('$buildDirectory/ssh-keys/ssh_config');
+  final File platformKernelDill = fs.file('$buildDirectory/flutter_runner_patched_sdk/platform_strong.dill');
+  final File flutterPatchedSdk = fs.file('$buildDirectory/flutter_runner_patched_sdk');
+  final String packages = '$buildDirectory/dartlang/gen/$path/${name}_dart_library.packages';
+  final String outputDill = '$buildDirectory/${name}_tmp.dill';
+
+  // TODO(jonahwilliams): running from fuchsia root hangs hot reload for some reason.
+  // switch to the project root directory and run from there.
+  originalWorkingDirectory = fs.currentDirectory.path;
+  fs.currentDirectory = path;
+
+  // Check for a package with a lib directory.
+  String targetFile = 'lib/main.dart';
+  if (!fs.file(targetFile).existsSync()) {
+    // Otherwise assume the package is flat.
+    targetFile = 'main.dart';
+  }
+  final List<String> command = <String>[
+    'attach',
+    '--module',
+    name,
+    '--isolate-filter',
+    name,
+    '--target',
+    targetFile,
+    '--target-model',
+    'flutter', // TODO(jonahwilliams): change to flutter_runner when dart SDK rolls
+    '--output-dill',
+    outputDill,
+    '--packages',
+    packages,
+  ];
+  if (verbose) {
+    command.add('--verbose');
+  }
+  Cache.disableLocking(); // ignore: invalid_use_of_visible_for_testing_member
+  await runner.run(
+    command,
+    <FlutterCommand>[
+      _FuchsiaAttachCommand(),
+      _FuchsiaDoctorCommand(), // If attach fails the tool will attempt to run doctor.
+    ],
+    verbose: verbose,
+    muteCommandLogging: false,
+    verboseHelp: false,
+    overrides: <Type, Generator>{
+      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
+      Artifacts: () => OverrideArtifacts(
+        parent: CachedArtifacts(),
+        frontendServer: frontendServer,
+        engineDartBinary: dartSdk,
+        platformKernelDill: platformKernelDill,
+        flutterPatchedSdk: flutterPatchedSdk,
+      ),
+      HotRunnerConfig: () => HotRunnerConfig()..computeDartDependencies = false,
+    }
+  );
+}
+
+List<String> _extractPathAndName(String gnTarget) {
+  // Separate strings like //path/to/target:app into [path/to/target, app]
+  final int lastColon = gnTarget.lastIndexOf(':');
+  if (lastColon < 0) {
+    throwToolExit('invalid path: $gnTarget');
+  }
+  final String name = gnTarget.substring(lastColon + 1);
+  // Skip '//' and chop off after :
+  if ((gnTarget.length < 3) || (gnTarget[0] != '/') || (gnTarget[1] != '/')) {
+    throwToolExit('invalid path: $gnTarget');
+  }
+  final String path = gnTarget.substring(2, lastColon);
+  return <String>[path, name];
+}
+
+class _FuchsiaDoctorCommand extends DoctorCommand {
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    Cache.flutterRoot = '$originalWorkingDirectory/third_party/dart-pkg/git/flutter';
+    return super.runCommand();
+  }
+}
+
+class _FuchsiaAttachCommand extends AttachCommand {
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    Cache.flutterRoot = '$originalWorkingDirectory/third_party/dart-pkg/git/flutter';
+    return super.runCommand();
+  }
+}
diff --git a/packages/flutter_tools/bin/fuchsia_tools.dart b/packages/flutter_tools/bin/fuchsia_tools.dart
deleted file mode 100644
index 9a0867e..0000000
--- a/packages/flutter_tools/bin/fuchsia_tools.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2018 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 'package:flutter_tools/fuchsia_executable.dart' as executable;
-
-void main(List<String> args) {
-  executable.main(args);
-}
diff --git a/packages/flutter_tools/lib/fuchsia_executable.dart b/packages/flutter_tools/lib/fuchsia_executable.dart
deleted file mode 100644
index d2cbbce..0000000
--- a/packages/flutter_tools/lib/fuchsia_executable.dart
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2018 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 'package:args/args.dart';
-
-import 'runner.dart' as runner;
-
-import 'src/artifacts.dart';
-import 'src/base/common.dart';
-import 'src/base/context.dart';
-import 'src/base/file_system.dart';
-import 'src/commands/attach.dart';
-import 'src/commands/devices.dart';
-import 'src/commands/shell_completion.dart';
-import 'src/fuchsia/fuchsia_sdk.dart';
-import 'src/run_hot.dart';
-import 'src/runner/flutter_command.dart';
-
- final ArgParser parser = ArgParser.allowAnything()
-  ..addOption('verbose', abbr: 'v')
-  ..addOption('help', abbr: 'h')
-  ..addOption(
-    'frontend-server',
-    help: 'The path to the frontend server snapshot.',
-  )
-  ..addOption(
-    'dart-sdk',
-    help: 'The path to the patched dart-sdk binary.',
-  )
-  ..addOption(
-    'ssh-config',
-    help: 'The path to the ssh configuration file.',
-  );
-
-/// Main entry point for fuchsia commands.
-///
-/// This function is intended to be used within the fuchsia source tree.
-Future<void> main(List<String> args) async {
-  final ArgResults results = parser.parse(args);
-  final bool verbose = results['verbose'];
-  final bool help = results['help'];
-  final bool verboseHelp = help && verbose;
-  final File dartSdk = fs.file(results['dart-sdk']);
-  final File frontendServer = fs.file(results['frontend-server']);
-  final File sshConfig = fs.file(results['ssh-config']);
-
-  if (!dartSdk.existsSync()) {
-    throwToolExit('--dart-sdk is required: ${dartSdk.path} does not exist.');
-  }
-  if (!frontendServer.existsSync()) {
-    throwToolExit('--frontend-server is required: ${frontendServer.path} does not exist.');
-  }
-  if (!sshConfig.existsSync()) {
-    throwToolExit('--ssh-config is required: ${sshConfig.path} does not exist.');
-  }
-
-  await runner.run(args, <FlutterCommand>[
-    AttachCommand(verboseHelp: verboseHelp),
-    DevicesCommand(),
-    ShellCompletionCommand(),
-  ], verbose: verbose,
-     muteCommandLogging: help,
-     verboseHelp: verboseHelp,
-     overrides: <Type, Generator>{
-      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
-      Artifacts: () => OverrideArtifacts(
-        parent: CachedArtifacts(),
-        frontendServer: frontendServer,
-        engineDartBinary: dartSdk,
-      ),
-      HotRunnerConfig: () => HotRunnerConfig()
-        ..computeDartDependencies = false,
-     });
-}
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 0fa9df2..371a042 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -311,11 +311,15 @@
     @required this.parent,
     this.frontendServer,
     this.engineDartBinary,
+    this.platformKernelDill,
+    this.flutterPatchedSdk,
   }) : assert(parent != null);
 
   final Artifacts parent;
   final File frontendServer;
   final File engineDartBinary;
+  final File platformKernelDill;
+  final File flutterPatchedSdk;
 
   @override
   String getArtifactPath(Artifact artifact, [TargetPlatform platform, BuildMode mode]) {
@@ -325,6 +329,12 @@
     if (artifact == Artifact.engineDartBinary && engineDartBinary != null) {
       return engineDartBinary.path;
     }
+    if (artifact == Artifact.platformKernelDill && platformKernelDill != null) {
+      return platformKernelDill.path;
+    }
+    if (artifact == Artifact.flutterPatchedSdkPath && flutterPatchedSdk != null) {
+      return flutterPatchedSdk.path;
+    }
     return parent.getArtifactPath(artifact, platform, mode);
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index a4d9d6c..844abb0b 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -147,6 +147,10 @@
             : Uri.parse('http://$ipv4Loopback:$localPort/');
           status.stop();
         } catch (_) {
+          final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
+          for (ForwardedPort port in ports) {
+            await device.portForwarder.unforward(port);
+          }
           status.cancel();
           rethrow;
         }