From f3ce9d2fcbb0583aa153d6a872ccd3bae84711d9 Mon Sep 17 00:00:00 2001 From: Lau Ching Jun Date: Thu, 29 Feb 2024 15:10:50 -0800 Subject: [PATCH] Make daemon server work on ipv6-only machines. (#144359) Retry binding on ipv6 if binding on ipv4 failed. --- .../lib/src/commands/daemon.dart | 24 ++++- .../general.shard/commands/daemon_test.dart | 96 +++++++++++++++++++ 2 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 packages/flutter_tools/test/general.shard/commands/daemon_test.dart diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 05bac1d14ec..009f485023d 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -73,7 +73,7 @@ class DaemonCommand extends FlutterCommand { throwToolExit('Invalid port for `--listen-on-tcp-port`: $error'); } - await _DaemonServer( + await DaemonServer( port: port, logger: StdoutLogger( terminal: globals.terminal, @@ -100,12 +100,14 @@ class DaemonCommand extends FlutterCommand { } } -class _DaemonServer { - _DaemonServer({ +@visibleForTesting +class DaemonServer { + DaemonServer({ this.port, required this.logger, this.notifyingLogger, - }); + @visibleForTesting Future Function(InternetAddress address, int port) bind = ServerSocket.bind, + }) : _bind = bind; final int? port; @@ -115,8 +117,20 @@ class _DaemonServer { // Logger that sends the message to the other end of daemon connection. final NotifyingLogger? notifyingLogger; + final Future Function(InternetAddress address, int port) _bind; + Future run() async { - final ServerSocket serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, port!); + ServerSocket? serverSocket; + try { + serverSocket = await _bind(InternetAddress.loopbackIPv4, port!); + } on SocketException { + logger.printTrace('Bind on $port failed with IPv4, retrying on IPv6'); + } + + // If binding on IPv4 failed, try binding on IPv6. + // Omit try catch here, let the failure fallthrough. + serverSocket ??= await _bind(InternetAddress.loopbackIPv6, port!); + logger.printStatus('Daemon server listening on ${serverSocket.port}'); final StreamSubscription subscription = serverSocket.listen( diff --git a/packages/flutter_tools/test/general.shard/commands/daemon_test.dart b/packages/flutter_tools/test/general.shard/commands/daemon_test.dart new file mode 100644 index 00000000000..b294e6cb9fb --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/daemon_test.dart @@ -0,0 +1,96 @@ +// Copyright 2014 The Flutter 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:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/commands/daemon.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; + +void main() { + testWithoutContext('binds on ipv4 normally', () async { + final FakeServerSocket socket = FakeServerSocket(); + final BufferLogger logger = BufferLogger.test(); + + int bindCalledTimes = 0; + final List bindAddresses = []; + final List bindPorts = []; + + final DaemonServer server = DaemonServer( + port: 123, + logger: logger, + bind: (Object? address, int port) async { + bindCalledTimes++; + bindAddresses.add(address); + bindPorts.add(port); + return socket; + }, + ); + await server.run(); + expect(bindCalledTimes, 1); + expect(bindAddresses, [InternetAddress.loopbackIPv4]); + expect(bindPorts, [123]); + }); + + testWithoutContext('binds on ipv6 if ipv4 failed normally', () async { + final FakeServerSocket socket = FakeServerSocket(); + final BufferLogger logger = BufferLogger.test(); + + int bindCalledTimes = 0; + final List bindAddresses = []; + final List bindPorts = []; + + final DaemonServer server = DaemonServer( + port: 123, + logger: logger, + bind: (Object? address, int port) async { + bindCalledTimes++; + bindAddresses.add(address); + bindPorts.add(port); + if (address == InternetAddress.loopbackIPv4) { + throw const SocketException('fail'); + } + return socket; + }, + ); + await server.run(); + expect(bindCalledTimes, 2); + expect(bindAddresses, [InternetAddress.loopbackIPv4, InternetAddress.loopbackIPv6]); + expect(bindPorts, [123, 123]); + }); +} + +class FakeServerSocket extends Fake implements ServerSocket { + FakeServerSocket(); + + @override + int get port => 1; + + bool closeCalled = false; + final StreamController controller = StreamController(); + + @override + StreamSubscription listen( + void Function(Socket event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + // Close the controller immediately for testing purpose. + scheduleMicrotask(() { + controller.close(); + }); + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + Future close() async { + closeCalled = true; + return this; + } +}