From e38374be0d2c3fe0ce73eb1f22a2050aeecd38e8 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Thu, 18 Jun 2020 15:26:55 -0700 Subject: [PATCH 1/4] Add back multi server for node platform Fixes #1278 Copy the `_loopback` code from `package:multi_server_socket` but model it as a `List` instead of a single server. Remove the unnecessary arguments for handling anything other than port 0. --- pkgs/test/CHANGELOG.md | 2 + pkgs/test/lib/src/runner/node/platform.dart | 70 ++++++++++++++++++--- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md index a732b1bc3..2f9fb66ad 100644 --- a/pkgs/test/CHANGELOG.md +++ b/pkgs/test/CHANGELOG.md @@ -2,6 +2,8 @@ * Avoid a confusing stack trace when there is a problem loading a platform when using the JSON reporter and enabling debugging. +* Restore behavior of listening for both `IPv6 and `IPv4` sockets for the node + platform. ## 1.15.0 diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart index 194e14830..c575cd0fc 100644 --- a/pkgs/test/lib/src/runner/node/platform.dart +++ b/pkgs/test/lib/src/runner/node/platform.dart @@ -99,15 +99,11 @@ class NodePlatform extends PlatformPlugin /// source map for the compiled suite. Future> _loadChannel( String path, Runtime runtime, SuiteConfiguration suiteConfig) async { - ServerSocket server; - try { - server = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0); - } on SocketException { - server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); - } + final servers = await _loopback(); try { - var pair = await _spawnProcess(path, runtime, suiteConfig, server.port); + var pair = + await _spawnProcess(path, runtime, suiteConfig, servers.first.port); var process = pair.first; // Forward Node's standard IO to the print handler so it's associated with @@ -117,7 +113,7 @@ class NodePlatform extends PlatformPlugin process.stdout.transform(lineSplitter).listen(print); process.stderr.transform(lineSplitter).listen(print); - var socket = await server.first; + var socket = await Future.any(servers.map((s) => s.first)); var channel = StreamChannel(socket.cast>(), socket) .transform(StreamChannelTransformer.fromCodec(utf8)) .transform(chunksToLines) @@ -129,7 +125,7 @@ class NodePlatform extends PlatformPlugin return Pair(channel, pair.last); } catch (_) { - unawaited(server.close().catchError((_) {})); + unawaited(Future.wait(servers.map((s) => s.close().catchError((_) {})))); rethrow; } } @@ -300,3 +296,59 @@ class NodePlatform extends PlatformPlugin }); final _closeMemo = AsyncMemoizer(); } + +Future> _loopback({int remainingRetries = 5}) async { + if (!await _supportsIPv4) { + return [await ServerSocket.bind(InternetAddress.loopbackIPv6, 0)]; + } + + var v4Server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + if (!await _supportsIPv6) return [v4Server]; + + try { + // Reuse the IPv4 server's port so that if [port] is 0, both servers use + // the same ephemeral port. + var v6Server = + await ServerSocket.bind(InternetAddress.loopbackIPv6, v4Server.port); + return [v4Server, v6Server]; + } on SocketException catch (error) { + if (error.osError.errorCode != _addressInUseErrno) rethrow; + if (remainingRetries == 0) rethrow; + + // A port being available on IPv4 doesn't necessarily mean that the same + // port is available on IPv6. If it's not (which is rare in practice), + // we try again until we find one that's available on both. + unawaited(v4Server.close()); + return await _loopback(remainingRetries: remainingRetries - 1); + } +} + +/// Whether this computer supports binding to IPv6 addresses. +final Future _supportsIPv6 = () async { + try { + var socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0); + unawaited(socket.close()); + return true; + } on SocketException catch (_) { + return false; + } +}(); + +/// Whether this computer supports binding to IPv4 addresses. +final Future _supportsIPv4 = () async { + try { + var socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + unawaited(socket.close()); + return true; + } on SocketException catch (_) { + return false; + } +}(); + +/// The error code for an error caused by a port already being in use. +final int _addressInUseErrno = () { + if (Platform.isWindows) return 10048; + if (Platform.isMacOS) return 48; + assert(Platform.isLinux); + return 98; +}(); From 6fcd27fa4c6abdfe46b0410b50a43cb366f75ccd Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Thu, 18 Jun 2020 15:32:43 -0700 Subject: [PATCH 2/4] Typo --- pkgs/test/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md index 2f9fb66ad..cd4ab8c38 100644 --- a/pkgs/test/CHANGELOG.md +++ b/pkgs/test/CHANGELOG.md @@ -2,7 +2,7 @@ * Avoid a confusing stack trace when there is a problem loading a platform when using the JSON reporter and enabling debugging. -* Restore behavior of listening for both `IPv6 and `IPv4` sockets for the node +* Restore behavior of listening for both `IPv6` and `IPv4` sockets for the node platform. ## 1.15.0 From f355958cfea6ca0001f043ef6ed361aec88e0c25 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 19 Jun 2020 13:22:06 -0700 Subject: [PATCH 3/4] Merge streams instead of calling .first on each --- pkgs/test/lib/src/runner/node/platform.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart index c575cd0fc..99fd7d473 100644 --- a/pkgs/test/lib/src/runner/node/platform.dart +++ b/pkgs/test/lib/src/runner/node/platform.dart @@ -113,7 +113,7 @@ class NodePlatform extends PlatformPlugin process.stdout.transform(lineSplitter).listen(print); process.stderr.transform(lineSplitter).listen(print); - var socket = await Future.any(servers.map((s) => s.first)); + var socket = await StreamGroup.merge(servers).first; var channel = StreamChannel(socket.cast>(), socket) .transform(StreamChannelTransformer.fromCodec(utf8)) .transform(chunksToLines) From 58383da027c600f78bb619730b927359b00659ca Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 19 Jun 2020 14:41:10 -0700 Subject: [PATCH 4/4] Use finally block If there is no exception and we never close the servers they can hold the process open. I'm not sure if this fully explains the flakiness on travis... --- pkgs/test/lib/src/runner/node/platform.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart index 99fd7d473..a9601ed35 100644 --- a/pkgs/test/lib/src/runner/node/platform.dart +++ b/pkgs/test/lib/src/runner/node/platform.dart @@ -124,9 +124,8 @@ class NodePlatform extends PlatformPlugin })); return Pair(channel, pair.last); - } catch (_) { + } finally { unawaited(Future.wait(servers.map((s) => s.close().catchError((_) {})))); - rethrow; } }