Fix race condition in protocol_discovery.dart (#10092)

For some reaosn, when we discovered our URI, we were re-instantiating
the `Completer` instance variable whose future we listen to in `nextUri()`.
This led to a race between a caller calling `nextUri()` and us discovering
the URI. If we happened to discover our URI before a caller called
`nextUri()`, then they would be left waiting on a future from the newly
allocated `Completer` (which would never complete).

Fixes #10064
diff --git a/packages/flutter_tools/lib/src/protocol_discovery.dart b/packages/flutter_tools/lib/src/protocol_discovery.dart
index c4a6e12..d3f3654 100644
--- a/packages/flutter_tools/lib/src/protocol_discovery.dart
+++ b/packages/flutter_tools/lib/src/protocol_discovery.dart
@@ -9,72 +9,62 @@
 import 'device.dart';
 import 'globals.dart';
 
-/// Discover service protocol on a device
-/// and forward the service protocol device port to the host.
+/// Discovers a specific service protocol on a device, and forward the service
+/// protocol device port to the host.
 class ProtocolDiscovery {
-  /// [logReader] - a [DeviceLogReader] to look for service messages in.
-  ProtocolDiscovery(DeviceLogReader logReader, String serviceName,
-      {this.portForwarder, this.hostPort, this.defaultHostPort})
-      : _logReader = logReader, _serviceName = serviceName {
+  ProtocolDiscovery._(
+    DeviceLogReader logReader,
+    String serviceName, {
+    this.portForwarder,
+    this.hostPort,
+    this.defaultHostPort,
+  }) : _logReader = logReader, _serviceName = serviceName {
     assert(_logReader != null);
-    _subscription = _logReader.logLines.listen(_onLine);
     assert(portForwarder == null || defaultHostPort != null);
+    _deviceLogSubscription = _logReader.logLines.listen(_onLine);
   }
 
   factory ProtocolDiscovery.observatory(DeviceLogReader logReader,
           {DevicePortForwarder portForwarder, int hostPort}) =>
-      new ProtocolDiscovery(logReader, kObservatoryService,
+      new ProtocolDiscovery._(logReader, _kObservatoryService,
           portForwarder: portForwarder,
           hostPort: hostPort,
           defaultHostPort: kDefaultObservatoryPort);
 
   factory ProtocolDiscovery.diagnosticService(DeviceLogReader logReader,
           {DevicePortForwarder portForwarder, int hostPort}) =>
-      new ProtocolDiscovery(logReader, kDiagnosticService,
+      new ProtocolDiscovery._(logReader, _kDiagnosticService,
           portForwarder: portForwarder,
           hostPort: hostPort,
           defaultHostPort: kDefaultDiagnosticPort);
 
-  static const String kObservatoryService = 'Observatory';
-  static const String kDiagnosticService = 'Diagnostic server';
+  static const String _kObservatoryService = 'Observatory';
+  static const String _kDiagnosticService = 'Diagnostic server';
 
   final DeviceLogReader _logReader;
   final String _serviceName;
   final DevicePortForwarder portForwarder;
-  int hostPort;
+  final int hostPort;
   final int defaultHostPort;
+  final Completer<Uri> _completer = new Completer<Uri>();
 
-  Completer<Uri> _completer = new Completer<Uri>();
-  StreamSubscription<String> _subscription;
+  StreamSubscription<String> _deviceLogSubscription;
 
-  /// The [Future] returned by this function will complete when the next service
-  /// Uri is found.
-  Future<Uri> nextUri() async {
-    final Uri deviceUri = await _completer.future.timeout(
-      const Duration(seconds: 60), onTimeout: () {
-        throwToolExit('Timeout while attempting to retrieve Uri for $_serviceName');
-      }
-    );
-    printTrace('$_serviceName Uri on device: $deviceUri');
-    Uri hostUri;
-    if (portForwarder != null) {
-      final int devicePort = deviceUri.port;
-      hostPort ??= await portScanner.findPreferredPort(defaultHostPort);
-      hostPort = await portForwarder
-          .forward(devicePort, hostPort: hostPort)
-          .timeout(const Duration(seconds: 60), onTimeout: () {
-            throwToolExit('Timeout while atempting to foward device port $devicePort for $_serviceName');
-          });
-      printTrace('Forwarded host port $hostPort to device port $devicePort for $_serviceName');
-      hostUri = deviceUri.replace(port: hostPort);
-    } else {
-      hostUri = deviceUri;
-    }
-    return hostUri;
+  /// The discovered service URI.
+  Future<Uri> get uri {
+    return _completer.future
+        .timeout(const Duration(seconds: 60), onTimeout: () {
+          throwToolExit('Timeout while attempting to retrieve Uri for $_serviceName');
+        }).whenComplete(() {
+          _stopScrapingLogs();
+        });
   }
 
-  void cancel() {
-    _subscription.cancel();
+  Future<Null> cancel() => _stopScrapingLogs();
+
+  Future<Null> _stopScrapingLogs() async {
+    await _deviceLogSubscription?.cancel();
+    _deviceLogSubscription = null;
   }
 
   void _onLine(String line) {
@@ -84,19 +74,35 @@
     if (index >= 0) {
       try {
         uri = Uri.parse(line.substring(index + prefix.length));
-      } catch (_) {
-        // Ignore errors.
+      } catch (error) {
+        _stopScrapingLogs();
+        _completer.completeError(error);
       }
     }
-    if (uri != null)
-      _located(uri);
+
+    if (uri != null) {
+      assert(!_completer.isCompleted);
+      _stopScrapingLogs();
+      _completer.complete(_forwardPort(uri));
+    }
   }
 
-  void _located(Uri uri) {
-    assert(_completer != null);
-    assert(!_completer.isCompleted);
+  Future<Uri> _forwardPort(Uri deviceUri) async {
+    printTrace('$_serviceName Uri on device: $deviceUri');
+    Uri hostUri = deviceUri;
 
-    _completer.complete(uri);
-    _completer = new Completer<Uri>();
+    if (portForwarder != null) {
+      final int devicePort = deviceUri.port;
+      int hostPort = this.hostPort ?? await portScanner.findPreferredPort(defaultHostPort);
+      hostPort = await portForwarder
+          .forward(devicePort, hostPort: hostPort)
+          .timeout(const Duration(seconds: 60), onTimeout: () {
+        throwToolExit('Timeout while atempting to foward device port $devicePort for $_serviceName');
+      });
+      printTrace('Forwarded host port $hostPort to device port $devicePort for $_serviceName');
+      hostUri = deviceUri.replace(port: hostPort);
+    }
+
+    return hostUri;
   }
 }