From 80042124ad461548fcd518e7d41557d59cc5405b Mon Sep 17 00:00:00 2001 From: Lau Ching Jun Date: Tue, 9 Apr 2024 13:00:21 -0700 Subject: [PATCH] Support mdns when attaching to proxied devices. (#146021) Also move the vm service discovery logic into platform-specific implementation of `Device`s. This is to avoid having platform-specific code in attach.dart. --- .../lib/src/android/android_device.dart | 21 ++ .../lib/src/commands/attach.dart | 137 +++--------- .../lib/src/commands/daemon.dart | 38 ++++ packages/flutter_tools/lib/src/device.dart | 30 +++ ...evice_vm_service_discovery_for_attach.dart | 110 ++++++++++ .../lib/src/fuchsia/fuchsia_device.dart | 43 ++++ .../flutter_tools/lib/src/ios/devices.dart | 38 ++++ .../flutter_tools/lib/src/ios/simulators.dart | 32 +++ .../lib/src/macos/macos_ipad_device.dart | 32 +++ .../lib/src/proxied_devices/devices.dart | 111 ++++++++++ .../commands.shard/hermetic/attach_test.dart | 99 +++++++-- ..._vm_service_discovery_for_attach_test.dart | 132 ++++++++++++ .../proxied_devices/proxied_devices_test.dart | 195 ++++++++++++++++++ 13 files changed, 897 insertions(+), 121 deletions(-) create mode 100644 packages/flutter_tools/lib/src/device_vm_service_discovery_for_attach.dart create mode 100644 packages/flutter_tools/test/general.shard/device_vm_service_discovery_for_attach_test.dart diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 68f98a66c5..1715f655a9 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -18,6 +18,7 @@ import '../build_info.dart'; import '../convert.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../project.dart'; import '../protocol_discovery.dart'; import 'android.dart'; @@ -800,6 +801,26 @@ class AndroidDevice extends Device { } } + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) => + LogScanningVMServiceDiscoveryForAttach( + // If it's an Android device, attaching relies on past log searching + // to find the service protocol. + Future.value(getLogReader(includePastLogs: true)), + portForwarder: portForwarder, + ipv6: ipv6, + devicePort: filterDevicePort, + hostPort: expectedHostPort, + logger: logger, + ); + @override late final DevicePortForwarder? portForwarder = () { final String? adbPath = _androidSdk.adbPath; diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 9dd507b4c2..404b116c9d 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -21,13 +21,11 @@ import '../compile.dart'; import '../daemon.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; -import '../fuchsia/fuchsia_device.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../ios/devices.dart'; -import '../ios/simulators.dart'; import '../macos/macos_ipad_device.dart'; import '../mdns_discovery.dart'; import '../project.dart'; -import '../protocol_discovery.dart'; import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; @@ -286,116 +284,48 @@ known, it can be explicitly provided to attach via the command-line, e.g. : null; Stream? vmServiceUri; - bool usesIpv6 = ipv6!; + final bool usesIpv6 = ipv6!; final String ipv6Loopback = InternetAddress.loopbackIPv6.address; final String ipv4Loopback = InternetAddress.loopbackIPv4.address; final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback; final bool isWirelessIOSDevice = (device is IOSDevice) && device.isWirelesslyConnected; if ((debugPort == null && debugUri == null) || isWirelessIOSDevice) { - if (device is FuchsiaDevice) { - final String? module = stringArg('module'); - if (module == null) { - throwToolExit("'--module' is required for attaching to a Fuchsia device"); - } - usesIpv6 = device.ipv6; - FuchsiaIsolateDiscoveryProtocol? isolateDiscoveryProtocol; - try { - isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module); - vmServiceUri = Stream.value(await isolateDiscoveryProtocol.uri).asBroadcastStream(); - } on Exception { - isolateDiscoveryProtocol?.dispose(); - final List ports = device.portForwarder.forwardedPorts.toList(); - for (final ForwardedPort port in ports) { - await device.portForwarder.unforward(port); + // The device port we expect to have the debug port be listening + final int? devicePort = debugPort ?? debugUri?.port ?? deviceVmservicePort; + + final VMServiceDiscoveryForAttach vmServiceDiscovery = device.getVMServiceDiscoveryForAttach( + appId: appId, + fuchsiaModule: stringArg('module'), + filterDevicePort: devicePort, + expectedHostPort: hostVmservicePort, + ipv6: usesIpv6, + logger: _logger, + ); + + _logger.printStatus('Waiting for a connection from Flutter on ${device.name}...'); + final Status discoveryStatus = _logger.startSpinner( + timeout: const Duration(seconds: 30), + slowWarningCallback: () { + // On iOS we rely on mDNS to find Dart VM Service. Remind the user to allow local network permissions on the device. + if (_isIOSDevice(device)) { + return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n\n' + 'Click "Allow" to the prompt on your device asking if you would like to find and connect devices on your local network. ' + 'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. ' + "If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n"; } - rethrow; - } - } else if (_isIOSDevice(device)) { - // Protocol Discovery relies on logging. On iOS earlier than 13, logging is gathered using syslog. - // syslog is not available for iOS 13+. For iOS 13+, Protocol Discovery gathers logs from the VMService. - // Since we don't have access to the VMService yet, Protocol Discovery cannot be used for iOS 13+. - // Also, wireless devices must be found using mDNS and cannot use Protocol Discovery. - final bool compatibleWithProtocolDiscovery = (device is IOSDevice) && - device.majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion && - !isWirelessIOSDevice; - _logger.printStatus('Waiting for a connection from Flutter on ${device.name}...'); - final Status discoveryStatus = _logger.startSpinner( - timeout: const Duration(seconds: 30), - slowWarningCallback: () { - // If relying on mDNS to find Dart VM Service, remind the user to allow local network permissions. - if (!compatibleWithProtocolDiscovery) { - return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n\n' - 'Click "Allow" to the prompt asking if you would like to find and connect devices on your local network. ' - 'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. ' - "If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n"; - } + return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n'; + }, + ); - return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n'; - }, - ); + vmServiceUri = vmServiceDiscovery.uris; - int? devicePort; - if (debugPort != null) { - devicePort = debugPort; - } else if (debugUri != null) { - devicePort = debugUri?.port; - } else if (deviceVmservicePort != null) { - devicePort = deviceVmservicePort; - } - - final Future mDNSDiscoveryFuture = MDnsVmServiceDiscovery.instance!.getVMServiceUriForAttach( - appId, - device, - usesIpv6: usesIpv6, - useDeviceIPAsHost: isWirelessIOSDevice, - deviceVmservicePort: devicePort, - ); - - Future? protocolDiscoveryFuture; - if (compatibleWithProtocolDiscovery) { - final ProtocolDiscovery vmServiceDiscovery = ProtocolDiscovery.vmService( - device.getLogReader(), - portForwarder: device.portForwarder, - ipv6: ipv6!, - devicePort: devicePort, - hostPort: hostVmservicePort, - logger: _logger, - ); - protocolDiscoveryFuture = vmServiceDiscovery.uri; - } - - final Uri? foundUrl; - if (protocolDiscoveryFuture == null) { - foundUrl = await mDNSDiscoveryFuture; - } else { - foundUrl = await Future.any( - >[mDNSDiscoveryFuture, protocolDiscoveryFuture] - ); - } + // Stop the timer once we receive the first uri. + vmServiceUri = vmServiceUri.map((Uri uri) { discoveryStatus.stop(); - - vmServiceUri = foundUrl == null - ? null - : Stream.value(foundUrl).asBroadcastStream(); - } - // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery. - if (vmServiceUri == null) { - final ProtocolDiscovery vmServiceDiscovery = - ProtocolDiscovery.vmService( - // If it's an Android device, attaching relies on past log searching - // to find the service protocol. - await device.getLogReader(includePastLogs: device is AndroidDevice), - portForwarder: device.portForwarder, - ipv6: ipv6!, - devicePort: deviceVmservicePort, - hostPort: hostVmservicePort, - logger: _logger, - ); - _logger.printStatus('Waiting for a connection from Flutter on ${device.name}...'); - vmServiceUri = vmServiceDiscovery.uris; - } + return uri; + }); } else { vmServiceUri = Stream .fromFuture( @@ -559,8 +489,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. Future _validateArguments() async { } bool _isIOSDevice(Device device) { - return (device is IOSDevice) || - (device is IOSSimulator) || + return (device.platformType == PlatformType.ios) || (device is MacOSDesignedForIPadDevice); } } diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index d74f0ae73c..e020b11f37 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -21,6 +21,7 @@ import '../convert.dart'; import '../daemon.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../emulator.dart'; import '../features.dart'; import '../globals.dart' as globals; @@ -1009,6 +1010,8 @@ class DeviceDomain extends Domain { registerHandler('shutdownDartDevelopmentService', shutdownDartDevelopmentService); registerHandler('setExternalDevToolsUriForDartDevelopmentService', setExternalDevToolsUriForDartDevelopmentService); registerHandler('getDiagnostics', getDiagnostics); + registerHandler('startVMServiceDiscoveryForAttach', startVMServiceDiscoveryForAttach); + registerHandler('stopVMServiceDiscoveryForAttach', stopVMServiceDiscoveryForAttach); // Use the device manager discovery so that client provided device types // are usable via the daemon protocol. @@ -1325,6 +1328,41 @@ class DeviceDomain extends Domain { ...diagnostics, ]; } + + final Map> _vmServiceDiscoverySubscriptions = >{}; + + Future startVMServiceDiscoveryForAttach(Map args) async { + final String? deviceId = _getStringArg(args, 'deviceId', required: true); + final String? appId = _getStringArg(args, 'appId'); + final String? fuchsiaModule = _getStringArg(args, 'fuchsiaModule'); + final int? filterDevicePort = _getIntArg(args, 'filterDevicePort'); + final bool? ipv6 = _getBoolArg(args, 'ipv6'); + + final Device? device = await daemon.deviceDomain._getDevice(deviceId); + if (device == null) { + throw DaemonException("device '$deviceId' not found"); + } + + final String id = '${_id++}'; + + final VMServiceDiscoveryForAttach discovery = device.getVMServiceDiscoveryForAttach( + appId: appId, + fuchsiaModule: fuchsiaModule, + filterDevicePort: filterDevicePort, + ipv6: ipv6 ?? false, + logger: globals.logger + ); + _vmServiceDiscoverySubscriptions[id] = discovery.uris.listen( + (Uri uri) => sendEvent('device.VMServiceDiscoveryForAttach.$id', uri.toString()), + ); + + return id; + } + + Future stopVMServiceDiscoveryForAttach(Map args) async { + final String? id = _getStringArg(args, 'id', required: true); + await _vmServiceDiscoverySubscriptions.remove(id)?.cancel(); + } } class DevToolsDomain extends Domain { diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index c3469bb830..96006cdf4a 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -16,6 +16,7 @@ import 'base/utils.dart'; import 'build_info.dart'; import 'devfs.dart'; import 'device_port_forwarder.dart'; +import 'device_vm_service_discovery_for_attach.dart'; import 'project.dart'; import 'vmservice.dart'; import 'web/compile.dart'; @@ -737,6 +738,35 @@ abstract class Device { /// Clear the device's logs. void clearLogs(); + /// Get the [VMServiceDiscoveryForAttach] instance for this device, which + /// discovers, and forwards any necessary ports to the vm service uri of a + /// running app on the device. + /// + /// If `appId` is specified, on supported platforms, the service discovery + /// will only return the VM service URI from the given app. + /// + /// If `fuchsiaModule` is specified, this will only return the VM service uri + /// from the specified Fuchsia module. + /// + /// If `filterDevicePort` is specified, this will only return the VM service + /// uri that matches the given port on the device. + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) => + LogScanningVMServiceDiscoveryForAttach( + Future.value(getLogReader()), + portForwarder: portForwarder, + devicePort: filterDevicePort, + hostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + ); + /// Start an app package on the current device. /// /// [platformArgs] allows callers to pass platform-specific arguments to the diff --git a/packages/flutter_tools/lib/src/device_vm_service_discovery_for_attach.dart b/packages/flutter_tools/lib/src/device_vm_service_discovery_for_attach.dart new file mode 100644 index 0000000000..5b4920b377 --- /dev/null +++ b/packages/flutter_tools/lib/src/device_vm_service_discovery_for_attach.dart @@ -0,0 +1,110 @@ +// 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:async/async.dart'; + +import 'base/logger.dart'; +import 'device.dart'; +import 'device_port_forwarder.dart'; +import 'mdns_discovery.dart'; +import 'protocol_discovery.dart'; + +/// Discovers the VM service uri on a device, and forwards the port to the host. +/// +/// This is mainly used during a `flutter attach`. +abstract class VMServiceDiscoveryForAttach { + VMServiceDiscoveryForAttach(); + + /// The discovered VM service URis. + /// + /// Port forwarding is only attempted when this is invoked, for each VM + /// Service URI in the stream. + Stream get uris; +} + +/// An implementation of [VMServiceDiscoveryForAttach] that uses log scanning +/// for the discovery. +class LogScanningVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach { + LogScanningVMServiceDiscoveryForAttach( + Future logReader, { + DevicePortForwarder? portForwarder, + int? hostPort, + int? devicePort, + required bool ipv6, + required Logger logger, + }) { + _protocolDiscovery = (() async => ProtocolDiscovery.vmService( + await logReader, + portForwarder: portForwarder, + ipv6: ipv6, + devicePort: devicePort, + hostPort: hostPort, + logger: logger, + ))(); + } + + late final Future _protocolDiscovery; + + @override + Stream get uris { + final StreamController controller = StreamController(); + _protocolDiscovery.then( + (ProtocolDiscovery protocolDiscovery) async { + await controller.addStream(protocolDiscovery.uris); + await controller.close(); + }, + onError: (Object error) => controller.addError(error), + ); + return controller.stream; + } +} + +/// An implementation of [VMServiceDiscoveryForAttach] that uses mdns for the +/// discovery. +class MdnsVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach { + MdnsVMServiceDiscoveryForAttach({ + required this.device, + this.appId, + required this.usesIpv6, + required this.useDeviceIPAsHost, + this.deviceVmservicePort, + this.hostVmservicePort, + }); + + final Device device; + final String? appId; + final bool usesIpv6; + final bool useDeviceIPAsHost; + final int? deviceVmservicePort; + final int? hostVmservicePort; + + @override + Stream get uris { + final Future mDNSDiscoveryFuture = MDnsVmServiceDiscovery.instance!.getVMServiceUriForAttach( + appId, + device, + usesIpv6: usesIpv6, + useDeviceIPAsHost: useDeviceIPAsHost, + deviceVmservicePort: deviceVmservicePort, + hostVmservicePort: hostVmservicePort, + ); + + return Stream.fromFuture(mDNSDiscoveryFuture).where((Uri? uri) => uri != null).cast().asBroadcastStream(); + } +} + +/// An implementation of [VMServiceDiscoveryForAttach] that delegates to other +/// [VMServiceDiscoveryForAttach] instances for discovery. +class DelegateVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach { + DelegateVMServiceDiscoveryForAttach(this.delegates); + + final List delegates; + + @override + Stream get uris => + StreamGroup.merge( + delegates.map((VMServiceDiscoveryForAttach delegate) => delegate.uris)); +} diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart index cddf8bdb9a..5da81c7cc2 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart @@ -21,6 +21,7 @@ import '../base/time.dart'; import '../build_info.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../globals.dart' as globals; import '../project.dart'; import '../runner/flutter_command.dart'; @@ -595,6 +596,24 @@ class FuchsiaDevice extends Device { @override void clearLogs() {} + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) { + if (fuchsiaModule == null) { + throwToolExit("'--module' is required for attaching to a Fuchsia device"); + } + if (expectedHostPort != null) { + throwToolExit("'--host-vmservice-port' is not supported when attaching to a Fuchsia device"); + } + return FuchsiaIsolateVMServiceDiscoveryForAttach(getIsolateDiscoveryProtocol(fuchsiaModule)); + } + /// [true] if the current host address is IPv6. late final bool ipv6 = isIPv6Address(id); @@ -739,6 +758,30 @@ class FuchsiaDevice extends Device { } } +class FuchsiaIsolateVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach { + FuchsiaIsolateVMServiceDiscoveryForAttach(this.isolateDiscoveryProtocol); + final FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol; + + @override + Stream get uris { + final Future uriFuture = (() async { + // Wrapping the call in an anonymous async function for easier error handling. + try { + return await isolateDiscoveryProtocol.uri; + } on Exception { + final FuchsiaDevice device = isolateDiscoveryProtocol._device; + isolateDiscoveryProtocol.dispose(); + final List ports = device.portForwarder.forwardedPorts.toList(); + for (final ForwardedPort port in ports) { + await device.portForwarder.unforward(port); + } + rethrow; + } + })(); + return Stream.fromFuture(uriFuture).asBroadcastStream(); + } +} + class FuchsiaIsolateDiscoveryProtocol { FuchsiaIsolateDiscoveryProtocol( this._device, diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index bdf55e15b8..ad462a76ef 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -22,6 +22,7 @@ import '../build_info.dart'; import '../convert.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../globals.dart' as globals; import '../macos/xcdevice.dart'; import '../mdns_discovery.dart'; @@ -1026,6 +1027,43 @@ class IOSDevice extends Device { @override void clearLogs() { } + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) { + final bool compatibleWithProtocolDiscovery = majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion && + !isWirelesslyConnected; + final MdnsVMServiceDiscoveryForAttach mdnsVMServiceDiscoveryForAttach = MdnsVMServiceDiscoveryForAttach( + device: this, + appId: appId, + deviceVmservicePort: filterDevicePort, + hostVmservicePort: expectedHostPort, + usesIpv6: ipv6, + useDeviceIPAsHost: isWirelesslyConnected, + ); + + if (compatibleWithProtocolDiscovery) { + return DelegateVMServiceDiscoveryForAttach([ + mdnsVMServiceDiscoveryForAttach, + super.getVMServiceDiscoveryForAttach( + appId: appId, + fuchsiaModule: fuchsiaModule, + filterDevicePort: filterDevicePort, + expectedHostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + ), + ]); + } else { + return mdnsVMServiceDiscoveryForAttach; + } + } + @override bool get supportsScreenshot { if (isCoreDevice) { diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 2ee83a3674..9a1d0cf4e3 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -21,6 +21,7 @@ import '../convert.dart'; import '../devfs.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../globals.dart' as globals; import '../macos/xcode.dart'; import '../project.dart'; @@ -654,6 +655,37 @@ class IOSSimulator extends Device { } } + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) { + final MdnsVMServiceDiscoveryForAttach mdnsVMServiceDiscoveryForAttach = MdnsVMServiceDiscoveryForAttach( + device: this, + appId: appId, + deviceVmservicePort: filterDevicePort, + hostVmservicePort: expectedHostPort, + usesIpv6: ipv6, + useDeviceIPAsHost: false, + ); + + return DelegateVMServiceDiscoveryForAttach([ + mdnsVMServiceDiscoveryForAttach, + super.getVMServiceDiscoveryForAttach( + appId: appId, + fuchsiaModule: fuchsiaModule, + filterDevicePort: filterDevicePort, + expectedHostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + ), + ]); + } + @override bool get supportsScreenshot => true; diff --git a/packages/flutter_tools/lib/src/macos/macos_ipad_device.dart b/packages/flutter_tools/lib/src/macos/macos_ipad_device.dart index cb34310d09..e61cc88dae 100644 --- a/packages/flutter_tools/lib/src/macos/macos_ipad_device.dart +++ b/packages/flutter_tools/lib/src/macos/macos_ipad_device.dart @@ -14,6 +14,7 @@ import '../base/platform.dart'; import '../build_info.dart'; import '../desktop_device.dart'; import '../device.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../ios/ios_workflow.dart'; import '../project.dart'; @@ -59,6 +60,37 @@ class MacOSDesignedForIPadDevice extends DesktopDevice { @override String? executablePathForDevice(ApplicationPackage package, BuildInfo buildInfo) => null; + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) { + final MdnsVMServiceDiscoveryForAttach mdnsVMServiceDiscoveryForAttach = MdnsVMServiceDiscoveryForAttach( + device: this, + appId: appId, + deviceVmservicePort: filterDevicePort, + hostVmservicePort: expectedHostPort, + usesIpv6: ipv6, + useDeviceIPAsHost: false, + ); + + return DelegateVMServiceDiscoveryForAttach([ + mdnsVMServiceDiscoveryForAttach, + super.getVMServiceDiscoveryForAttach( + appId: appId, + fuchsiaModule: fuchsiaModule, + filterDevicePort: filterDevicePort, + expectedHostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + ), + ]); + } + @override Future startApp( ApplicationPackage? package, { diff --git a/packages/flutter_tools/lib/src/proxied_devices/devices.dart b/packages/flutter_tools/lib/src/proxied_devices/devices.dart index 25f2cf4608..4b4a9403f4 100644 --- a/packages/flutter_tools/lib/src/proxied_devices/devices.dart +++ b/packages/flutter_tools/lib/src/proxied_devices/devices.dart @@ -17,6 +17,7 @@ import '../convert.dart'; import '../daemon.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; +import '../device_vm_service_discovery_for_attach.dart'; import '../project.dart'; import 'debounce_data_stream.dart'; import 'file_transfer.dart'; @@ -296,6 +297,35 @@ class ProxiedDevice extends Device { @override void clearLogs() => throw UnimplementedError(); + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) => + ProxiedVMServiceDiscoveryForAttach( + connection, + id, + proxiedPortForwarder: proxiedPortForwarder, + appId: appId, + fuchsiaModule: fuchsiaModule, + filterDevicePort: filterDevicePort, + expectedHostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + fallbackDiscovery: () => super.getVMServiceDiscoveryForAttach( + appId: appId, + fuchsiaModule: fuchsiaModule, + filterDevicePort: filterDevicePort, + expectedHostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + ), + ); + @override Future startApp( PrebuiltApplicationPackage package, { @@ -863,3 +893,84 @@ class ProxiedDartDevelopmentService implements DartDevelopmentService { }); } } + +class ProxiedVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach { + ProxiedVMServiceDiscoveryForAttach( + this.connection, + this.deviceId, { + required this.proxiedPortForwarder, + required this.fallbackDiscovery, + this.appId, + this.fuchsiaModule, + this.filterDevicePort, + this.expectedHostPort, + required this.ipv6, + required this.logger, + }); + + /// [DaemonConnection] used to communicate with the daemon. + final DaemonConnection connection; + + final String deviceId; + + final String? appId; + final String? fuchsiaModule; + final int? filterDevicePort; + final int? expectedHostPort; + final bool ipv6; + final Logger logger; + + final ProxiedPortForwarder proxiedPortForwarder; + + VMServiceDiscoveryForAttach Function() fallbackDiscovery; + + Stream? _uris; + + @override + Stream get uris { + if (_uris == null) { + String? requestId; + final StreamController controller = StreamController(); + + controller.onListen = () { + connection.sendRequest('device.startVMServiceDiscoveryForAttach', { + 'deviceId': deviceId, + 'appId': appId, + 'fuchsiaModule': fuchsiaModule, + 'filterDevicePort': filterDevicePort, + 'ipv6': ipv6, + }).then( + (Object? response) async { + requestId = _cast(response); + final Stream vmService = connection + .listenToEvent('device.VMServiceDiscoveryForAttach.$requestId') + .asyncMap((DaemonEventData event) async { + // Forward the port. + final Uri remoteUri = Uri.parse(_cast(event.data)); + final int port = remoteUri.port; + final int localPort = await proxiedPortForwarder.forward(port, hostPort: expectedHostPort, ipv6: ipv6); + return remoteUri.replace(port: localPort); + }); + await controller.addStream(vmService); + }, + onError: (Object e) { + // Daemon throws string types. + if (e is String && e.contains('command not understood')) { + // Use a fallback if the daemon does not support VM service discovery. + controller.addStream(fallbackDiscovery().uris); + } else { + controller.addError(e); + } + }, + ); + }; + controller.onCancel = () { + if (requestId != null) { + connection.sendRequest('device.stopVMServiceDiscoveryForAttach', {'id': requestId}); + } + }; + _uris = controller.stream; + } + return _uris!; + } +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart index ca9c45d67a..dfd23e70cd 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart @@ -22,6 +22,7 @@ import 'package:flutter_tools/src/commands/attach.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device_port_forwarder.dart'; +import 'package:flutter_tools/src/device_vm_service_discovery_for_attach.dart'; import 'package:flutter_tools/src/ios/application_package.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/mdns_discovery.dart'; @@ -204,8 +205,8 @@ void main() { processInfo: processInfo, fileSystem: testFileSystem, )).run(['attach']); + await completer.future; await Future.wait(>[ - completer.future, fakeLogReader.dispose(), loggerSubscription.cancel(), ]); @@ -275,8 +276,8 @@ void main() { processInfo: processInfo, fileSystem: testFileSystem, )).run(['attach', '--local-engine-src-path=$localEngineSrc', '--local-engine=$localEngineDir', '--local-engine-host=$localEngineDir']); + await completer.future; await Future.wait(>[ - completer.future, fakeLogReader.dispose(), loggerSubscription.cancel(), ]); @@ -331,12 +332,15 @@ void main() { )).run(['attach']); await fakeLogReader.dispose(); - expect(portForwarder.devicePort, devicePort); - expect(portForwarder.hostPort, hostPort); - expect(hotRunnerFactory.devices, hasLength(1)); + // Listen to the URI before checking port forwarder. Port forwarding + // is done as a side effect when generating the uri. final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; expect(vmServiceUri.toString(), 'http://127.0.0.1:$hostPort/xyz/'); + + expect(portForwarder.devicePort, devicePort); + expect(portForwarder.hostPort, hostPort); + expect(hotRunnerFactory.devices, hasLength(1)); }, overrides: { FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), @@ -396,13 +400,15 @@ void main() { )).run(['attach']); await fakeLogReader.dispose(); - expect(portForwarder.devicePort, null); - expect(portForwarder.hostPort, hostPort); - expect(hotRunnerFactory.devices, hasLength(1)); - + // Listen to the URI before checking port forwarder. Port forwarding + // is done as a side effect when generating the uri. final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; expect(vmServiceUri.toString(), 'http://111.111.111.111:123/xyz/'); + + expect(portForwarder.devicePort, null); + expect(portForwarder.hostPort, hostPort); + expect(hotRunnerFactory.devices, hasLength(1)); }, overrides: { FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), @@ -467,13 +473,15 @@ void main() { )).run(['attach', '--debug-port', '123']); await fakeLogReader.dispose(); - expect(portForwarder.devicePort, null); - expect(portForwarder.hostPort, hostPort); - expect(hotRunnerFactory.devices, hasLength(1)); - + // Listen to the URI before checking port forwarder. Port forwarding + // is done as a side effect when generating the uri. final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; expect(vmServiceUri.toString(), 'http://111.111.111.111:123/xyz/'); + + expect(portForwarder.devicePort, null); + expect(portForwarder.hostPort, hostPort); + expect(hotRunnerFactory.devices, hasLength(1)); }, overrides: { FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), @@ -542,13 +550,15 @@ void main() { )).run(['attach', '--debug-url', 'https://0.0.0.0:123']); await fakeLogReader.dispose(); - expect(portForwarder.devicePort, null); - expect(portForwarder.hostPort, hostPort); - expect(hotRunnerFactory.devices, hasLength(1)); - + // Listen to the URI before checking port forwarder. Port forwarding + // is done as a side effect when generating the uri. final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; expect(vmServiceUri.toString(), 'http://111.111.111.111:123/xyz/'); + + expect(portForwarder.devicePort, null); + expect(portForwarder.hostPort, hostPort); + expect(hotRunnerFactory.devices, hasLength(1)); }, overrides: { FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), @@ -1464,6 +1474,24 @@ class FakeAndroidDevice extends Fake implements AndroidDevice { @override bool get ephemeral => true; + + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) => + LogScanningVMServiceDiscoveryForAttach( + Future.value(getLogReader()), + portForwarder: portForwarder, + devicePort: filterDevicePort, + hostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + ); } class FakeIOSDevice extends Fake implements IOSDevice { @@ -1527,6 +1555,43 @@ class FakeIOSDevice extends Fake implements IOSDevice { @override bool get ephemeral => true; + + @override + VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ + String? appId, + String? fuchsiaModule, + int? filterDevicePort, + int? expectedHostPort, + required bool ipv6, + required Logger logger, + }) { + final bool compatibleWithProtocolDiscovery = majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion && + !isWirelesslyConnected; + final MdnsVMServiceDiscoveryForAttach mdnsVMServiceDiscoveryForAttach = MdnsVMServiceDiscoveryForAttach( + device: this, + appId: appId, + deviceVmservicePort: filterDevicePort, + hostVmservicePort: expectedHostPort, + usesIpv6: ipv6, + useDeviceIPAsHost: isWirelesslyConnected, + ); + + if (compatibleWithProtocolDiscovery) { + return DelegateVMServiceDiscoveryForAttach([ + mdnsVMServiceDiscoveryForAttach, + LogScanningVMServiceDiscoveryForAttach( + Future.value(getLogReader()), + portForwarder: portForwarder, + devicePort: filterDevicePort, + hostPort: expectedHostPort, + ipv6: ipv6, + logger: logger, + ), + ]); + } else { + return mdnsVMServiceDiscoveryForAttach; + } + } } class FakeMDnsClient extends Fake implements MDnsClient { diff --git a/packages/flutter_tools/test/general.shard/device_vm_service_discovery_for_attach_test.dart b/packages/flutter_tools/test/general.shard/device_vm_service_discovery_for_attach_test.dart new file mode 100644 index 0000000000..598a660e46 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/device_vm_service_discovery_for_attach_test.dart @@ -0,0 +1,132 @@ +// 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 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/device_port_forwarder.dart'; +import 'package:flutter_tools/src/device_vm_service_discovery_for_attach.dart'; +import 'package:test/fake.dart'; + +import '../src/common.dart'; +import '../src/fake_devices.dart'; + +void main() { + group('LogScanningVMServiceDiscoveryForAttach', () { + testWithoutContext('can discover the port', () async { + final FakeDeviceLogReader logReader = FakeDeviceLogReader(); + final LogScanningVMServiceDiscoveryForAttach discovery = LogScanningVMServiceDiscoveryForAttach( + Future.value(logReader), + ipv6: false, + logger: BufferLogger.test(), + ); + + logReader.addLine('The Dart VM service is listening on http://127.0.0.1:9999'); + + expect(await discovery.uris.first, Uri.parse('http://127.0.0.1:9999')); + }); + + testWithoutContext('ignores the port that does not match devicePort', () async { + final FakeDeviceLogReader logReader = FakeDeviceLogReader(); + final LogScanningVMServiceDiscoveryForAttach discovery = LogScanningVMServiceDiscoveryForAttach( + Future.value(logReader), + devicePort: 9998, + ipv6: false, + logger: BufferLogger.test(), + ); + + logReader.addLine('The Dart VM service is listening on http://127.0.0.1:9999'); + logReader.addLine('The Dart VM service is listening on http://127.0.0.1:9998'); + + expect(await discovery.uris.first, Uri.parse('http://127.0.0.1:9998')); + }); + + testWithoutContext('forwards the port if given a port forwarder', () async { + final FakeDeviceLogReader logReader = FakeDeviceLogReader(); + final FakePortForwarder portForwarder = FakePortForwarder(9900); + final LogScanningVMServiceDiscoveryForAttach discovery = LogScanningVMServiceDiscoveryForAttach( + Future.value(logReader), + portForwarder: portForwarder, + ipv6: false, + logger: BufferLogger.test(), + ); + + logReader.addLine('The Dart VM service is listening on http://127.0.0.1:9999'); + + expect(await discovery.uris.first, Uri.parse('http://127.0.0.1:9900')); + expect(portForwarder.forwardDevicePort, 9999); + expect(portForwarder.forwardHostPort, null); + }); + + testWithoutContext('uses the host port if given', () async { + final FakeDeviceLogReader logReader = FakeDeviceLogReader(); + final FakePortForwarder portForwarder = FakePortForwarder(9900); + final LogScanningVMServiceDiscoveryForAttach discovery = LogScanningVMServiceDiscoveryForAttach( + Future.value(logReader), + portForwarder: portForwarder, + hostPort: 9901, + ipv6: false, + logger: BufferLogger.test(), + ); + + logReader.addLine('The Dart VM service is listening on http://127.0.0.1:9999'); + + expect(await discovery.uris.first, Uri.parse('http://127.0.0.1:9900')); + expect(portForwarder.forwardDevicePort, 9999); + expect(portForwarder.forwardHostPort, 9901); + }); + }); + + group('DelegateVMServiceDiscoveryForAttach', () { + late List uris1; + late List uris2; + late FakeVmServiceDiscoveryForAttach fakeDiscovery1; + late FakeVmServiceDiscoveryForAttach fakeDiscovery2; + late DelegateVMServiceDiscoveryForAttach delegateDiscovery; + + setUp(() { + uris1 = []; + uris2 = []; + fakeDiscovery1 = FakeVmServiceDiscoveryForAttach(uris1); + fakeDiscovery2 = FakeVmServiceDiscoveryForAttach(uris2); + delegateDiscovery = DelegateVMServiceDiscoveryForAttach([fakeDiscovery1, fakeDiscovery2]); + }); + + testWithoutContext('uris returns from both delegates', () async { + uris1.add(Uri.parse('http://127.0.0.1:1')); + uris1.add(Uri.parse('http://127.0.0.2:2')); + uris2.add(Uri.parse('http://127.0.0.3:3')); + uris2.add(Uri.parse('http://127.0.0.4:4')); + + expect(await delegateDiscovery.uris.toList(), unorderedEquals([ + Uri.parse('http://127.0.0.1:1'), + Uri.parse('http://127.0.0.2:2'), + Uri.parse('http://127.0.0.3:3'), + Uri.parse('http://127.0.0.4:4'), + ])); + }); + }); +} + +class FakePortForwarder extends Fake implements DevicePortForwarder { + FakePortForwarder(this.forwardReturnValue); + + int? forwardDevicePort; + int? forwardHostPort; + final int forwardReturnValue; + + @override + Future forward(int devicePort, { int? hostPort }) async { + forwardDevicePort = devicePort; + forwardHostPort = hostPort; + return forwardReturnValue; + } +} + +class FakeVmServiceDiscoveryForAttach extends Fake implements VMServiceDiscoveryForAttach { + FakeVmServiceDiscoveryForAttach(this._uris); + + final List _uris; + + @override + Stream get uris => Stream.fromIterable(_uris); +} diff --git a/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart b/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart index 4960fe97f4..4fae8997d8 100644 --- a/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart @@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/utils.dart'; import 'package:flutter_tools/src/daemon.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/device_vm_service_discovery_for_attach.dart'; import 'package:flutter_tools/src/proxied_devices/devices.dart'; import 'package:flutter_tools/src/proxied_devices/file_transfer.dart'; import 'package:test/fake.dart'; @@ -803,6 +804,187 @@ void main() { expect(localDds.shutdownCalled, true); }); }); + + group('ProxiedVMServiceDiscoveryForAttach', () { + testWithoutContext('sends the request and forwards the port', () async { + final FakeProxiedPortForwarder portForwarder = FakeProxiedPortForwarder(); + portForwarder.forwardReturnValue = 400; + final ProxiedVMServiceDiscoveryForAttach discovery = ProxiedVMServiceDiscoveryForAttach( + clientDaemonConnection, + 'test_device', + proxiedPortForwarder: portForwarder, + fallbackDiscovery: () => throw UnimplementedError(), + ipv6: false, + logger: bufferLogger, + ); + + final Completer uriCompleter = Completer(); + + // Start listening on the stream to trigger sending the request. + discovery.uris.listen(uriCompleter.complete); + + final Stream broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); + final DaemonMessage startMessage = await broadcastOutput.first; + expect(startMessage.data['id'], isNotNull); + expect(startMessage.data['method'], 'device.startVMServiceDiscoveryForAttach'); + expect(startMessage.data['params'], { + 'deviceId': 'test_device', + 'appId': null, + 'fuchsiaModule': null, + 'filterDevicePort': null, + 'ipv6': false, + }); + + serverDaemonConnection.sendResponse(startMessage.data['id']!, 'request_id'); + serverDaemonConnection.sendEvent('device.VMServiceDiscoveryForAttach.request_id', 'http://127.0.0.1:300/auth_code'); + + expect(await uriCompleter.future, Uri.parse('http://127.0.0.1:400/auth_code')); + expect(portForwarder.forwardedDevicePort, 300); + expect(portForwarder.forwardedHostPort, null); + }); + + testWithoutContext('sends additional information, and forwards the correct port', () async { + final FakeProxiedPortForwarder portForwarder = FakeProxiedPortForwarder(); + portForwarder.forwardReturnValue = 400; + final ProxiedVMServiceDiscoveryForAttach discovery = ProxiedVMServiceDiscoveryForAttach( + clientDaemonConnection, + 'test_device', + proxiedPortForwarder: portForwarder, + fallbackDiscovery: () => throw UnimplementedError(), + appId: 'test_app_id', + fuchsiaModule: 'test_fuchsia_module', + filterDevicePort: 100, + expectedHostPort: 200, + ipv6: false, + logger: bufferLogger, + ); + + final Completer uriCompleter = Completer(); + + // Start listening on the stream to trigger sending the request. + discovery.uris.listen(uriCompleter.complete); + + final Stream broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); + final DaemonMessage startMessage = await broadcastOutput.first; + expect(startMessage.data['id'], isNotNull); + expect(startMessage.data['method'], 'device.startVMServiceDiscoveryForAttach'); + expect(startMessage.data['params'], { + 'deviceId': 'test_device', + 'appId': 'test_app_id', + 'fuchsiaModule': 'test_fuchsia_module', + 'filterDevicePort': 100, + 'ipv6': false, + }); + + serverDaemonConnection.sendResponse(startMessage.data['id']!, 'request_id'); + serverDaemonConnection.sendEvent('device.VMServiceDiscoveryForAttach.request_id', 'http://127.0.0.1:300/auth_code'); + + expect(await uriCompleter.future, Uri.parse('http://127.0.0.1:400/auth_code')); + expect(portForwarder.forwardedDevicePort, 300); + expect(portForwarder.forwardedHostPort, 200); + }); + + testWithoutContext('use the fallback discovery if the remote daemon does not support proxied discovery', () async { + final FakeProxiedPortForwarder portForwarder = FakeProxiedPortForwarder(); + final Stream fallbackUri = Stream.value(Uri.parse('http://127.0.0.1:500/fallback_auth_code')); + final ProxiedVMServiceDiscoveryForAttach discovery = ProxiedVMServiceDiscoveryForAttach( + clientDaemonConnection, + 'test_device', + proxiedPortForwarder: portForwarder, + fallbackDiscovery: () => FakeVMServiceDiscoveryForAttach(fallbackUri), + ipv6: false, + logger: bufferLogger, + ); + + final Completer uriCompleter = Completer(); + + // Start listening on the stream to trigger sending the request. + discovery.uris.listen(uriCompleter.complete); + + final Stream broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); + final DaemonMessage startMessage = await broadcastOutput.first; + expect(startMessage.data['id'], isNotNull); + expect(startMessage.data['method'], 'device.startVMServiceDiscoveryForAttach'); + expect(startMessage.data['params'], { + 'deviceId': 'test_device', + 'appId': null, + 'fuchsiaModule': null, + 'filterDevicePort': null, + 'ipv6': false, + }); + serverDaemonConnection.sendErrorResponse(startMessage.data['id']!, 'command not understood: device.startDartDevelopmentService', StackTrace.current); + + expect(await uriCompleter.future, Uri.parse('http://127.0.0.1:500/fallback_auth_code')); + expect(portForwarder.forwardedDevicePort, null); + expect(portForwarder.forwardedHostPort, null); + }); + + testWithoutContext('forwards other error from the daemon', () async { + final FakeProxiedPortForwarder portForwarder = FakeProxiedPortForwarder(); + final Stream fallbackUri = Stream.value(Uri.parse('http://127.0.0.1:500/fallback_auth_code')); + final ProxiedVMServiceDiscoveryForAttach discovery = ProxiedVMServiceDiscoveryForAttach( + clientDaemonConnection, + 'test_device', + proxiedPortForwarder: portForwarder, + fallbackDiscovery: () => FakeVMServiceDiscoveryForAttach(fallbackUri), + ipv6: false, + logger: bufferLogger, + ); + + // Start listening on the stream to trigger sending the request. + final Future uriFuture = discovery.uris.first; + + final Stream broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); + final DaemonMessage startMessage = await broadcastOutput.first; + expect(startMessage.data['id'], isNotNull); + expect(startMessage.data['method'], 'device.startVMServiceDiscoveryForAttach'); + expect(startMessage.data['params'], { + 'deviceId': 'test_device', + 'appId': null, + 'fuchsiaModule': null, + 'filterDevicePort': null, + 'ipv6': false, + }); + serverDaemonConnection.sendErrorResponse(startMessage.data['id']!, 'other error', StackTrace.current); + + expect(uriFuture, throwsA('other error')); + expect(portForwarder.forwardedDevicePort, null); + expect(portForwarder.forwardedHostPort, null); + }); + + testWithoutContext('forwards the port forwarder error', () async { + final FakeProxiedPortForwarder portForwarder = FakeProxiedPortForwarder(); + portForwarder.forwardThrowException = TestException(); + final ProxiedVMServiceDiscoveryForAttach discovery = ProxiedVMServiceDiscoveryForAttach( + clientDaemonConnection, + 'test_device', + proxiedPortForwarder: portForwarder, + fallbackDiscovery: () => throw UnimplementedError(), + ipv6: false, + logger: bufferLogger, + ); + + // Start listening on the stream to trigger sending the request. + final Future uriFuture = discovery.uris.first; + + final Stream broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); + final DaemonMessage startMessage = await broadcastOutput.first; + expect(startMessage.data['id'], isNotNull); + expect(startMessage.data['method'], 'device.startVMServiceDiscoveryForAttach'); + expect(startMessage.data['params'], { + 'deviceId': 'test_device', + 'appId': null, + 'fuchsiaModule': null, + 'filterDevicePort': null, + 'ipv6': false, + }); + + serverDaemonConnection.sendResponse(startMessage.data['id']!, 'request_id'); + serverDaemonConnection.sendEvent('device.VMServiceDiscoveryForAttach.request_id', 'http://127.0.0.1:300/auth_code'); + + expect(uriFuture, throwsA(isA())); + }); + }); } class FakeDaemonStreams implements DaemonStreams { @@ -934,6 +1116,7 @@ class FakeProxiedPortForwarder extends Fake implements ProxiedPortForwarder { int? originalRemotePortReturnValue; int? receivedLocalForwardedPort; + Exception? forwardThrowException; int? forwardReturnValue; int? forwardedDevicePort; int? forwardedHostPort; @@ -950,6 +1133,9 @@ class FakeProxiedPortForwarder extends Fake implements ProxiedPortForwarder { forwardedDevicePort = devicePort; forwardedHostPort = hostPort; forwardedIpv6 = ipv6; + if (forwardThrowException != null) { + throw forwardThrowException!; + } return forwardReturnValue!; } } @@ -999,3 +1185,12 @@ class FakeFileTransfer extends Fake implements FileTransfer { @override Future binaryForRebuilding(File file, List delta) async => binary!; } + +class FakeVMServiceDiscoveryForAttach extends Fake implements VMServiceDiscoveryForAttach { + FakeVMServiceDiscoveryForAttach(this.uris); + + @override + Stream uris; +} + +class TestException implements Exception {}