Remove race conditions involving finding available ports (#18698)

This is an attempt to reland #18488 with less breakage on macOS.
diff --git a/dev/devicelab/bin/tasks/commands_test.dart b/dev/devicelab/bin/tasks/commands_test.dart
index 6aa1f6b..0b420c6 100644
--- a/dev/devicelab/bin/tasks/commands_test.dart
+++ b/dev/devicelab/bin/tasks/commands_test.dart
@@ -35,12 +35,14 @@
         .listen((String line) {
           print('run:stdout: $line');
           stdout.add(line);
-          if (lineContainsServicePort(line)) {
+          if (vmServicePort == null) {
             vmServicePort = parseServicePort(line);
-            print('service protocol connection available at port $vmServicePort');
-            print('run: ready!');
-            ready.complete();
-            ok ??= true;
+            if (vmServicePort != null) {
+              print('service protocol connection available at port $vmServicePort');
+              print('run: ready!');
+              ready.complete();
+              ok ??= true;
+            }
           }
         });
       run.stderr
diff --git a/dev/devicelab/bin/tasks/routing_test.dart b/dev/devicelab/bin/tasks/routing_test.dart
index 269a13b..51cb30e 100644
--- a/dev/devicelab/bin/tasks/routing_test.dart
+++ b/dev/devicelab/bin/tasks/routing_test.dart
@@ -41,12 +41,14 @@
         .transform(const LineSplitter())
         .listen((String line) {
           print('run:stdout: $line');
-          if (lineContainsServicePort(line)) {
+          if (vmServicePort == null) {
             vmServicePort = parseServicePort(line);
-            print('service protocol connection available at port $vmServicePort');
-            print('run: ready!');
-            ready.complete();
-            ok ??= true;
+            if (vmServicePort != null) {
+              print('service protocol connection available at port $vmServicePort');
+              print('run: ready!');
+              ready.complete();
+              ok ??= true;
+            }
           }
         });
       run.stderr
diff --git a/dev/devicelab/bin/tasks/service_extensions_test.dart b/dev/devicelab/bin/tasks/service_extensions_test.dart
index 57f7cc6..966218e 100644
--- a/dev/devicelab/bin/tasks/service_extensions_test.dart
+++ b/dev/devicelab/bin/tasks/service_extensions_test.dart
@@ -33,12 +33,14 @@
           .transform(const LineSplitter())
           .listen((String line) {
         print('run:stdout: $line');
-        if (lineContainsServicePort(line)) {
+        if (vmServicePort == null) {
           vmServicePort = parseServicePort(line);
-          print('service protocol connection available at port $vmServicePort');
-          print('run: ready!');
-          ready.complete();
-          ok ??= true;
+          if (vmServicePort != null) {
+            print('service protocol connection available at port $vmServicePort');
+            print('run: ready!');
+            ready.complete();
+            ok ??= true;
+          }
         }
       });
       run.stderr
diff --git a/dev/devicelab/lib/framework/runner.dart b/dev/devicelab/lib/framework/runner.dart
index 0e95cb9..d9457ff 100644
--- a/dev/devicelab/lib/framework/runner.dart
+++ b/dev/devicelab/lib/framework/runner.dart
@@ -28,9 +28,8 @@
   if (!file(taskExecutable).existsSync())
     throw 'Executable Dart file not found: $taskExecutable';
 
-  final int vmServicePort = await findAvailablePort();
   final Process runner = await startProcess(dartBin, <String>[
-    '--enable-vm-service=$vmServicePort',
+    '--enable-vm-service=0', // zero causes the system to choose a free port
     '--no-pause-isolates-on-exit',
     taskExecutable,
   ]);
@@ -41,10 +40,17 @@
     runnerFinished = true;
   });
 
+  final Completer<int> port = new Completer<int>();
+
   final StreamSubscription<String> stdoutSub = runner.stdout
       .transform(const Utf8Decoder())
       .transform(const LineSplitter())
       .listen((String line) {
+    if (!port.isCompleted) {
+      final int portValue = parseServicePort(line, prefix: 'Observatory listening on ');
+      if (portValue != null)
+        port.complete(portValue);
+    }
     if (!silent) {
       stdout.writeln('[$taskName] [STDOUT] $line');
     }
@@ -59,7 +65,7 @@
 
   String waitingFor = 'connection';
   try {
-    final VMIsolateRef isolate = await _connectToRunnerIsolate(vmServicePort);
+    final VMIsolateRef isolate = await _connectToRunnerIsolate(await port.future);
     waitingFor = 'task completion';
     final Map<String, dynamic> taskResult =
         await isolate.invokeExtension('ext.cocoonRunTask').timeout(taskTimeoutWithGracePeriod);
diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart
index 59ed19c..5acc8e3 100644
--- a/dev/devicelab/lib/framework/utils.dart
+++ b/dev/devicelab/lib/framework/utils.dart
@@ -494,21 +494,6 @@
   return completer.future;
 }
 
-/// Return an unused TCP port number.
-Future<int> findAvailablePort() async {
-  int port = 20000;
-  while (true) {
-    try {
-      final ServerSocket socket =
-          await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, port); // ignore: deprecated_member_use
-      await socket.close();
-      return port;
-    } catch (_) {
-      port++;
-    }
-  }
-}
-
 bool canRun(String path) => _processManager.canRun(path);
 
 String extractCloudAuthTokenArg(List<String> rawArgs) {
@@ -531,13 +516,20 @@
   return token;
 }
 
-// "An Observatory debugger and profiler on ... is available at: http://127.0.0.1:8100/"
-final RegExp _kObservatoryRegExp = new RegExp(r'An Observatory debugger .* is available at: (\S+:(\d+))');
-
-bool lineContainsServicePort(String line) => line.contains(_kObservatoryRegExp);
-
-int parseServicePort(String line) {
-  final Match match = _kObservatoryRegExp.firstMatch(line);
+/// Tries to extract a port from the string.
+///
+/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
+///
+/// The `multiLine` flag should be set to true if `line` is actually a buffer of many lines.
+int parseServicePort(String line, {
+  String prefix = 'An Observatory debugger .* is available at: ',
+  bool multiLine = false,
+}) {
+  // e.g. "An Observatory debugger and profiler on ... is available at: http://127.0.0.1:8100/"
+  final RegExp pattern = new RegExp('$prefix(\\S+:(\\d+)/\\S*)\$', multiLine: multiLine);
+  final Match match = pattern.firstMatch(line);
+  print(pattern);
+  print(match);
   return match == null ? null : int.parse(match.group(2));
 }
 
diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart
index cc19322..5db416d 100644
--- a/dev/devicelab/lib/tasks/perf_tests.dart
+++ b/dev/devicelab/lib/tasks/perf_tests.dart
@@ -441,8 +441,6 @@
       if (deviceOperatingSystem == DeviceOperatingSystem.ios)
         await prepareProvisioningCertificates(testDirectory);
 
-      final int observatoryPort = await findAvailablePort();
-
       final List<String> runOptions = <String>[
         '-v',
         '--profile',
@@ -450,11 +448,14 @@
         '-d',
         deviceId,
         '--observatory-port',
-        observatoryPort.toString(),
+        '0',
       ];
       if (testTarget != null)
         runOptions.addAll(<String>['-t', testTarget]);
-      await flutter('run', options: runOptions);
+      final String output = await evalFlutter('run', options: runOptions);
+      final int observatoryPort = parseServicePort(output, prefix: 'Successfully connected to service protocol: ', multiLine: true);
+      if (observatoryPort == null)
+        throw new Exception('Could not find observatory port in "flutter run" output.');
 
       final Map<String, dynamic> startData = await device.getMemoryStats(packageName);
 
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 24a3191..9028b9e 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -15,7 +15,6 @@
 import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
-import '../base/port_scanner.dart';
 import '../base/process.dart';
 import '../base/process_manager.dart';
 import '../base/utils.dart';
@@ -844,7 +843,7 @@
         final int devicePort = _extractPort(splitLine[2]);
 
         // Failed, skip.
-        if ((hostPort == null) || (devicePort == null))
+        if (hostPort == null || devicePort == null)
           continue;
 
         ports.add(new ForwardedPort(hostPort, devicePort));
@@ -855,16 +854,30 @@
   }
 
   @override
-  Future<int> forward(int devicePort, { int hostPort }) async {
-    if ((hostPort == null) || (hostPort == 0)) {
-      // Auto select host port.
-      hostPort = await portScanner.findAvailablePort();
-    }
-
-    await runCheckedAsync(device.adbCommandForDevice(
+  Future<int> forward(int devicePort, {int hostPort}) async {
+    hostPort ??= 0;
+    final RunResult process = await runCheckedAsync(device.adbCommandForDevice(
       <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
     ));
 
+    if (process.stderr.isNotEmpty)
+      process.throwException('adb returned error:\n${process.stderr}');
+
+    if (process.exitCode != 0) {
+      if (process.stdout.isNotEmpty)
+        process.throwException('adb returned error:\n${process.stdout}');
+      process.throwException('adb failed without a message');
+    }
+
+    if (hostPort == 0) {
+      if (process.stdout.isEmpty)
+        process.throwException('adb did not report forwarded port');
+      hostPort = int.tryParse(process.stdout) ?? (throw 'adb returned invalid port number:\n${process.stdout}');
+    } else {
+      if (process.stdout.isNotEmpty)
+        process.throwException('adb returned error:\n${process.stdout}');
+    }
+
     return hostPort;
   }
 
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index 1599f0e..f0ad58f 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -169,10 +169,10 @@
   }
 
   Future<LicensesAccepted> get licensesAccepted async {
-    LicensesAccepted status = LicensesAccepted.unknown;
+    LicensesAccepted status;
 
     void _onLine(String line) {
-      if (licenseAccepted.hasMatch(line)) {
+      if (status == null && licenseAccepted.hasMatch(line)) {
         status = LicensesAccepted.all;
       } else if (licenseCounts.hasMatch(line)) {
         final Match match = licenseCounts.firstMatch(line);
@@ -196,22 +196,22 @@
     );
     process.stdin.write('n\n');
     final Future<void> output = process.stdout
-        .transform(const Utf8Decoder(allowMalformed: true))
-        .transform(const LineSplitter())
-        .listen(_onLine)
-        .asFuture<void>(null);
+      .transform(const Utf8Decoder(allowMalformed: true))
+      .transform(const LineSplitter())
+      .listen(_onLine)
+      .asFuture<void>(null);
     final Future<void> errors = process.stderr
-        .transform(const Utf8Decoder(allowMalformed: true))
-        .transform(const LineSplitter())
-        .listen(_onLine)
-        .asFuture<void>(null);
+      .transform(const Utf8Decoder(allowMalformed: true))
+      .transform(const LineSplitter())
+      .listen(_onLine)
+      .asFuture<void>(null);
     try {
       await Future.wait<void>(<Future<void>>[output, errors]).timeout(const Duration(seconds: 30));
     } catch (TimeoutException) {
       printTrace('Intentionally killing ${androidSdk.sdkManagerPath}');
       processManager.killPid(process.pid);
     }
-    return status;
+    return status ?? LicensesAccepted.unknown;
   }
 
   /// Run the Android SDK manager tool in order to accept SDK licenses.
diff --git a/packages/flutter_tools/lib/src/base/common.dart b/packages/flutter_tools/lib/src/base/common.dart
index ea60c69..01c2cba 100644
--- a/packages/flutter_tools/lib/src/base/common.dart
+++ b/packages/flutter_tools/lib/src/base/common.dart
@@ -5,8 +5,6 @@
 import 'file_system.dart';
 import 'platform.dart';
 
-const int kDefaultObservatoryPort = 8100;
-
 /// Return the absolute path of the user's home directory
 String get homeDirPath {
   if (_homeDirPath == null) {
diff --git a/packages/flutter_tools/lib/src/base/port_scanner.dart b/packages/flutter_tools/lib/src/base/port_scanner.dart
deleted file mode 100644
index fda5316..0000000
--- a/packages/flutter_tools/lib/src/base/port_scanner.dart
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2017 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 'context.dart';
-import 'io.dart';
-
-const int _kMaxSearchIterations = 20;
-
-PortScanner get portScanner => context[PortScanner];
-
-abstract class PortScanner {
-  const PortScanner();
-
-  /// Returns true if the specified [port] is available to bind to.
-  Future<bool> isPortAvailable(int port);
-
-  /// Returns an available ephemeral port.
-  Future<int> findAvailablePort();
-
-  /// Returns an available port as close to [defaultPort] as possible.
-  ///
-  /// If [defaultPort] is available, this will return it. Otherwise, it will
-  /// search for an available port close to [defaultPort]. If it cannot find one,
-  /// it will return any available port.
-  Future<int> findPreferredPort(int defaultPort) async {
-    int iterationCount = 0;
-
-    while (iterationCount < _kMaxSearchIterations) {
-      final int port = defaultPort + iterationCount;
-      if (await isPortAvailable(port))
-        return port;
-      iterationCount++;
-    }
-
-    return findAvailablePort();
-  }
-}
-
-class HostPortScanner extends PortScanner {
-  const HostPortScanner();
-
-  @override
-  Future<bool> isPortAvailable(int port) async {
-    try {
-      // TODO(ianh): This is super racy.
-      final ServerSocket socket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, port); // ignore: deprecated_member_use
-      await socket.close();
-      return true;
-    } catch (error) {
-      return false;
-    }
-  }
-
-  @override
-  Future<int> findAvailablePort() async {
-    ServerSocket socket;
-    try {
-      socket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 0); // ignore: deprecated_member_use
-    } on SocketException {
-      socket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V6, 0, v6Only: true); // ignore: deprecated_member_use
-    }
-    final int port = socket.port;
-    await socket.close();
-    return port;
-  }
-}
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index 992687a..3f7507b 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -229,7 +229,7 @@
     workingDirectory: workingDirectory,
     environment: _environment(allowReentrantFlutter, environment),
   );
-  final RunResult runResults = new RunResult(results);
+  final RunResult runResults = new RunResult(results, cmd);
   printTrace(runResults.toString());
   return runResults;
 }
@@ -240,10 +240,10 @@
   Map<String, String> environment
 }) async {
   final RunResult result = await runAsync(
-      cmd,
-      workingDirectory: workingDirectory,
-      allowReentrantFlutter: allowReentrantFlutter,
-      environment: environment
+    cmd,
+    workingDirectory: workingDirectory,
+    allowReentrantFlutter: allowReentrantFlutter,
+    environment: environment,
   );
   if (result.exitCode != 0)
     throw 'Exit code ${result.exitCode} from: ${cmd.join(' ')}:\n$result';
@@ -364,10 +364,12 @@
 }
 
 class RunResult {
-  RunResult(this.processResult);
+  RunResult(this.processResult, this._command) : assert(_command != null), assert(_command.isNotEmpty);
 
   final ProcessResult processResult;
 
+  final List<String> _command;
+
   int get exitCode => processResult.exitCode;
   String get stdout => processResult.stdout;
   String get stderr => processResult.stderr;
@@ -381,4 +383,14 @@
       out.writeln(processResult.stderr);
     return out.toString().trimRight();
   }
+
+ /// Throws a [ProcessException] with the given `message`.
+ void throwException(String message) {
+    throw new ProcessException(
+      _command.first,
+      _command.skip(1).toList(),
+      message,
+      exitCode,
+    );
+  }
 }
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 13fe2e1..d4004f1 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -8,6 +8,7 @@
 import '../base/io.dart';
 import '../cache.dart';
 import '../device.dart';
+import '../globals.dart';
 import '../protocol_discovery.dart';
 import '../resident_runner.dart';
 import '../run_hot.dart';
@@ -77,7 +78,7 @@
       try {
         observatoryDiscovery = new ProtocolDiscovery.observatory(
             device.getLogReader(), portForwarder: device.portForwarder);
-        print('Listening.');
+        printStatus('Listening.');
         observatoryUri = await observatoryDiscovery.uri;
       } finally {
         await observatoryDiscovery?.cancel();
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 945b55e..47a3036 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -55,8 +55,7 @@
   void usesPortOptions() {
     argParser.addOption('observatory-port',
         help: 'Listen to the given port for an observatory debugger connection.\n'
-              'Specifying port 0 will find a random free port.\n'
-              'Defaults to the first available port after $kDefaultObservatoryPort.'
+              'Specifying port 0 (the default) will find a random free port.'
     );
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart
index eb67025..3999a5b 100644
--- a/packages/flutter_tools/lib/src/commands/trace.dart
+++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -16,14 +16,18 @@
 class TraceCommand extends FlutterCommand {
   TraceCommand() {
     requiresPubspecYaml();
-    argParser.addFlag('start', negatable: false, help: 'Start tracing.');
-    argParser.addFlag('stop', negatable: false, help: 'Stop tracing.');
-    argParser.addOption('out', help: 'Specify the path of the saved trace file.');
-    argParser.addOption('duration',
-        defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.');
     argParser.addOption('debug-port',
-        defaultsTo: kDefaultObservatoryPort.toString(),
-        help: 'Local port where the observatory is listening.');
+      help: 'Local port where the observatory is listening. Required.',
+    );
+    argParser.addFlag('start', negatable: false, help: 'Start tracing. Implied if --stop is also omitted.');
+    argParser.addFlag('stop', negatable: false, help: 'Stop tracing. Implied if --start is also omitted.');
+    argParser.addOption('duration',
+      abbr: 'd',
+      help: 'Time to wait after starting (if --start is specified or implied) and before\n'
+            'stopping (if --stop is specified or implied).\n'
+            'Defaults to ten seconds if --stop is specified or implied, zero otherwise.',
+    );
+    argParser.addOption('out', help: 'Specify the path of the saved trace file.');
   }
 
   @override
@@ -34,13 +38,39 @@
 
   @override
   final String usageFooter =
-    '\`trace\` called with no arguments will automatically start tracing, delay a set amount of\n'
-    'time (controlled by --duration), and stop tracing. To explicitly control tracing, call trace\n'
-    'with --start and later with --stop.';
+    '\`trace\` called without the --start or --stop flags will automatically start tracing,\n'
+    'delay a set amount of time (controlled by --duration), and stop tracing. To explicitly\n'
+    'control tracing, call trace with --start and later with --stop.\n'
+    'The --debug-port argument is required.';
 
   @override
   Future<Null> runCommand() async {
-    final int observatoryPort = int.parse(argResults['debug-port']);
+    int observatoryPort;
+    if (argResults.wasParsed('debug-port')) {
+      observatoryPort = int.tryParse(argResults['debug-port']);
+    }
+    if (observatoryPort == null) {
+      throwToolExit('The --debug-port argument must be specified.');
+    }
+
+    bool start = argResults['start'];
+    bool stop = argResults['stop'];
+    if (!start && !stop) {
+      start = true;
+      stop = true;
+    }
+    assert(start || stop);
+
+    Duration duration;
+    if (argResults.wasParsed('duration')) {
+      try {
+        duration = new Duration(seconds: int.parse(argResults['duration']));
+      } on FormatException {
+        throwToolExit('Invalid duration passed to --duration; it should be a positive number of seconds.');
+      }
+    } else {
+      duration = stop ? const Duration(seconds: 10) : Duration.zero;
+    }
 
     // TODO(danrubel): this will break if we move to the new observatory URL
     // See https://github.com/flutter/flutter/issues/7038
@@ -56,20 +86,11 @@
 
     Cache.releaseLockEarly();
 
-    if ((!argResults['start'] && !argResults['stop']) ||
-        (argResults['start'] && argResults['stop'])) {
-      // Setting neither flags or both flags means do both commands and wait
-      // duration seconds in between.
+    if (start)
       await tracing.startTracing();
-      await new Future<Null>.delayed(
-        new Duration(seconds: int.parse(argResults['duration'])),
-        () => _stopTracing(tracing)
-      );
-    } else if (argResults['stop']) {
+    await new Future<Null>.delayed(duration);
+    if (stop)
       await _stopTracing(tracing);
-    } else {
-      await tracing.startTracing();
-    }
   }
 
   Future<Null> _stopTracing(Tracing tracing) async {
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index d5c23c3..cdc640f 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -19,7 +19,6 @@
 import 'base/logger.dart';
 import 'base/os.dart';
 import 'base/platform.dart';
-import 'base/port_scanner.dart';
 import 'base/utils.dart';
 import 'cache.dart';
 import 'compile.dart';
@@ -70,7 +69,6 @@
       KernelCompiler: () => const KernelCompiler(),
       Logger: () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger(),
       OperatingSystemUtils: () => new OperatingSystemUtils(),
-      PortScanner: () => const HostPortScanner(),
       SimControl: () => new SimControl(),
       Stdio: () => const Stdio(),
       Usage: () => new Usage(),
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index b2009b7..073c8f5 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -7,10 +7,8 @@
 
 import 'android/android_device.dart';
 import 'application_package.dart';
-import 'base/common.dart';
 import 'base/context.dart';
 import 'base/file_system.dart';
-import 'base/port_scanner.dart';
 import 'base/utils.dart';
 import 'build_info.dart';
 import 'globals.dart';
@@ -367,14 +365,6 @@
   final int observatoryPort;
 
   bool get hasObservatoryPort => observatoryPort != null;
-
-  /// Return the user specified observatory port. If that isn't available,
-  /// return [kDefaultObservatoryPort], or a port close to that one.
-  Future<int> findBestObservatoryPort() {
-    if (hasObservatoryPort)
-      return new Future<int>.value(observatoryPort);
-    return portScanner.findPreferredPort(observatoryPort ?? kDefaultObservatoryPort);
-  }
 }
 
 class LaunchResult {
@@ -414,9 +404,9 @@
   List<ForwardedPort> get forwardedPorts;
 
   /// Forward [hostPort] on the host to [devicePort] on the device.
-  /// If [hostPort] is null, will auto select a host port.
+  /// If [hostPort] is null or zero, will auto select a host port.
   /// Returns a Future that completes with the host port.
-  Future<int> forward(int devicePort, { int hostPort });
+  Future<int> forward(int devicePort, {int hostPort});
 
   /// Stops forwarding [forwardedPort].
   Future<Null> unforward(ForwardedPort forwardedPort);
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index abc212b..9e93624 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -10,7 +10,6 @@
 import '../base/io.dart';
 import '../base/logger.dart';
 import '../base/platform.dart';
-import '../base/port_scanner.dart';
 import '../base/process.dart';
 import '../base/process_manager.dart';
 import '../build_info.dart';
@@ -199,14 +198,9 @@
     if (debuggingOptions.useTestFonts)
       launchArguments.add('--use-test-fonts');
 
-    if (debuggingOptions.debuggingEnabled) {
+    if (debuggingOptions.debuggingEnabled)
       launchArguments.add('--enable-checked-mode');
 
-      // Note: We do NOT need to set the observatory port since this is going to
-      // be setup on the device. Let it pick a port automatically. We will check
-      // the port picked and scrape that later.
-    }
-
     if (debuggingOptions.enableSoftwareRendering)
       launchArguments.add('--enable-software-rendering');
 
@@ -507,28 +501,46 @@
   @override
   List<ForwardedPort> get forwardedPorts => _forwardedPorts;
 
+  static const Duration _kiProxyPortForwardTimeout = const Duration(seconds: 1);
+
   @override
   Future<int> forward(int devicePort, {int hostPort}) async {
-    if ((hostPort == null) || (hostPort == 0)) {
-      // Auto select host port.
-      hostPort = await portScanner.findAvailablePort();
+    final bool autoselect = hostPort == null || hostPort == 0;
+    if (autoselect)
+      hostPort = 1024;
+
+    Process process;
+
+    bool connected = false;
+    while (!connected) {
+      printTrace('attempting to forward device port $devicePort to host port $hostPort');
+      // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID
+      process = await runCommand(<String>[
+        device._iproxyPath,
+        hostPort.toString(),
+        devicePort.toString(),
+        device.id,
+      ]);
+      // TODO(ianh): This is a flakey race condition, https://github.com/libimobiledevice/libimobiledevice/issues/674
+      connected = !await process.stdout.isEmpty.timeout(_kiProxyPortForwardTimeout, onTimeout: () => false);
+      if (!connected) {
+        if (autoselect) {
+          hostPort += 1;
+          if (hostPort > 65535)
+            throw new Exception('Could not find open port on host.');
+        } else {
+          throw new Exception('Port $hostPort is not available.');
+        }
+      }
     }
+    assert(connected);
+    assert(process != null);
 
-    // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID
-    final Process process = await runCommand(<String>[
-      device._iproxyPath,
-      hostPort.toString(),
-      devicePort.toString(),
-      device.id,
-    ]);
-
-    final ForwardedPort forwardedPort = new ForwardedPort.withContext(hostPort,
-        devicePort, process);
-
+    final ForwardedPort forwardedPort = new ForwardedPort.withContext(
+      hostPort, devicePort, process,
+    );
     printTrace('Forwarded port $forwardedPort');
-
     _forwardedPorts.add(forwardedPort);
-
     return hostPort;
   }
 
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 99be8fc..4b7a528 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -305,8 +305,7 @@
         args.add('--skia-deterministic-rendering');
       if (debuggingOptions.useTestFonts)
         args.add('--use-test-fonts');
-
-      final int observatoryPort = await debuggingOptions.findBestObservatoryPort();
+      final int observatoryPort = debuggingOptions.observatoryPort ?? 0;
       args.add('--observatory-port=$observatoryPort');
     }
 
@@ -693,7 +692,7 @@
 
   @override
   Future<int> forward(int devicePort, {int hostPort}) async {
-    if ((hostPort == null) || (hostPort == 0)) {
+    if (hostPort == null || hostPort == 0) {
       hostPort = devicePort;
     }
     assert(devicePort == hostPort);
diff --git a/packages/flutter_tools/lib/src/protocol_discovery.dart b/packages/flutter_tools/lib/src/protocol_discovery.dart
index a1f849d..f1e62ed 100644
--- a/packages/flutter_tools/lib/src/protocol_discovery.dart
+++ b/packages/flutter_tools/lib/src/protocol_discovery.dart
@@ -4,9 +4,7 @@
 
 import 'dart:async';
 
-import 'base/common.dart';
 import 'base/io.dart';
-import 'base/port_scanner.dart';
 import 'device.dart';
 import 'globals.dart';
 
@@ -18,10 +16,8 @@
     this.serviceName, {
     this.portForwarder,
     this.hostPort,
-    this.defaultHostPort,
     this.ipv6,
   }) : assert(logReader != null),
-       assert(portForwarder == null || defaultHostPort != null),
        _prefix = '$serviceName listening on ' {
     _deviceLogSubscription = logReader.logLines.listen(_handleLine);
   }
@@ -37,7 +33,6 @@
       logReader, kObservatoryService,
       portForwarder: portForwarder,
       hostPort: hostPort,
-      defaultHostPort: kDefaultObservatoryPort,
       ipv6: ipv6,
     );
   }
@@ -46,7 +41,6 @@
   final String serviceName;
   final DevicePortForwarder portForwarder;
   final int hostPort;
-  final int defaultHostPort;
   final bool ipv6;
 
   final String _prefix;
@@ -88,16 +82,15 @@
     Uri hostUri = deviceUri;
 
     if (portForwarder != null) {
-      final int devicePort = deviceUri.port;
-      int hostPort = this.hostPort ?? await portScanner.findPreferredPort(defaultHostPort);
-      hostPort = await portForwarder.forward(devicePort, hostPort: hostPort);
-      printTrace('Forwarded host port $hostPort to device port $devicePort for $serviceName');
-      hostUri = deviceUri.replace(port: hostPort);
+      final int actualDevicePort = deviceUri.port;
+      final int actualHostPort = await portForwarder.forward(actualDevicePort, hostPort: hostPort);
+      printTrace('Forwarded host port $actualHostPort to device port $actualDevicePort for $serviceName');
+      hostUri = deviceUri.replace(port: actualHostPort);
     }
 
     assert(new InternetAddress(hostUri.host).isLoopback);
     if (ipv6) {
-      hostUri = hostUri.replace(host: InternetAddress.LOOPBACK_IP_V6.host); // ignore: deprecated_member_use
+      hostUri = hostUri.replace(host: InternetAddress.loopbackIPv6.host);
     }
 
     return hostUri;
diff --git a/packages/flutter_tools/lib/src/tester/flutter_tester.dart b/packages/flutter_tools/lib/src/tester/flutter_tester.dart
index 53f67fa..2d29a31 100644
--- a/packages/flutter_tools/lib/src/tester/flutter_tester.dart
+++ b/packages/flutter_tools/lib/src/tester/flutter_tester.dart
@@ -41,13 +41,10 @@
 
 // TODO(scheglov): This device does not currently work with full restarts.
 class FlutterTesterDevice extends Device {
-  final _FlutterTesterDeviceLogReader _logReader =
-      new _FlutterTesterDeviceLogReader();
+  FlutterTesterDevice(String deviceId) : super(deviceId);
 
   Process _process;
 
-  FlutterTesterDevice(String deviceId) : super(deviceId);
-
   @override
   Future<bool> get isLocalEmulator async => false;
 
@@ -69,6 +66,9 @@
   @override
   void clearLogs() {}
 
+  final _FlutterTesterDeviceLogReader _logReader =
+      new _FlutterTesterDeviceLogReader();
+
   @override
   DeviceLogReader getLogReader({ApplicationPackage app}) => _logReader;
 
@@ -118,12 +118,10 @@
       '--packages=${PackageMap.globalPackagesPath}',
     ];
     if (debuggingOptions.debuggingEnabled) {
-      if (debuggingOptions.startPaused) {
+      if (debuggingOptions.startPaused)
         command.add('--start-paused');
-      }
       if (debuggingOptions.hasObservatoryPort)
-        command
-            .add('--observatory-port=${debuggingOptions.hasObservatoryPort}');
+        command.add('--observatory-port=${debuggingOptions.observatoryPort}');
     }
 
     // Build assets and perform initial compilation.
@@ -170,9 +168,10 @@
       if (!debuggingOptions.debuggingEnabled)
         return new LaunchResult.succeeded();
 
-      final ProtocolDiscovery observatoryDiscovery =
-          new ProtocolDiscovery.observatory(getLogReader(),
-              hostPort: debuggingOptions.observatoryPort);
+      final ProtocolDiscovery observatoryDiscovery = new ProtocolDiscovery.observatory(
+        getLogReader(),
+        hostPort: debuggingOptions.observatoryPort,
+      );
 
       final Uri observatoryUri = await observatoryDiscovery.uri;
       return new LaunchResult.succeeded(observatoryUri: observatoryUri);
@@ -186,7 +185,6 @@
   Future<bool> stopApp(ApplicationPackage app) async {
     _process?.kill();
     _process = null;
-
     return true;
   }
 
@@ -195,6 +193,8 @@
 }
 
 class FlutterTesterDevices extends PollingDeviceDiscovery {
+  FlutterTesterDevices() : super('Flutter tester');
+
   static const String kTesterDeviceId = 'flutter-tester';
 
   static bool showFlutterTesterDevice = false;
@@ -202,8 +202,6 @@
   final FlutterTesterDevice _testerDevice =
       new FlutterTesterDevice(kTesterDeviceId);
 
-  FlutterTesterDevices() : super('Flutter tester');
-
   @override
   bool get canListAnything => true;
 
diff --git a/packages/flutter_tools/test/base/build_test.dart b/packages/flutter_tools/test/base/build_test.dart
index e4ba850..6c5ae23 100644
--- a/packages/flutter_tools/test/base/build_test.dart
+++ b/packages/flutter_tools/test/base/build_test.dart
@@ -381,7 +381,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'snapshot_assembly.S')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
@@ -427,7 +427,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'snapshot_assembly.S')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
@@ -474,7 +474,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'vm_snapshot_data')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
@@ -525,7 +525,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'vm_snapshot_data')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
@@ -571,7 +571,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'snapshot_assembly.S')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
@@ -617,7 +617,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'snapshot_assembly.S')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
@@ -683,7 +683,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'vm_snapshot_data')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
@@ -734,7 +734,7 @@
         fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'vm_snapshot_data')} : ',
       };
 
