Add ServiceProtocolDiscovery
diff --git a/packages/flutter_tools/lib/src/service_protocol.dart b/packages/flutter_tools/lib/src/service_protocol.dart
new file mode 100644
index 0000000..ffbbdfa
--- /dev/null
+++ b/packages/flutter_tools/lib/src/service_protocol.dart
@@ -0,0 +1,52 @@
+// Copyright 2016 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 'device.dart';
+
+/// Discover service protocol ports on devices.
+class ServiceProtocolDiscovery {
+  /// [logReader] A [DeviceLogReader] to look for Observatory messages in.
+  ServiceProtocolDiscovery(DeviceLogReader logReader)
+      : _logReader = logReader {
+    assert(_logReader != null);
+    if (!_logReader.isReading)
+      _logReader.start();
+
+    _logReader.lines.listen(_onLine);
+  }
+
+  final DeviceLogReader _logReader;
+  Completer _completer = new Completer();
+
+  /// The [Future] returned by this function will complete when the next
+  /// service protocol port is found.
+  Future<int> nextPort() {
+    return _completer.future;
+  }
+
+  void _onLine(String line) {
+    int portNumber = 0;
+    if (line.startsWith('Observatory listening on http://')) {
+      try {
+        RegExp portExp = new RegExp(r"\d+.\d+.\d+.\d+:(\d+)");
+        var port = portExp.firstMatch(line).group(1);
+        portNumber = int.parse(port);
+      } catch (_) {
+        // Ignore errors.
+      }
+    }
+    if (portNumber != 0) {
+      _located(portNumber);
+    }
+  }
+
+  void _located(int port) {
+    assert(_completer != null);
+    assert(!_completer.isCompleted);
+    _completer.complete(port);
+    _completer = new Completer();
+  }
+}
diff --git a/packages/flutter_tools/test/service_protocol_test.dart b/packages/flutter_tools/test/service_protocol_test.dart
new file mode 100644
index 0000000..7c62e57
--- /dev/null
+++ b/packages/flutter_tools/test/service_protocol_test.dart
@@ -0,0 +1,47 @@
+// Copyright 2016 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:test/test.dart';
+
+import 'package:flutter_tools/src/service_protocol.dart';
+
+import 'src/mocks.dart';
+
+main() => defineTests();
+
+defineTests() {
+  group('service_protocol', () {
+    test('Discovery Heartbeat', () async {
+      MockDeviceLogReader logReader = new MockDeviceLogReader();
+      ServiceProtocolDiscovery discoverer =
+          new ServiceProtocolDiscovery(logReader);
+      // Get next port future.
+      Future nextPort = discoverer.nextPort();
+      expect(nextPort, isNotNull);
+      // Inject some lines.
+      logReader.addLine('HELLO WORLD');
+      logReader.addLine(
+          'Observatory listening on http://127.0.0.1:9999');
+      // Await the port.
+      expect(await nextPort, 9999);
+      // Get next port future.
+      nextPort = discoverer.nextPort();
+      logReader.addLine(
+          'Observatory listening on http://127.0.0.1:3333');
+      expect(await nextPort, 3333);
+      // Get next port future.
+      nextPort = discoverer.nextPort();
+      // Inject some bad lines.
+      logReader.addLine('Observatory listening on http://127.0.0.1');
+      logReader.addLine('Observatory listening on http://127.0.0.1:');
+      logReader.addLine(
+      'Observatory listening on http://127.0.0.1:apple');
+      int port = await nextPort.timeout(
+      const Duration(milliseconds: 100), onTimeout: () => 77);
+      // Expect the timeout port.
+      expect(port, 77);
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 38a4610..1d7b819 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -2,6 +2,7 @@
 // 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:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/application_package.dart';
 import 'package:flutter_tools/src/build_configuration.dart';
@@ -51,6 +52,39 @@
     iOSSimulator: new MockIOSSimulator());
 }
 
+class MockDeviceLogReader extends DeviceLogReader {
+  String get name => 'MockLogReader';
+
+  final StreamController<String> _linesStreamController =
+      new StreamController<String>.broadcast();
+
+  final Completer _finishedCompleter = new Completer();
+
+  Stream<String> get lines => _linesStreamController.stream;
+
+  void addLine(String line) {
+    _linesStreamController.add(line);
+  }
+
+  bool _started = false;
+
+  Future start() {
+    assert(!_started);
+    _started = true;
+    return new Future.value(this);
+  }
+
+  bool get isReading => _started;
+
+  Future stop() {
+    assert(_started);
+    _started = false;
+    return new Future.value(this);
+  }
+
+  Future get finished => _finishedCompleter.future;
+}
+
 void applyMocksToCommand(FlutterCommand command) {
   command
     ..applicationPackages = new MockApplicationPackageStore()