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;
+}
diff --git a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
new file mode 100644
index 0000000..8695773
--- /dev/null
+++ b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
@@ -0,0 +1,29 @@
+// 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/src/fuchsia/fuchsia_device.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('fuchsia device', () {
+    testUsingContext('stores the requested id and name', () {
+      const String deviceId = 'e80::0000:a00a:f00f:2002/3';
+      const String name = 'halfbaked';
+      final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name);
+      expect(device.id, deviceId);
+      expect(device.name, name);
+    });
+
+    test('parse netls log output', () {
+      const String example = 'device lilia-shore-only-last (fe80::0000:a00a:f00f:2002/3)';
+      final List<FuchsiaDevice> devices = parseFuchsiaDeviceOutput(example);
+
+      expect(devices.length, 1);
+      expect(devices.first.id, 'fe80::0000:a00a:f00f:2002/3');
+      expect(devices.first.name, 'lilia-shore-only-last');
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/fuchsia/fuchsia_workflow_test.dart b/packages/flutter_tools/test/fuchsia/fuchsia_workflow_test.dart
new file mode 100644
index 0000000..974c55f
--- /dev/null
+++ b/packages/flutter_tools/test/fuchsia/fuchsia_workflow_test.dart
@@ -0,0 +1,41 @@
+// 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/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
+class MockFile extends Mock implements File {}
+
+void main() {
+  bool fxPresent = false;
+  final MockOperatingSystemUtils utils = MockOperatingSystemUtils();
+  final MockFile file = MockFile();
+  when(utils.which('fx')).thenAnswer((Invocation _) => fxPresent ? file : null);
+
+  group('android workflow', () {
+    testUsingContext('can not list and launch devices if there is no `fx` command', () {
+      fxPresent = false;
+      expect(fuchsiaWorkflow.canLaunchDevices, false);
+      expect(fuchsiaWorkflow.canListDevices, false);
+      expect(fuchsiaWorkflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      OperatingSystemUtils: () => utils,
+    });
+
+    testUsingContext('can list and launch devices supported if there is a `fx` command', () {
+      fxPresent = true;
+      expect(fuchsiaWorkflow.canLaunchDevices, true);
+      expect(fuchsiaWorkflow.canListDevices, true);
+      expect(fuchsiaWorkflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      OperatingSystemUtils: () => utils,
+    });
+  });
+}