-      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
+      final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
       when(xcode.cc(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
       when(xcode.clang(any)).thenAnswer((_) => new Future<RunResult>.value(successResult));
 
diff --git a/packages/flutter_tools/test/integration/flutter_tester_test.dart b/packages/flutter_tools/test/integration/flutter_tester_test.dart
index 1717000..b4a06e7 100644
--- a/packages/flutter_tools/test/integration/flutter_tester_test.dart
+++ b/packages/flutter_tools/test/integration/flutter_tester_test.dart
@@ -42,10 +42,13 @@
     });
 
     Future<LaunchResult> start(String mainPath) async {
-      return await device.startApp(null,
-          mainPath: mainPath,
-          debuggingOptions: new DebuggingOptions.enabled(
-              const BuildInfo(BuildMode.debug, null)));
+      return await device.startApp(
+        null,
+        mainPath: mainPath,
+        debuggingOptions: new DebuggingOptions.enabled(
+          const BuildInfo(BuildMode.debug, null),
+        ),
+      );
     }
 
     testUsingContext('start', () async {
diff --git a/packages/flutter_tools/test/protocol_discovery_test.dart b/packages/flutter_tools/test/protocol_discovery_test.dart
index 02d047e..b022100 100644
--- a/packages/flutter_tools/test/protocol_discovery_test.dart
+++ b/packages/flutter_tools/test/protocol_discovery_test.dart
@@ -115,16 +115,16 @@
       testUsingContext('default port', () async {
         final MockDeviceLogReader logReader = new MockDeviceLogReader();
         final ProtocolDiscovery discoverer = new ProtocolDiscovery.observatory(
-            logReader,
-            portForwarder: new MockPortForwarder(99),
-            hostPort: 54777);
+          logReader,
+          portForwarder: new MockPortForwarder(99),
+        );
 
         // Get next port future.
         final Future<Uri> nextUri = discoverer.uri;
         logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
         final Uri uri = await nextUri;
-        expect(uri.port, 54777);
-        expect('$uri', 'http://127.0.0.1:54777/PTwjm8Ii8qg=/');
+        expect(uri.port, 99);
+        expect('$uri', 'http://127.0.0.1:99/PTwjm8Ii8qg=/');
 
         discoverer.cancel();
         logReader.dispose();
@@ -133,9 +133,10 @@
       testUsingContext('specified port', () async {
         final MockDeviceLogReader logReader = new MockDeviceLogReader();
         final ProtocolDiscovery discoverer = new ProtocolDiscovery.observatory(
-            logReader,
-            portForwarder: new MockPortForwarder(99),
-            hostPort: 1243);
+          logReader,
+          portForwarder: new MockPortForwarder(99),
+          hostPort: 1243,
+        );
 
         // Get next port future.
         final Future<Uri> nextUri = discoverer.uri;
@@ -148,13 +149,33 @@
         logReader.dispose();
       });
 
+      testUsingContext('specified port zero', () async {
+        final MockDeviceLogReader logReader = new MockDeviceLogReader();
+        final ProtocolDiscovery discoverer = new ProtocolDiscovery.observatory(
+          logReader,
+          portForwarder: new MockPortForwarder(99),
+          hostPort: 0,
+        );
+
+        // Get next port future.
+        final Future<Uri> nextUri = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+        final Uri uri = await nextUri;
+        expect(uri.port, 99);
+        expect('$uri', 'http://127.0.0.1:99/PTwjm8Ii8qg=/');
+
+        discoverer.cancel();
+        logReader.dispose();
+      });
+
       testUsingContext('ipv6', () async {
         final MockDeviceLogReader logReader = new MockDeviceLogReader();
         final ProtocolDiscovery discoverer = new ProtocolDiscovery.observatory(
-            logReader,
-            portForwarder: new MockPortForwarder(99),
-            hostPort: 54777,
-            ipv6: true);
+          logReader,
+          portForwarder: new MockPortForwarder(99),
+          hostPort: 54777,
+          ipv6: true,
+        );
 
         // Get next port future.
         final Future<Uri> nextUri = discoverer.uri;
@@ -175,7 +196,12 @@
   MockPortForwarder([this.availablePort]);
 
   @override
-  Future<int> forward(int devicePort, {int hostPort}) async => hostPort ?? availablePort;
+  Future<int> forward(int devicePort, {int hostPort}) async {
+    hostPort ??= 0;
+    if (hostPort == 0)
+      return availablePort;
+    return hostPort;
+  }
 
   @override
   List<ForwardedPort> get forwardedPorts => throw 'not implemented';
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 32a5e23..e1a48ed 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -12,7 +12,6 @@
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/os.dart';
-import 'package:flutter_tools/src/base/port_scanner.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/context_runner.dart';
 import 'package:flutter_tools/src/device.dart';
@@ -77,7 +76,6 @@
           },
           Logger: () => new BufferLogger(),
           OperatingSystemUtils: () => new MockOperatingSystemUtils(),
-          PortScanner: () => new MockPortScanner(),
           SimControl: () => new MockSimControl(),
           Usage: () => new MockUsage(),
           XcodeProjectInterpreter: () => new MockXcodeProjectInterpreter(),
@@ -127,16 +125,6 @@
   }
 }
 
