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.
This commit is contained in:
Lau Ching Jun 2024-04-09 13:00:21 -07:00 committed by GitHub
parent 51e70fa16b
commit 80042124ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 897 additions and 121 deletions

View File

@ -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<DeviceLogReader>.value(getLogReader(includePastLogs: true)),
portForwarder: portForwarder,
ipv6: ipv6,
devicePort: filterDevicePort,
hostPort: expectedHostPort,
logger: logger,
);
@override
late final DevicePortForwarder? portForwarder = () {
final String? adbPath = _androidSdk.adbPath;

View File

@ -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<Uri>? 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<Uri>.value(await isolateDiscoveryProtocol.uri).asBroadcastStream();
} on Exception {
isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> 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<Uri?> mDNSDiscoveryFuture = MDnsVmServiceDiscovery.instance!.getVMServiceUriForAttach(
appId,
device,
usesIpv6: usesIpv6,
useDeviceIPAsHost: isWirelessIOSDevice,
deviceVmservicePort: devicePort,
);
Future<Uri?>? 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(
<Future<Uri?>>[mDNSDiscoveryFuture, protocolDiscoveryFuture]
);
}
// Stop the timer once we receive the first uri.
vmServiceUri = vmServiceUri.map((Uri uri) {
discoveryStatus.stop();
vmServiceUri = foundUrl == null
? null
: Stream<Uri>.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<Uri>
.fromFuture(
@ -559,8 +489,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
Future<void> _validateArguments() async { }
bool _isIOSDevice(Device device) {
return (device is IOSDevice) ||
(device is IOSSimulator) ||
return (device.platformType == PlatformType.ios) ||
(device is MacOSDesignedForIPadDevice);
}
}

View File

@ -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<String, StreamSubscription<Uri>> _vmServiceDiscoverySubscriptions = <String, StreamSubscription<Uri>>{};
Future<String> startVMServiceDiscoveryForAttach(Map<String, Object?> 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<void> stopVMServiceDiscoveryForAttach(Map<String, Object?> args) async {
final String? id = _getStringArg(args, 'id', required: true);
await _vmServiceDiscoverySubscriptions.remove(id)?.cancel();
}
}
class DevToolsDomain extends Domain {

View File

@ -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<DeviceLogReader>.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

View File

@ -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<Uri> get uris;
}
/// An implementation of [VMServiceDiscoveryForAttach] that uses log scanning
/// for the discovery.
class LogScanningVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach {
LogScanningVMServiceDiscoveryForAttach(
Future<DeviceLogReader> 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> _protocolDiscovery;
@override
Stream<Uri> get uris {
final StreamController<Uri> controller = StreamController<Uri>();
_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<Uri> get uris {
final Future<Uri?> mDNSDiscoveryFuture = MDnsVmServiceDiscovery.instance!.getVMServiceUriForAttach(
appId,
device,
usesIpv6: usesIpv6,
useDeviceIPAsHost: useDeviceIPAsHost,
deviceVmservicePort: deviceVmservicePort,
hostVmservicePort: hostVmservicePort,
);
return Stream<Uri?>.fromFuture(mDNSDiscoveryFuture).where((Uri? uri) => uri != null).cast<Uri>().asBroadcastStream();
}
}
/// An implementation of [VMServiceDiscoveryForAttach] that delegates to other
/// [VMServiceDiscoveryForAttach] instances for discovery.
class DelegateVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach {
DelegateVMServiceDiscoveryForAttach(this.delegates);
final List<VMServiceDiscoveryForAttach> delegates;
@override
Stream<Uri> get uris =>
StreamGroup.merge<Uri>(
delegates.map((VMServiceDiscoveryForAttach delegate) => delegate.uris));
}

View File

@ -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<Uri> get uris {
final Future<Uri> 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<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
rethrow;
}
})();
return Stream<Uri>.fromFuture(uriFuture).asBroadcastStream();
}
}
class FuchsiaIsolateDiscoveryProtocol {
FuchsiaIsolateDiscoveryProtocol(
this._device,

View File

@ -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(<VMServiceDiscoveryForAttach>[
mdnsVMServiceDiscoveryForAttach,
super.getVMServiceDiscoveryForAttach(
appId: appId,
fuchsiaModule: fuchsiaModule,
filterDevicePort: filterDevicePort,
expectedHostPort: expectedHostPort,
ipv6: ipv6,
logger: logger,
),
]);
} else {
return mdnsVMServiceDiscoveryForAttach;
}
}
@override
bool get supportsScreenshot {
if (isCoreDevice) {

View File

@ -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(<VMServiceDiscoveryForAttach>[
mdnsVMServiceDiscoveryForAttach,
super.getVMServiceDiscoveryForAttach(
appId: appId,
fuchsiaModule: fuchsiaModule,
filterDevicePort: filterDevicePort,
expectedHostPort: expectedHostPort,
ipv6: ipv6,
logger: logger,
),
]);
}
@override
bool get supportsScreenshot => true;

View File

@ -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(<VMServiceDiscoveryForAttach>[
mdnsVMServiceDiscoveryForAttach,
super.getVMServiceDiscoveryForAttach(
appId: appId,
fuchsiaModule: fuchsiaModule,
filterDevicePort: filterDevicePort,
expectedHostPort: expectedHostPort,
ipv6: ipv6,
logger: logger,
),
]);
}
@override
Future<LaunchResult> startApp(
ApplicationPackage? package, {

View File

@ -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<LaunchResult> 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<Uri>? _uris;
@override
Stream<Uri> get uris {
if (_uris == null) {
String? requestId;
final StreamController<Uri> controller = StreamController<Uri>();
controller.onListen = () {
connection.sendRequest('device.startVMServiceDiscoveryForAttach', <String, Object?>{
'deviceId': deviceId,
'appId': appId,
'fuchsiaModule': fuchsiaModule,
'filterDevicePort': filterDevicePort,
'ipv6': ipv6,
}).then(
(Object? response) async {
requestId = _cast<String>(response);
final Stream<Uri> vmService = connection
.listenToEvent('device.VMServiceDiscoveryForAttach.$requestId')
.asyncMap((DaemonEventData event) async {
// Forward the port.
final Uri remoteUri = Uri.parse(_cast<String>(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', <String, Object?>{'id': requestId});
}
};
_uris = controller.stream;
}
return _uris!;
}
}

View File

@ -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(<String>['attach']);
await completer.future;
await Future.wait<void>(<Future<void>>[
completer.future,
fakeLogReader.dispose(),
loggerSubscription.cancel(),
]);
@ -275,8 +276,8 @@ void main() {
processInfo: processInfo,
fileSystem: testFileSystem,
)).run(<String>['attach', '--local-engine-src-path=$localEngineSrc', '--local-engine=$localEngineDir', '--local-engine-host=$localEngineDir']);
await completer.future;
await Future.wait<void>(<Future<void>>[
completer.future,
fakeLogReader.dispose(),
loggerSubscription.cancel(),
]);
@ -331,12 +332,15 @@ void main() {
)).run(<String>['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: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
@ -396,13 +400,15 @@ void main() {
)).run(<String>['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: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
@ -467,13 +473,15 @@ void main() {
)).run(<String>['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: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
@ -542,13 +550,15 @@ void main() {
)).run(<String>['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: <Type, Generator>{
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<DeviceLogReader>.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(<VMServiceDiscoveryForAttach>[
mdnsVMServiceDiscoveryForAttach,
LogScanningVMServiceDiscoveryForAttach(
Future<DeviceLogReader>.value(getLogReader()),
portForwarder: portForwarder,
devicePort: filterDevicePort,
hostPort: expectedHostPort,
ipv6: ipv6,
logger: logger,
),
]);
} else {
return mdnsVMServiceDiscoveryForAttach;
}
}
}
class FakeMDnsClient extends Fake implements MDnsClient {

View File

@ -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<FakeDeviceLogReader>.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<FakeDeviceLogReader>.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<FakeDeviceLogReader>.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<FakeDeviceLogReader>.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<Uri> uris1;
late List<Uri> uris2;
late FakeVmServiceDiscoveryForAttach fakeDiscovery1;
late FakeVmServiceDiscoveryForAttach fakeDiscovery2;
late DelegateVMServiceDiscoveryForAttach delegateDiscovery;
setUp(() {
uris1 = <Uri>[];
uris2 = <Uri>[];
fakeDiscovery1 = FakeVmServiceDiscoveryForAttach(uris1);
fakeDiscovery2 = FakeVmServiceDiscoveryForAttach(uris2);
delegateDiscovery = DelegateVMServiceDiscoveryForAttach(<VMServiceDiscoveryForAttach>[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>[
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<int> forward(int devicePort, { int? hostPort }) async {
forwardDevicePort = devicePort;
forwardHostPort = hostPort;
return forwardReturnValue;
}
}
class FakeVmServiceDiscoveryForAttach extends Fake implements VMServiceDiscoveryForAttach {
FakeVmServiceDiscoveryForAttach(this._uris);
final List<Uri> _uris;
@override
Stream<Uri> get uris => Stream<Uri>.fromIterable(_uris);
}

View File

@ -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<Uri> uriCompleter = Completer<Uri>();
// Start listening on the stream to trigger sending the request.
discovery.uris.listen(uriCompleter.complete);
final Stream<DaemonMessage> 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'], <String, Object?>{
'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<Uri> uriCompleter = Completer<Uri>();
// Start listening on the stream to trigger sending the request.
discovery.uris.listen(uriCompleter.complete);
final Stream<DaemonMessage> 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'], <String, Object?>{
'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<Uri> fallbackUri = Stream<Uri>.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<Uri> uriCompleter = Completer<Uri>();
// Start listening on the stream to trigger sending the request.
discovery.uris.listen(uriCompleter.complete);
final Stream<DaemonMessage> 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'], <String, Object?>{
'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<Uri> fallbackUri = Stream<Uri>.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<Uri> uriFuture = discovery.uris.first;
final Stream<DaemonMessage> 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'], <String, Object?>{
'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<Uri> uriFuture = discovery.uris.first;
final Stream<DaemonMessage> 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'], <String, Object?>{
'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<TestException>()));
});
});
}
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<Uint8List> binaryForRebuilding(File file, List<FileDeltaBlock> delta) async => binary!;
}
class FakeVMServiceDiscoveryForAttach extends Fake implements VMServiceDiscoveryForAttach {
FakeVMServiceDiscoveryForAttach(this.uris);
@override
Stream<Uri> uris;
}
class TestException implements Exception {}