diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 390297317..a11613dfa 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -4,6 +4,9 @@ is set, the expression compiler worker is started with `--[no-]deprecated-js-interop`. This requires a Dart SDK that supports the flag. +- Fix the extension backend and debug service intermittently failing to start + with "Address already in use" by letting the OS pick the port instead of + probing for one on `[::1]` first. ## 27.1.4 diff --git a/dwds/lib/src/utilities/server.dart b/dwds/lib/src/utilities/server.dart index 776f34d59..2e2f76077 100644 --- a/dwds/lib/src/utilities/server.dart +++ b/dwds/lib/src/utilities/server.dart @@ -33,28 +33,26 @@ Future findUnusedPort() async { return port; } -/// Finds unused port and binds a new http server to it. +/// Binds a new http server to [hostname]. /// -/// Retries a few times to recover from errors due to -/// another thread or process opening the same port. -/// Starts by trying to bind to [port] if specified. +/// Tries [port] first if specified, then falls back to port 0 so the OS picks a +/// port that is free for this exact bind. A port from [findUnusedPort] is only +/// known to be free on `[::1]`. It can still be held over IPv4, which makes the +/// dual-stack `[::]` bind used for `'any'` fail with "Address already in use". +/// +/// Retries a few times to recover from transient bind errors. Future startHttpServer(String hostname, {int? port}) async { - HttpServer? httpServer; - final retries = 5; - var i = 0; - var foundPort = port ?? await findUnusedPort(); - while (i < retries) { - i++; + const retries = 5; + var requestedPort = port ?? 0; + for (var attempt = 1; ; attempt++) { try { - httpServer = await HttpMultiServer.bind(hostname, foundPort); + return await HttpMultiServer.bind(hostname, requestedPort); } on SocketException { - if (i == retries) rethrow; + if (attempt == retries) rethrow; } - if (httpServer != null || i == retries) return httpServer!; - foundPort = await findUnusedPort(); + requestedPort = 0; await Future.delayed(const Duration(milliseconds: 100)); } - return httpServer!; } /// Handles [requests] using [handler]. diff --git a/dwds/test/utilities/server_test.dart b/dwds/test/utilities/server_test.dart new file mode 100644 index 000000000..2739e1c2c --- /dev/null +++ b/dwds/test/utilities/server_test.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'package:dwds/src/utilities/server.dart'; +import 'package:test/test.dart'; + +void main() { + group('startHttpServer', () { + for (final hostname in ['any', 'localhost']) { + test('binds an OS-assigned port on $hostname', () async { + final server = await startHttpServer(hostname); + addTearDown(server.close); + expect(server.port, isNot(0)); + }); + + test('falls back to another port on $hostname when the requested port is ' + 'taken', () async { + final holder = await startHttpServer(hostname); + addTearDown(holder.close); + + final server = await startHttpServer(hostname, port: holder.port); + addTearDown(server.close); + expect(server.port, isNot(0)); + expect(server.port, isNot(holder.port)); + }); + } + }); +}