-class MockPortScanner extends PortScanner {
-  static int _nextAvailablePort = 12345;
-
-  @override
-  Future<bool> isPortAvailable(int port) async => true;
-
-  @override
-  Future<int> findAvailablePort() async => _nextAvailablePort++;
-}
-
 class MockDeviceManager implements DeviceManager {
   List<Device> devices = <Device>[];
 
diff --git a/packages/flutter_tools/test/vmservice_test.dart b/packages/flutter_tools/test/vmservice_test.dart
index cc76885..34d60cf 100644
--- a/packages/flutter_tools/test/vmservice_test.dart
+++ b/packages/flutter_tools/test/vmservice_test.dart
@@ -4,7 +4,6 @@
 
 import 'package:test/test.dart';
 
-import 'package:flutter_tools/src/base/port_scanner.dart';
 import 'package:flutter_tools/src/vmservice.dart';
 
 import 'src/common.dart';
@@ -13,9 +12,8 @@
 void main() {
   group('VMService', () {
     testUsingContext('fails connection eagerly in the connect() method', () async {
-      final int port = await const HostPortScanner().findAvailablePort();
       expect(
-        VMService.connect(Uri.parse('http://localhost:$port')),
+        VMService.connect(Uri.parse('http://host.invalid:9999/')),
         throwsToolExit(),
       );
     });