Support for fuchsia device discovery, workflow, and sdk wrapper (#23282)

diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index c470e17..afa56d4 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -26,6 +26,8 @@
 import 'device.dart';
 import 'doctor.dart';
 import 'emulator.dart';
+import 'fuchsia/fuchsia_sdk.dart';
+import 'fuchsia/fuchsia_workflow.dart';
 import 'ios/cocoapods.dart';
 import 'ios/ios_workflow.dart';
 import 'ios/mac.dart';
@@ -62,6 +64,8 @@
       Doctor: () => const Doctor(),
       DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance,
       EmulatorManager: () => EmulatorManager(),
+      FuchsiaSdk: () => FuchsiaSdk(),
+      FuchsiaWorkflow: () => FuchsiaWorkflow(),
       Flags: () => const EmptyFlags(),
       FlutterVersion: () => FlutterVersion(const Clock()),
       GenSnapshot: () => const GenSnapshot(),
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 7704cea..8225f4f 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -11,6 +11,8 @@
 import 'base/file_system.dart';
 import 'base/utils.dart';
 import 'build_info.dart';
+import 'fuchsia/fuchsia_device.dart';
+
 import 'globals.dart';
 import 'ios/devices.dart';
 import 'ios/simulators.dart';
@@ -27,6 +29,7 @@
     _deviceDiscoverers.add(AndroidDevices());
     _deviceDiscoverers.add(IOSDevices());
     _deviceDiscoverers.add(IOSSimulators());
+    _deviceDiscoverers.add(FuchsiaDevices());
     _deviceDiscoverers.add(FlutterTesterDevices());
   }
 
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index f712fad..409f954 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -19,6 +19,7 @@
 import 'base/version.dart';
 import 'cache.dart';
 import 'device.dart';
+import 'fuchsia/fuchsia_workflow.dart';
 import 'globals.dart';
 import 'intellij/intellij.dart';
 import 'ios/ios_workflow.dart';
@@ -80,6 +81,9 @@
 
       if (androidWorkflow.appliesToHostPlatform)
         _workflows.add(androidWorkflow);
+
+      if (fuchsiaWorkflow.appliesToHostPlatform)
+        _workflows.add(fuchsiaWorkflow);
     }
     return _workflows;
   }
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index 2ce84d6..5fe13ea 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -4,10 +4,16 @@
 
 import 'dart:async';
 
+import 'package:meta/meta.dart';
+
 import '../application_package.dart';
+import '../base/platform.dart';
 import '../build_info.dart';
 import '../device.dart';
 
+import 'fuchsia_sdk.dart';
+import 'fuchsia_workflow.dart';
+
 /// Read the log for a particular device.
 class _FuchsiaLogReader extends DeviceLogReader {
   _FuchsiaLogReader(this._device);
@@ -27,6 +33,49 @@
   String toString() => name;
 }
 
+class FuchsiaDevices extends PollingDeviceDiscovery {
+  FuchsiaDevices() : super('Fuchsia devices');
+
+  @override
+  bool get supportsPlatform => platform.isLinux || platform.isMacOS;
+
+  @override
+  bool get canListAnything => fuchsiaWorkflow.canListDevices;
+
+  @override
+  Future<List<Device>> pollingGetDevices() async {
+    if (!fuchsiaWorkflow.canListDevices) {
+      return <Device>[];
+    }
+    final String text = await fuchsiaSdk.netls();
+    return parseFuchsiaDeviceOutput(text);
+  }
+
+  @override
+  Future<List<String>> getDiagnostics() async => const <String>[];
+}
+
+/// Parses output from the netls tool into fuchsia devices.
+///
+/// Example output:
+///     $ ./netls
+///     > device liliac-shore-only-last (fe80::82e4:da4d:fe81:227d/3)
+@visibleForTesting
+List<FuchsiaDevice> parseFuchsiaDeviceOutput(String text) {
+  final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
+  for (String rawLine in text.trim().split('\n')) {
+    final String line = rawLine.trim();
+    if (!line.startsWith('device'))
+      continue;
+    // ['device', 'device name', '(id)']
+    final List<String> words = line.split(' ');
+    final String name = words[1];
+    final String id = words[2].substring(1, words[2].length - 1);
+    devices.add(FuchsiaDevice(id, name: name));
+  }
+  return devices;
+}
+
 class FuchsiaDevice extends Device {
   FuchsiaDevice(String id, { this.name }) : super(id);
 
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
new file mode 100644
index 0000000..d010d3b
--- /dev/null
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
@@ -0,0 +1,54 @@
+// 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 '../base/common.dart';
+import '../base/context.dart';
+import '../base/process.dart';
+
+/// The [FuchsiaSdk] instance.
+FuchsiaSdk get fuchsiaSdk => context[FuchsiaSdk];
+
+/// The Fuchsia SDK shell commands.
+///
+/// This workflow assumes development within the fuchsia source tree,
+/// including a working fx command-line tool in the user's PATH.
+class FuchsiaSdk {
+
+  /// Invokes the `netaddr` command.
+  ///
+  /// This returns the network address of an attached fuchsia device. Does
+  /// not currently support multiple attached devices.
+  ///
+  /// Example output:
+  ///     $ fx netaddr --fuchsia
+  ///     > fe80::9aaa:fcff:fe60:d3af%eth1
+  Future<String> netaddr() async {
+    try {
+      final RunResult process = await runAsync(<String>['fx', 'netaddr', '--fuchsia', '--nowait']);
+      return process.stdout;
+    } on ArgumentError catch (exception) {
+      throwToolExit('$exception');
+    }
+    return null;
+  }
+
+  /// Invokes the `netls` command.
+  ///
+  /// This lists attached fuchsia devices with their name and address. Does
+  /// not currently support multiple attached devices.
+  ///
+  /// Example output:
+  ///     $ fx netls
+  ///     > device liliac-shore-only-last (fe80::82e4:da4d:fe81:227d/3)
+  Future<String> netls() async {
+    try {
+      final RunResult process = await runAsync(
+        <String>['fx', 'netls', '--nowait']);
+      return process.stdout;
+    } on ArgumentError catch (exception) {
+      throwToolExit('$exception');
+    }
+    return null;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_workflow.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_workflow.dart
new file mode 100644
index 0000000..7978376
--- /dev/null
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_workflow.dart
@@ -0,0 +1,34 @@
+// 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 '../base/context.dart';
+import '../base/os.dart';
+import '../base/platform.dart';
+import '../doctor.dart';
+
+/// The [FuchsiaWorkflow] instance.
+FuchsiaWorkflow get fuchsiaWorkflow => context[FuchsiaWorkflow];
+
+/// The Fuchsia-specific implementation of a [Workflow].
+///
+/// This workflow assumes development within the fuchsia source tree,
+/// including a working fx command-line tool in the user's PATH.
+class FuchsiaWorkflow implements Workflow {
+
+  @override
+  bool get appliesToHostPlatform => platform.isLinux || platform.isMacOS;
+
+  @override
+  bool get canListDevices {
+    return os.which('fx') != null;
+  }
+
+  @override
+  bool get canLaunchDevices {
+    return os.which('fx') != null;
+  }
+
+  @override
+  bool get canListEmulators => false;
+}