[ DDS ] Handle case where requested DDS port is already in use

Instead of hanging with no output to the console, a useful error message
is output to the console before shutting down the VM process.

Fixes https://github.com/dart-lang/sdk/issues/48160

Change-Id: Ib37c0cdf8b1bae6f164abff4a3b3100e944d8a3c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/228961
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart
index 457f673..2ce7fbc 100644
--- a/pkg/dartdev/lib/src/commands/run.dart
+++ b/pkg/dartdev/lib/src/commands/run.dart
@@ -333,11 +333,16 @@
         stderrSub.cancel();
         completer.complete();
       } else {
+        stderrSub.cancel();
         final error = result['error'] ?? event;
         final stacktrace = result['stacktrace'] ?? '';
-        stderrSub.cancel();
-        completer.completeError(
-            'Could not start Observatory HTTP server:\n$error\n$stacktrace\n');
+        String message = 'Could not start the VM service: ';
+        if (error.contains('Failed to create server socket')) {
+          message += '$host:$port is already in use.\n';
+        } else {
+          message += '$error\n$stacktrace\n';
+        }
+        completer.completeError(message);
       }
     });
     try {
diff --git a/pkg/dartdev/test/smoke/smoke_test.dart b/pkg/dartdev/test/smoke/smoke_test.dart
index 7b1c688..38dd049 100644
--- a/pkg/dartdev/test/smoke/smoke_test.dart
+++ b/pkg/dartdev/test/smoke/smoke_test.dart
@@ -51,6 +51,24 @@
         }
       });
 
+      test('dart run --enable-vm-service smoke.dart with used port', () async {
+        final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
+        final result = await Process.run(
+          Platform.executable,
+          [
+            'run',
+            '--enable-vm-service=${server.port}',
+            script,
+          ],
+        );
+        expect(
+          result.stderr,
+          'Could not start Observatory HTTP server: localhost:${server.port} is already in use.\n',
+        );
+        expect(result.stdout, isEmpty);
+        server.close();
+      });
+
       // This test verifies that an error isn't thrown when a valid experiment
       // is passed.
       // Experiments are lists here:
diff --git a/pkg/dds/lib/src/dds_impl.dart b/pkg/dds/lib/src/dds_impl.dart
index 1d76900..000a15b 100644
--- a/pkg/dds/lib/src/dds_impl.dart
+++ b/pkg/dds/lib/src/dds_impl.dart
@@ -152,14 +152,30 @@
     // Start the DDS server. Run in an error Zone to ensure that asynchronous
     // exceptions encountered during request handling are handled, as exceptions
     // thrown during request handling shouldn't take down the entire service.
-    _server = await runZonedGuarded(
-      () async => await io.serve(handler, host, port),
+    late String errorMessage;
+    final tmpServer = await runZonedGuarded(
+      () async {
+        try {
+          return await io.serve(handler, host, port);
+        } on SocketException catch (e) {
+          errorMessage = e.message;
+          if (e.osError != null) {
+            errorMessage += ' (${e.osError!.message})';
+          }
+          errorMessage += ': ${e.address?.host}:${e.port}';
+          return null;
+        }
+      },
       (error, stack) {
         if (shouldLogRequests) {
           print('Asynchronous error: $error\n$stack');
         }
       },
-    )!;
+    );
+    if (tmpServer == null) {
+      throw DartDevelopmentServiceException.connectionIssue(errorMessage);
+    }
+    _server = tmpServer;
 
     final tmpUri = Uri(
       scheme: 'http',