Unverified Commit a07e8a6a authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

[reland] Support wireless debugging (#118895)

* Reland "Support iOS wireless debugging (#118104)"

This reverts commit cbf2e168.

* Remove device loading status
parent bf72f5eb
...@@ -24,6 +24,7 @@ import '../device.dart'; ...@@ -24,6 +24,7 @@ import '../device.dart';
import '../device_port_forwarder.dart'; import '../device_port_forwarder.dart';
import '../fuchsia/fuchsia_device.dart'; import '../fuchsia/fuchsia_device.dart';
import '../ios/devices.dart'; import '../ios/devices.dart';
import '../ios/iproxy.dart';
import '../ios/simulators.dart'; import '../ios/simulators.dart';
import '../macos/macos_ipad_device.dart'; import '../macos/macos_ipad_device.dart';
import '../mdns_discovery.dart'; import '../mdns_discovery.dart';
...@@ -229,7 +230,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. ...@@ -229,7 +230,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
} }
if (debugPort != null && debugUri != null) { if (debugPort != null && debugUri != null) {
throwToolExit( throwToolExit(
'Either --debugPort or --debugUri can be provided, not both.'); 'Either --debug-port or --debug-url can be provided, not both.');
} }
if (userIdentifier != null) { if (userIdentifier != null) {
...@@ -282,8 +283,9 @@ known, it can be explicitly provided to attach via the command-line, e.g. ...@@ -282,8 +283,9 @@ known, it can be explicitly provided to attach via the command-line, e.g.
final String ipv6Loopback = InternetAddress.loopbackIPv6.address; final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
final String ipv4Loopback = InternetAddress.loopbackIPv4.address; final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback; final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
final bool isNetworkDevice = (device is IOSDevice) && device.interfaceType == IOSDeviceConnectionInterface.network;
if (debugPort == null && debugUri == null) { if ((debugPort == null && debugUri == null) || isNetworkDevice) {
if (device is FuchsiaDevice) { if (device is FuchsiaDevice) {
final String module = stringArgDeprecated('module')!; final String module = stringArgDeprecated('module')!;
if (module == null) { if (module == null) {
...@@ -303,16 +305,73 @@ known, it can be explicitly provided to attach via the command-line, e.g. ...@@ -303,16 +305,73 @@ known, it can be explicitly provided to attach via the command-line, e.g.
rethrow; rethrow;
} }
} else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) { } else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
final Uri? uriFromMdns = // Protocol Discovery relies on logging. On iOS earlier than 13, logging is gathered using syslog.
await MDnsObservatoryDiscovery.instance!.getObservatoryUri( // 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, network devices must be found using mDNS and cannot use Protocol Discovery.
final bool compatibleWithProtocolDiscovery = (device is IOSDevice) &&
device.majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion &&
!isNetworkDevice;
_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';
},
);
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, appId,
device, device,
usesIpv6: usesIpv6, usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort, isNetworkDevice: isNetworkDevice,
deviceVmservicePort: devicePort,
); );
observatoryUri = uriFromMdns == null
Future<Uri?>? protocolDiscoveryFuture;
if (compatibleWithProtocolDiscovery) {
final ProtocolDiscovery vmServiceDiscovery = ProtocolDiscovery.observatory(
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]
);
}
discoveryStatus.stop();
observatoryUri = foundUrl == null
? null ? null
: Stream<Uri>.value(uriFromMdns).asBroadcastStream(); : Stream<Uri>.value(foundUrl).asBroadcastStream();
} }
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery. // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
if (observatoryUri == null) { if (observatoryUri == null) {
...@@ -335,7 +394,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. ...@@ -335,7 +394,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
} else { } else {
observatoryUri = Stream<Uri> observatoryUri = Stream<Uri>
.fromFuture( .fromFuture(
buildObservatoryUri( buildVMServiceUri(
device, device,
debugUri?.host ?? hostname, debugUri?.host ?? hostname,
debugPort ?? debugUri!.port, debugPort ?? debugUri!.port,
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:args/args.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart'; import 'package:package_config/package_config_types.dart';
...@@ -21,6 +22,8 @@ import '../dart/package_map.dart'; ...@@ -21,6 +22,8 @@ import '../dart/package_map.dart';
import '../device.dart'; import '../device.dart';
import '../drive/drive_service.dart'; import '../drive/drive_service.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../ios/devices.dart';
import '../ios/iproxy.dart';
import '../resident_runner.dart'; import '../resident_runner.dart';
import '../runner/flutter_command.dart' show FlutterCommandCategory, FlutterCommandResult, FlutterOptions; import '../runner/flutter_command.dart' show FlutterCommandCategory, FlutterCommandResult, FlutterOptions;
import '../web/web_device.dart'; import '../web/web_device.dart';
...@@ -203,6 +206,27 @@ class DriveCommand extends RunCommandBase { ...@@ -203,6 +206,27 @@ class DriveCommand extends RunCommandBase {
@override @override
bool get cachePubGet => false; bool get cachePubGet => false;
String? get applicationBinaryPath => stringArgDeprecated(FlutterOptions.kUseApplicationBinary);
Future<Device?> get targetedDevice async {
return findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
}
// Network devices need `publish-port` to be enabled because it requires mDNS.
// If the flag wasn't provided as an actual argument and it's a network device,
// change it to be enabled.
@override
Future<bool> get disablePortPublication async {
final ArgResults? localArgResults = argResults;
final Device? device = await targetedDevice;
final bool isNetworkDevice = device is IOSDevice && device.interfaceType == IOSDeviceConnectionInterface.network;
if (isNetworkDevice && localArgResults != null && !localArgResults.wasParsed('publish-port')) {
_logger.printTrace('Network device is being used. Changing `publish-port` to be enabled.');
return false;
}
return !boolArgDeprecated('publish-port');
}
@override @override
Future<void> validateCommand() async { Future<void> validateCommand() async {
if (userIdentifier != null) { if (userIdentifier != null) {
...@@ -223,8 +247,7 @@ class DriveCommand extends RunCommandBase { ...@@ -223,8 +247,7 @@ class DriveCommand extends RunCommandBase {
if (await _fileSystem.type(testFile) != FileSystemEntityType.file) { if (await _fileSystem.type(testFile) != FileSystemEntityType.file) {
throwToolExit('Test file not found: $testFile'); throwToolExit('Test file not found: $testFile');
} }
final String? applicationBinaryPath = stringArgDeprecated(FlutterOptions.kUseApplicationBinary); final Device? device = await targetedDevice;
final Device? device = await findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
if (device == null) { if (device == null) {
throwToolExit(null); throwToolExit(null);
} }
......
...@@ -254,7 +254,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ...@@ -254,7 +254,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
purgePersistentCache: purgePersistentCache, purgePersistentCache: purgePersistentCache,
deviceVmServicePort: deviceVmservicePort, deviceVmServicePort: deviceVmservicePort,
hostVmServicePort: hostVmservicePort, hostVmServicePort: hostVmservicePort,
disablePortPublication: disablePortPublication, disablePortPublication: await disablePortPublication,
ddsPort: ddsPort, ddsPort: ddsPort,
devToolsServerAddress: devToolsServerAddress, devToolsServerAddress: devToolsServerAddress,
verboseSystemLogs: boolArgDeprecated('verbose-system-logs'), verboseSystemLogs: boolArgDeprecated('verbose-system-logs'),
......
...@@ -275,7 +275,7 @@ Future<T> runInContext<T>( ...@@ -275,7 +275,7 @@ Future<T> runInContext<T>(
featureFlags: featureFlags, featureFlags: featureFlags,
platform: globals.platform, platform: globals.platform,
), ),
MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery( MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
logger: globals.logger, logger: globals.logger,
flutterUsage: globals.flutterUsage, flutterUsage: globals.flutterUsage,
), ),
......
...@@ -17,6 +17,7 @@ import 'base/utils.dart'; ...@@ -17,6 +17,7 @@ import 'base/utils.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'devfs.dart'; import 'devfs.dart';
import 'device_port_forwarder.dart'; import 'device_port_forwarder.dart';
import 'ios/iproxy.dart';
import 'project.dart'; import 'project.dart';
import 'vmservice.dart'; import 'vmservice.dart';
...@@ -917,7 +918,13 @@ class DebuggingOptions { ...@@ -917,7 +918,13 @@ class DebuggingOptions {
/// * https://github.com/dart-lang/sdk/blob/main/sdk/lib/html/doc/NATIVE_NULL_ASSERTIONS.md /// * https://github.com/dart-lang/sdk/blob/main/sdk/lib/html/doc/NATIVE_NULL_ASSERTIONS.md
final bool nativeNullAssertions; final bool nativeNullAssertions;
List<String> getIOSLaunchArguments(EnvironmentType environmentType, String? route, Map<String, Object?> platformArgs) { List<String> getIOSLaunchArguments(
EnvironmentType environmentType,
String? route,
Map<String, Object?> platformArgs, {
bool ipv6 = false,
IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.none
}) {
final String dartVmFlags = computeDartVmFlags(this); final String dartVmFlags = computeDartVmFlags(this);
return <String>[ return <String>[
if (enableDartProfiling) '--enable-dart-profiling', if (enableDartProfiling) '--enable-dart-profiling',
...@@ -954,6 +961,9 @@ class DebuggingOptions { ...@@ -954,6 +961,9 @@ class DebuggingOptions {
// Use the suggested host port. // Use the suggested host port.
if (environmentType == EnvironmentType.simulator && hostVmServicePort != null) if (environmentType == EnvironmentType.simulator && hostVmServicePort != null)
'--observatory-port=$hostVmServicePort', '--observatory-port=$hostVmServicePort',
// Tell the observatory to listen on all interfaces, don't restrict to the loopback.
if (interfaceType == IOSDeviceConnectionInterface.network)
'--observatory-host=${ipv6 ? '::0' : '0.0.0.0'}',
]; ];
} }
......
...@@ -9,6 +9,7 @@ import 'package:process/process.dart'; ...@@ -9,6 +9,7 @@ import 'package:process/process.dart';
import 'package:vm_service/vm_service.dart' as vm_service; import 'package:vm_service/vm_service.dart' as vm_service;
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
...@@ -21,6 +22,7 @@ import '../device.dart'; ...@@ -21,6 +22,7 @@ import '../device.dart';
import '../device_port_forwarder.dart'; import '../device_port_forwarder.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../macos/xcdevice.dart'; import '../macos/xcdevice.dart';
import '../mdns_discovery.dart';
import '../project.dart'; import '../project.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
import '../vmservice.dart'; import '../vmservice.dart';
...@@ -189,15 +191,6 @@ class IOSDevice extends Device { ...@@ -189,15 +191,6 @@ class IOSDevice extends Device {
return majorVersionString != null ? int.tryParse(majorVersionString) ?? 0 : 0; return majorVersionString != null ? int.tryParse(majorVersionString) ?? 0 : 0;
} }
@override
bool get supportsHotReload => interfaceType == IOSDeviceConnectionInterface.usb;
@override
bool get supportsHotRestart => interfaceType == IOSDeviceConnectionInterface.usb;
@override
bool get supportsFlutterExit => interfaceType == IOSDeviceConnectionInterface.usb;
@override @override
final String name; final String name;
...@@ -318,7 +311,11 @@ class IOSDevice extends Device { ...@@ -318,7 +311,11 @@ class IOSDevice extends Device {
@visibleForTesting Duration? discoveryTimeout, @visibleForTesting Duration? discoveryTimeout,
}) async { }) async {
String? packageId; String? packageId;
if (interfaceType == IOSDeviceConnectionInterface.network &&
debuggingOptions.debuggingEnabled &&
debuggingOptions.disablePortPublication) {
throwToolExit('Cannot start app on wirelessly tethered iOS device. Try running again with the --publish-port flag');
}
if (!prebuiltApplication) { if (!prebuiltApplication) {
_logger.printTrace('Building ${package.name} for $id'); _logger.printTrace('Building ${package.name} for $id');
...@@ -353,8 +350,10 @@ class IOSDevice extends Device { ...@@ -353,8 +350,10 @@ class IOSDevice extends Device {
EnvironmentType.physical, EnvironmentType.physical,
route, route,
platformArgs, platformArgs,
ipv6: ipv6,
interfaceType: interfaceType,
); );
final Status installStatus = _logger.startProgress( Status startAppStatus = _logger.startProgress(
'Installing and launching...', 'Installing and launching...',
); );
try { try {
...@@ -379,9 +378,10 @@ class IOSDevice extends Device { ...@@ -379,9 +378,10 @@ class IOSDevice extends Device {
deviceLogReader.debuggerStream = iosDeployDebugger; deviceLogReader.debuggerStream = iosDeployDebugger;
} }
} }
// Don't port foward if debugging with a network device.
observatoryDiscovery = ProtocolDiscovery.observatory( observatoryDiscovery = ProtocolDiscovery.observatory(
deviceLogReader, deviceLogReader,
portForwarder: portForwarder, portForwarder: interfaceType == IOSDeviceConnectionInterface.network ? null : portForwarder,
hostPort: debuggingOptions.hostVmServicePort, hostPort: debuggingOptions.hostVmServicePort,
devicePort: debuggingOptions.deviceVmServicePort, devicePort: debuggingOptions.deviceVmServicePort,
ipv6: ipv6, ipv6: ipv6,
...@@ -412,12 +412,59 @@ class IOSDevice extends Device { ...@@ -412,12 +412,59 @@ class IOSDevice extends Device {
return LaunchResult.succeeded(); return LaunchResult.succeeded();
} }
_logger.printTrace('Application launched on the device. Waiting for observatory url.'); _logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.');
final Timer timer = Timer(discoveryTimeout ?? const Duration(seconds: 30), () {
_logger.printError('iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...'); final int defaultTimeout = interfaceType == IOSDeviceConnectionInterface.network ? 45 : 30;
final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: defaultTimeout), () {
_logger.printError('The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...');
// If debugging with a wireless device and the timeout is reached, remind the
// user to allow local network permissions.
if (interfaceType == IOSDeviceConnectionInterface.network) {
_logger.printError(
'\nClick "Allow" to the prompt asking if you would like to find and connect devices on your local network. '
'This is required for wireless debugging. 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."
);
} else {
iosDeployDebugger?.pauseDumpBacktraceResume(); iosDeployDebugger?.pauseDumpBacktraceResume();
}
}); });
final Uri? localUri = await observatoryDiscovery?.uri;
Uri? localUri;
if (interfaceType == IOSDeviceConnectionInterface.network) {
// Wait for Dart VM Service to start up.
final Uri? serviceURL = await observatoryDiscovery?.uri;
if (serviceURL == null) {
await iosDeployDebugger?.stopAndDumpBacktrace();
return LaunchResult.failed();
}
// If Dart VM Service URL with the device IP is not found within 5 seconds,
// change the status message to prompt users to click Allow. Wait 5 seconds because it
// should only show this message if they have not already approved the permissions.
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
startAppStatus.stop();
startAppStatus = _logger.startProgress(
'Waiting for approval of local network permissions...',
);
});
// Get Dart VM Service URL with the device IP as the host.
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
deviceVmservicePort: serviceURL.port,
isNetworkDevice: true,
);
mDNSLookupTimer.cancel();
} else {
localUri = await observatoryDiscovery?.uri;
}
timer.cancel(); timer.cancel();
if (localUri == null) { if (localUri == null) {
await iosDeployDebugger?.stopAndDumpBacktrace(); await iosDeployDebugger?.stopAndDumpBacktrace();
...@@ -429,7 +476,7 @@ class IOSDevice extends Device { ...@@ -429,7 +476,7 @@ class IOSDevice extends Device {
_logger.printError(e.message); _logger.printError(e.message);
return LaunchResult.failed(); return LaunchResult.failed();
} finally { } finally {
installStatus.stop(); startAppStatus.stop();
} }
} }
...@@ -569,7 +616,6 @@ String decodeSyslog(String line) { ...@@ -569,7 +616,6 @@ String decodeSyslog(String line) {
} }
} }
@visibleForTesting
class IOSDeviceLogReader extends DeviceLogReader { class IOSDeviceLogReader extends DeviceLogReader {
IOSDeviceLogReader._( IOSDeviceLogReader._(
this._iMobileDevice, this._iMobileDevice,
......
...@@ -305,12 +305,6 @@ class XCDevice { ...@@ -305,12 +305,6 @@ class XCDevice {
final IOSDeviceConnectionInterface interface = _interfaceType(device); final IOSDeviceConnectionInterface interface = _interfaceType(device);
// Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network).
// TODO(jmagman): Remove this check once wirelessly detected devices can be observed and attached, https://github.com/flutter/flutter/issues/15072.
if (interface != IOSDeviceConnectionInterface.usb) {
continue;
}
String? sdkVersion = _sdkVersion(device); String? sdkVersion = _sdkVersion(device);
if (sdkVersion != null) { if (sdkVersion != null) {
......
...@@ -13,114 +13,278 @@ import 'build_info.dart'; ...@@ -13,114 +13,278 @@ import 'build_info.dart';
import 'device.dart'; import 'device.dart';
import 'reporting/reporting.dart'; import 'reporting/reporting.dart';
/// A wrapper around [MDnsClient] to find a Dart observatory instance. /// A wrapper around [MDnsClient] to find a Dart VM Service instance.
class MDnsObservatoryDiscovery { class MDnsVmServiceDiscovery {
/// Creates a new [MDnsObservatoryDiscovery] object. /// Creates a new [MDnsVmServiceDiscovery] object.
/// ///
/// The [_client] parameter will be defaulted to a new [MDnsClient] if null. /// The [_client] parameter will be defaulted to a new [MDnsClient] if null.
/// The [applicationId] parameter may be null, and can be used to MDnsVmServiceDiscovery({
/// automatically select which application to use if multiple are advertising
/// Dart observatory ports.
MDnsObservatoryDiscovery({
MDnsClient? mdnsClient, MDnsClient? mdnsClient,
MDnsClient? preliminaryMDnsClient,
required Logger logger, required Logger logger,
required Usage flutterUsage, required Usage flutterUsage,
}): _client = mdnsClient ?? MDnsClient(), }) : _client = mdnsClient ?? MDnsClient(),
_preliminaryClient = preliminaryMDnsClient,
_logger = logger, _logger = logger,
_flutterUsage = flutterUsage; _flutterUsage = flutterUsage;
final MDnsClient _client; final MDnsClient _client;
// Used when discovering VM services with `queryForAttach` to do a preliminary
// check for already running services so that results are not cached in _client.
final MDnsClient? _preliminaryClient;
final Logger _logger; final Logger _logger;
final Usage _flutterUsage; final Usage _flutterUsage;
@visibleForTesting @visibleForTesting
static const String dartObservatoryName = '_dartobservatory._tcp.local'; static const String dartVmServiceName = '_dartobservatory._tcp.local';
static MDnsObservatoryDiscovery? get instance => context.get<MDnsObservatoryDiscovery>(); static MDnsVmServiceDiscovery? get instance => context.get<MDnsVmServiceDiscovery>();
/// Executes an mDNS query for a Dart Observatory. /// Executes an mDNS query for Dart VM Services.
/// Checks for services that have already been launched.
/// If none are found, it will listen for new services to become active
/// and return the first it finds that match the parameters.
/// ///
/// The [applicationId] parameter may be used to specify which application /// The [applicationId] parameter may be used to specify which application
/// to find. For Android, it refers to the package name; on iOS, it refers to /// to find. For Android, it refers to the package name; on iOS, it refers to
/// the bundle ID. /// the bundle ID.
/// ///
/// If it is not null, this method will find the port and authentication code /// The [deviceVmservicePort] parameter may be used to specify which port
/// of the Dart Observatory for that application. If it cannot find a Dart /// to find.
/// Observatory matching that application identifier, it will call ///
/// [throwToolExit]. /// The [isNetworkDevice] parameter flags whether to get the device IP
/// and the [ipv6] parameter flags whether to get an iPv6 address
/// (otherwise it will get iPv4).
///
/// The [timeout] parameter determines how long to continue to wait for
/// services to become active.
/// ///
/// If it is null and there are multiple ports available, the user will be /// If [applicationId] is not null, this method will find the port and authentication code
/// prompted with a list of available observatory ports and asked to select /// of the Dart VM Service for that application. If it cannot find a service matching
/// one. /// that application identifier after the [timeout], it will call [throwToolExit].
/// ///
/// If it is null and there is only one available instance of Observatory, /// If [applicationId] is null and there are multiple Dart VM Services available,
/// it will return that instance's information regardless of what application /// the user will be prompted with a list of available services with the respective
/// the Observatory instance is for. /// app-id and device-vmservice-port to use and asked to select one.
///
/// If it is null and there is only one available or it's the first found instance
/// of Dart VM Service, it will return that instance's information regardless of
/// what application the service instance is for.
@visibleForTesting @visibleForTesting
Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async { Future<MDnsVmServiceDiscoveryResult?> queryForAttach({
_logger.printTrace('Checking for advertised Dart observatories...'); String? applicationId,
try { int? deviceVmservicePort,
await _client.start(); bool ipv6 = false,
final List<PtrResourceRecord> pointerRecords = await _client bool isNetworkDevice = false,
.lookup<PtrResourceRecord>( Duration timeout = const Duration(minutes: 10),
ResourceRecordQuery.serverPointer(dartObservatoryName), }) async {
) // Poll for 5 seconds to see if there are already services running.
.toList(); // Use a new instance of MDnsClient so results don't get cached in _client.
if (pointerRecords.isEmpty) { // If no results are found, poll for a longer duration to wait for connections.
_logger.printTrace('No pointer records found.'); // If more than 1 result is found, throw an error since it can't be determined which to pick.
return null; // If only one is found, return it.
final List<MDnsVmServiceDiscoveryResult> results = await _pollingVmService(
_preliminaryClient ?? MDnsClient(),
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
ipv6: ipv6,
isNetworkDevice: isNetworkDevice,
timeout: const Duration(seconds: 5),
);
if (results.isEmpty) {
return firstMatchingVmService(
_client,
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
ipv6: ipv6,
isNetworkDevice: isNetworkDevice,
timeout: timeout,
);
} else if (results.length > 1) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('There are multiple Dart VM Services available.');
buffer.writeln('Rerun this command with one of the following passed in as the app-id and device-vmservice-port:');
buffer.writeln();
for (final MDnsVmServiceDiscoveryResult result in results) {
buffer.writeln(
' flutter attach --app-id "${result.domainName.replaceAll('.$dartVmServiceName', '')}" --device-vmservice-port ${result.port}');
}
throwToolExit(buffer.toString());
}
return results.first;
} }
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final Set<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet();
String? domainName; /// Executes an mDNS query for Dart VM Services.
if (applicationId != null) { /// Listens for new services to become active and returns the first it finds that
for (final String name in uniqueDomainNames) { /// match the parameters.
if (name.toLowerCase().startsWith(applicationId.toLowerCase())) { ///
domainName = name; /// The [applicationId] parameter must be set to specify which application
break; /// to find. For Android, it refers to the package name; on iOS, it refers to
/// the bundle ID.
///
/// The [deviceVmservicePort] parameter must be set to specify which port
/// to find.
///
/// [applicationId] and [deviceVmservicePort] are required for launch so that
/// if multiple flutter apps are running on different devices, it will
/// only match with the device running the desired app.
///
/// The [isNetworkDevice] parameter flags whether to get the device IP
/// and the [ipv6] parameter flags whether to get an iPv6 address
/// (otherwise it will get iPv4).
///
/// The [timeout] parameter determines how long to continue to wait for
/// services to become active.
///
/// If a Dart VM Service matching the [applicationId] and [deviceVmservicePort]
/// cannot be found after the [timeout], it will call [throwToolExit].
@visibleForTesting
Future<MDnsVmServiceDiscoveryResult?> queryForLaunch({
required String applicationId,
required int deviceVmservicePort,
bool ipv6 = false,
bool isNetworkDevice = false,
Duration timeout = const Duration(minutes: 10),
}) async {
// Query for a specific application and device port.
return firstMatchingVmService(
_client,
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
ipv6: ipv6,
isNetworkDevice: isNetworkDevice,
timeout: timeout,
);
} }
/// Polls for Dart VM Services and returns the first it finds that match
/// the [applicationId]/[deviceVmservicePort] (if applicable).
/// Returns null if no results are found.
@visibleForTesting
Future<MDnsVmServiceDiscoveryResult?> firstMatchingVmService(
MDnsClient client, {
String? applicationId,
int? deviceVmservicePort,
bool ipv6 = false,
bool isNetworkDevice = false,
Duration timeout = const Duration(minutes: 10),
}) async {
final List<MDnsVmServiceDiscoveryResult> results = await _pollingVmService(
client,
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
ipv6: ipv6,
isNetworkDevice: isNetworkDevice,
timeout: timeout,
quitOnFind: true,
);
if (results.isEmpty) {
return null;
} }
if (domainName == null) { return results.first;
throwToolExit('Did not find a observatory port advertised for $applicationId.');
} }
} else if (uniqueDomainNames.length > 1) {
final StringBuffer buffer = StringBuffer(); Future<List<MDnsVmServiceDiscoveryResult>> _pollingVmService(
buffer.writeln('There are multiple observatory ports available.'); MDnsClient client, {
buffer.writeln('Rerun this command with one of the following passed in as the appId:'); String? applicationId,
buffer.writeln(); int? deviceVmservicePort,
for (final String uniqueDomainName in uniqueDomainNames) { bool ipv6 = false,
buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}'); bool isNetworkDevice = false,
required Duration timeout,
bool quitOnFind = false,
}) async {
_logger.printTrace('Checking for advertised Dart VM Services...');
try {
await client.start();
final List<MDnsVmServiceDiscoveryResult> results =
<MDnsVmServiceDiscoveryResult>[];
final Set<String> uniqueDomainNames = <String>{};
// Listen for mDNS connections until timeout.
final Stream<PtrResourceRecord> ptrResourceStream = client.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(dartVmServiceName),
timeout: timeout
);
await for (final PtrResourceRecord ptr in ptrResourceStream) {
uniqueDomainNames.add(ptr.domainName);
String? domainName;
if (applicationId != null) {
// If applicationId is set, only use records that match it
if (ptr.domainName.toLowerCase().startsWith(applicationId.toLowerCase())) {
domainName = ptr.domainName;
} else {
continue;
} }
throwToolExit(buffer.toString());
} else { } else {
domainName = pointerRecords[0].domainName; domainName = ptr.domainName;
} }
_logger.printTrace('Checking for available port on $domainName'); _logger.printTrace('Checking for available port on $domainName');
// Here, if we get more than one, it should just be a duplicate. final List<SrvResourceRecord> srvRecords = await client
final List<SrvResourceRecord> srv = await _client
.lookup<SrvResourceRecord>( .lookup<SrvResourceRecord>(
ResourceRecordQuery.service(domainName), ResourceRecordQuery.service(domainName),
) )
.toList(); .toList();
if (srv.isEmpty) { if (srvRecords.isEmpty) {
return null; continue;
}
// If more than one SrvResourceRecord found, it should just be a duplicate.
final SrvResourceRecord srvRecord = srvRecords.first;
if (srvRecords.length > 1) {
_logger.printWarning(
'Unexpectedly found more than one Dart VM Service report for $domainName '
'- using first one (${srvRecord.port}).');
}
// If deviceVmservicePort is set, only use records that match it
if (deviceVmservicePort != null && srvRecord.port != deviceVmservicePort) {
continue;
}
// Get the IP address of the service if using a network device.
InternetAddress? ipAddress;
if (isNetworkDevice) {
List<IPAddressResourceRecord> ipAddresses = await client
.lookup<IPAddressResourceRecord>(
ipv6
? ResourceRecordQuery.addressIPv6(srvRecord.target)
: ResourceRecordQuery.addressIPv4(srvRecord.target),
)
.toList();
if (ipAddresses.isEmpty) {
throwToolExit('Did not find IP for service ${srvRecord.target}.');
}
// Filter out link-local addresses.
if (ipAddresses.length > 1) {
ipAddresses = ipAddresses.where((IPAddressResourceRecord element) => !element.address.isLinkLocal).toList();
}
ipAddress = ipAddresses.first.address;
if (ipAddresses.length > 1) {
_logger.printWarning(
'Unexpectedly found more than one IP for Dart VM Service ${srvRecord.target} '
'- using first one ($ipAddress).');
} }
if (srv.length > 1) {
_logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
} }
_logger.printTrace('Checking for authentication code for $domainName'); _logger.printTrace('Checking for authentication code for $domainName');
final List<TxtResourceRecord> txt = await _client final List<TxtResourceRecord> txt = await client
.lookup<TxtResourceRecord>( .lookup<TxtResourceRecord>(
ResourceRecordQuery.text(domainName), ResourceRecordQuery.text(domainName),
) )
.toList(); .toList();
if (txt == null || txt.isEmpty) { if (txt == null || txt.isEmpty) {
return MDnsObservatoryDiscoveryResult(srv.first.port, ''); results.add(MDnsVmServiceDiscoveryResult(domainName, srvRecord.port, ''));
if (quitOnFind) {
return results;
}
continue;
} }
const String authCodePrefix = 'authCode='; const String authCodePrefix = 'authCode=';
String? raw; String? raw;
...@@ -131,43 +295,145 @@ class MDnsObservatoryDiscovery { ...@@ -131,43 +295,145 @@ class MDnsObservatoryDiscovery {
} }
} }
if (raw == null) { if (raw == null) {
return MDnsObservatoryDiscoveryResult(srv.first.port, ''); results.add(MDnsVmServiceDiscoveryResult(domainName, srvRecord.port, ''));
if (quitOnFind) {
return results;
}
continue;
} }
String authCode = raw.substring(authCodePrefix.length); String authCode = raw.substring(authCodePrefix.length);
// The Observatory currently expects a trailing '/' as part of the // The Dart VM Service currently expects a trailing '/' as part of the
// URI, otherwise an invalid authentication code response is given. // URI, otherwise an invalid authentication code response is given.
if (!authCode.endsWith('/')) { if (!authCode.endsWith('/')) {
authCode += '/'; authCode += '/';
} }
return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
results.add(MDnsVmServiceDiscoveryResult(
domainName,
srvRecord.port,
authCode,
ipAddress: ipAddress
));
if (quitOnFind) {
return results;
}
}
// If applicationId is set and quitOnFind is true and no results matching
// the applicationId were found but other results were found, throw an error.
if (applicationId != null &&
quitOnFind &&
results.isEmpty &&
uniqueDomainNames.isNotEmpty) {
String message = 'Did not find a Dart VM Service advertised for $applicationId';
if (deviceVmservicePort != null) {
message += ' on port $deviceVmservicePort';
}
throwToolExit('$message.');
}
return results;
} finally { } finally {
_client.stop(); client.stop();
} }
} }
Future<Uri?> getObservatoryUri(String? applicationId, Device device, { /// Gets Dart VM Service Uri for `flutter attach`.
/// Executes an mDNS query and waits until a Dart VM Service is found.
///
/// Differs from `getVMServiceUriForLaunch` because it can search for any available Dart VM Service.
/// Since [applicationId] and [deviceVmservicePort] are optional, it can either look for any service
/// or a specific service matching [applicationId]/[deviceVmservicePort].
/// It may find more than one service, which will throw an error listing the found services.
Future<Uri?> getVMServiceUriForAttach(
String? applicationId,
Device device, {
bool usesIpv6 = false, bool usesIpv6 = false,
int? hostVmservicePort, int? hostVmservicePort,
int? deviceVmservicePort, int? deviceVmservicePort,
bool isNetworkDevice = false,
Duration timeout = const Duration(minutes: 10),
}) async { }) async {
final MDnsObservatoryDiscoveryResult? result = await query( final MDnsVmServiceDiscoveryResult? result = await queryForAttach(
applicationId: applicationId, applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort, deviceVmservicePort: deviceVmservicePort,
ipv6: usesIpv6,
isNetworkDevice: isNetworkDevice,
timeout: timeout,
); );
return _handleResult(
result,
device,
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
hostVmservicePort: hostVmservicePort,
usesIpv6: usesIpv6,
isNetworkDevice: isNetworkDevice
);
}
/// Gets Dart VM Service Uri for `flutter run`.
/// Executes an mDNS query and waits until the Dart VM Service service is found.
///
/// Differs from `getVMServiceUriForAttach` because it only searches for a specific service.
/// This is enforced by [applicationId] and [deviceVmservicePort] being required.
Future<Uri?> getVMServiceUriForLaunch(
String applicationId,
Device device, {
bool usesIpv6 = false,
int? hostVmservicePort,
required int deviceVmservicePort,
bool isNetworkDevice = false,
Duration timeout = const Duration(minutes: 10),
}) async {
final MDnsVmServiceDiscoveryResult? result = await queryForLaunch(
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
ipv6: usesIpv6,
isNetworkDevice: isNetworkDevice,
timeout: timeout,
);
return _handleResult(
result,
device,
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
hostVmservicePort: hostVmservicePort,
usesIpv6: usesIpv6,
isNetworkDevice: isNetworkDevice
);
}
Future<Uri?> _handleResult(
MDnsVmServiceDiscoveryResult? result,
Device device, {
String? applicationId,
int? deviceVmservicePort,
int? hostVmservicePort,
bool usesIpv6 = false,
bool isNetworkDevice = false,
}) async {
if (result == null) { if (result == null) {
await _checkForIPv4LinkLocal(device); await _checkForIPv4LinkLocal(device);
return null; return null;
} }
final String host;
final String host = usesIpv6 final InternetAddress? ipAddress = result.ipAddress;
if (isNetworkDevice && ipAddress != null) {
host = ipAddress.address;
} else {
host = usesIpv6
? InternetAddress.loopbackIPv6.address ? InternetAddress.loopbackIPv6.address
: InternetAddress.loopbackIPv4.address; : InternetAddress.loopbackIPv4.address;
return buildObservatoryUri( }
return buildVMServiceUri(
device, device,
host, host,
result.port, result.port,
hostVmservicePort, hostVmservicePort,
result.authCode, result.authCode,
isNetworkDevice,
); );
} }
...@@ -236,18 +502,26 @@ class MDnsObservatoryDiscovery { ...@@ -236,18 +502,26 @@ class MDnsObservatoryDiscovery {
} }
} }
class MDnsObservatoryDiscoveryResult { class MDnsVmServiceDiscoveryResult {
MDnsObservatoryDiscoveryResult(this.port, this.authCode); MDnsVmServiceDiscoveryResult(
this.domainName,
this.port,
this.authCode, {
this.ipAddress
});
final String domainName;
final int port; final int port;
final String authCode; final String authCode;
final InternetAddress? ipAddress;
} }
Future<Uri> buildObservatoryUri( Future<Uri> buildVMServiceUri(
Device device, Device device,
String host, String host,
int devicePort, [ int devicePort, [
int? hostVmservicePort, int? hostVmservicePort,
String? authCode, String? authCode,
bool isNetworkDevice = false,
]) async { ]) async {
String path = '/'; String path = '/';
if (authCode != null) { if (authCode != null) {
...@@ -259,8 +533,16 @@ Future<Uri> buildObservatoryUri( ...@@ -259,8 +533,16 @@ Future<Uri> buildObservatoryUri(
path += '/'; path += '/';
} }
hostVmservicePort ??= 0; hostVmservicePort ??= 0;
final int? actualHostPort = hostVmservicePort == 0 ?
final int? actualHostPort;
if (isNetworkDevice) {
// When debugging with a network device, port forwarding is not required
// so just use the device's port.
actualHostPort = devicePort;
} else {
actualHostPort = hostVmservicePort == 0 ?
await device.portForwarder?.forward(devicePort) : await device.portForwarder?.forward(devicePort) :
hostVmservicePort; hostVmservicePort;
}
return Uri(scheme: 'http', host: host, port: actualHostPort, path: path); return Uri(scheme: 'http', host: host, port: actualHostPort, path: path);
} }
...@@ -569,7 +569,7 @@ abstract class FlutterCommand extends Command<void> { ...@@ -569,7 +569,7 @@ abstract class FlutterCommand extends Command<void> {
); );
} }
bool get disablePortPublication => !boolArgDeprecated('publish-port'); Future<bool> get disablePortPublication async => !boolArgDeprecated('publish-port');
void usesIpv6Flag({required bool verboseHelp}) { void usesIpv6Flag({required bool verboseHelp}) {
argParser.addFlag(ipv6Flag, argParser.addFlag(ipv6Flag,
......
...@@ -23,6 +23,7 @@ import 'package:flutter_tools/src/device.dart'; ...@@ -23,6 +23,7 @@ import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart'; import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/ios/application_package.dart'; import 'package:flutter_tools/src/ios/application_package.dart';
import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/iproxy.dart';
import 'package:flutter_tools/src/macos/macos_ipad_device.dart'; import 'package:flutter_tools/src/macos/macos_ipad_device.dart';
import 'package:flutter_tools/src/mdns_discovery.dart'; import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
...@@ -83,6 +84,7 @@ void main() { ...@@ -83,6 +84,7 @@ void main() {
group('with one device and no specified target file', () { group('with one device and no specified target file', () {
const int devicePort = 499; const int devicePort = 499;
const int hostPort = 42; const int hostPort = 42;
final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch;
late FakeDeviceLogReader fakeLogReader; late FakeDeviceLogReader fakeLogReader;
late RecordingPortForwarder portForwarder; late RecordingPortForwarder portForwarder;
...@@ -102,17 +104,17 @@ void main() { ...@@ -102,17 +104,17 @@ void main() {
fakeLogReader.dispose(); fakeLogReader.dispose();
}); });
testUsingContext('succeeds with iOS device', () async { testUsingContext('succeeds with iOS device with protocol discovery', () async {
final FakeIOSDevice device = FakeIOSDevice( final FakeIOSDevice device = FakeIOSDevice(
logReader: fakeLogReader, logReader: fakeLogReader,
portForwarder: portForwarder, portForwarder: portForwarder,
majorSdkVersion: 12,
onGetLogReader: () { onGetLogReader: () {
fakeLogReader.addLine('Foo'); fakeLogReader.addLine('Foo');
fakeLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:$devicePort'); fakeLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:$devicePort');
return fakeLogReader; return fakeLogReader;
}, },
); );
testDeviceManager.devices = <Device>[device]; testDeviceManager.devices = <Device>[device];
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
...@@ -121,7 +123,20 @@ void main() { ...@@ -121,7 +123,20 @@ void main() {
completer.complete(); completer.complete();
} }
}); });
final Future<void> task = createTestCommandRunner(AttachCommand( final FakeHotRunner hotRunner = FakeHotRunner();
hotRunner.onAttach = (
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async => 0;
hotRunner.exited = false;
hotRunner.isWaitingForObservatory = false;
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
await createTestCommandRunner(AttachCommand(
hotRunnerFactory: hotRunnerFactory,
artifacts: artifacts, artifacts: artifacts,
stdio: stdio, stdio: stdio,
logger: logger, logger: logger,
...@@ -137,15 +152,309 @@ void main() { ...@@ -137,15 +152,309 @@ void main() {
expect(portForwarder.hostPort, hostPort); expect(portForwarder.hostPort, hostPort);
await fakeLogReader.dispose(); await fakeLogReader.dispose();
await expectLoggerInterruptEndsTask(task, logger);
await loggerSubscription.cancel(); await loggerSubscription.cancel();
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger, Logger: () => logger,
DeviceManager: () => testDeviceManager, DeviceManager: () => testDeviceManager,
MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery( MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
preliminaryMDnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
logger: logger,
flutterUsage: TestUsage(),
),
});
testUsingContext('succeeds with iOS device with mDNS', () async {
final FakeIOSDevice device = FakeIOSDevice(
logReader: fakeLogReader,
portForwarder: portForwarder,
majorSdkVersion: 16,
onGetLogReader: () {
fakeLogReader.addLine('Foo');
fakeLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:$devicePort');
return fakeLogReader;
},
);
testDeviceManager.devices = <Device>[device];
final FakeHotRunner hotRunner = FakeHotRunner();
hotRunner.onAttach = (
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async => 0;
hotRunner.exited = false;
hotRunner.isWaitingForObservatory = false;
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
await createTestCommandRunner(AttachCommand(
hotRunnerFactory: hotRunnerFactory,
artifacts: artifacts,
stdio: stdio,
logger: logger,
terminal: terminal,
signals: signals,
platform: platform,
processInfo: processInfo,
fileSystem: testFileSystem,
)).run(<String>['attach']);
await fakeLogReader.dispose();
expect(portForwarder.devicePort, devicePort);
expect(portForwarder.hostPort, hostPort);
expect(hotRunnerFactory.devices, hasLength(1));
final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
expect(observatoryUri.toString(), 'http://127.0.0.1:$hostPort/xyz/');
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
preliminaryMDnsClient: FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: devicePort, weight: 1, priority: 1, target: 'appId'),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[
TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
],
},
),
logger: logger,
flutterUsage: TestUsage(),
),
});
testUsingContext('succeeds with iOS device with mDNS network device', () async {
final FakeIOSDevice device = FakeIOSDevice(
logReader: fakeLogReader,
portForwarder: portForwarder,
majorSdkVersion: 16,
interfaceType: IOSDeviceConnectionInterface.network,
);
testDeviceManager.devices = <Device>[device];
final FakeHotRunner hotRunner = FakeHotRunner();
hotRunner.onAttach = (
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async => 0;
hotRunner.exited = false;
hotRunner.isWaitingForObservatory = false;
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
await createTestCommandRunner(AttachCommand(
hotRunnerFactory: hotRunnerFactory,
artifacts: artifacts,
stdio: stdio,
logger: logger,
terminal: terminal,
signals: signals,
platform: platform,
processInfo: processInfo,
fileSystem: testFileSystem,
)).run(<String>['attach']);
await fakeLogReader.dispose();
expect(portForwarder.devicePort, null);
expect(portForwarder.hostPort, hostPort);
expect(hotRunnerFactory.devices, hasLength(1));
final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
expect(observatoryUri.toString(), 'http://111.111.111.111:123/xyz/');
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
preliminaryMDnsClient: FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'target-foo': <IPAddressResourceRecord>[
IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'srv-foo': <TxtResourceRecord>[
TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
],
},
),
logger: logger,
flutterUsage: TestUsage(),
),
});
testUsingContext('succeeds with iOS device with mDNS network device with debug-port', () async {
final FakeIOSDevice device = FakeIOSDevice(
logReader: fakeLogReader,
portForwarder: portForwarder,
majorSdkVersion: 16,
interfaceType: IOSDeviceConnectionInterface.network,
);
testDeviceManager.devices = <Device>[device];
final FakeHotRunner hotRunner = FakeHotRunner();
hotRunner.onAttach = (
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async => 0;
hotRunner.exited = false;
hotRunner.isWaitingForObservatory = false;
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
await createTestCommandRunner(AttachCommand(
hotRunnerFactory: hotRunnerFactory,
artifacts: artifacts,
stdio: stdio,
logger: logger,
terminal: terminal,
signals: signals,
platform: platform,
processInfo: processInfo,
fileSystem: testFileSystem,
)).run(<String>['attach', '--debug-port', '123']);
await fakeLogReader.dispose();
expect(portForwarder.devicePort, null);
expect(portForwarder.hostPort, hostPort);
expect(hotRunnerFactory.devices, hasLength(1));
final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
expect(observatoryUri.toString(), 'http://111.111.111.111:123/xyz/');
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
preliminaryMDnsClient: FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
],
<String, List<SrvResourceRecord>>{
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord('srv-bar', future, port: 321, weight: 1, priority: 1, target: 'target-bar'),
],
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'target-foo': <IPAddressResourceRecord>[
IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'srv-foo': <TxtResourceRecord>[
TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
],
},
),
logger: logger,
flutterUsage: TestUsage(),
),
});
testUsingContext('succeeds with iOS device with mDNS network device with debug-url', () async {
final FakeIOSDevice device = FakeIOSDevice(
logReader: fakeLogReader,
portForwarder: portForwarder,
majorSdkVersion: 16,
interfaceType: IOSDeviceConnectionInterface.network,
);
testDeviceManager.devices = <Device>[device];
final FakeHotRunner hotRunner = FakeHotRunner();
hotRunner.onAttach = (
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async => 0;
hotRunner.exited = false;
hotRunner.isWaitingForObservatory = false;
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
await createTestCommandRunner(AttachCommand(
hotRunnerFactory: hotRunnerFactory,
artifacts: artifacts,
stdio: stdio,
logger: logger,
terminal: terminal,
signals: signals,
platform: platform,
processInfo: processInfo,
fileSystem: testFileSystem,
)).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));
final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
expect(observatoryUri.toString(), 'http://111.111.111.111:123/xyz/');
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
preliminaryMDnsClient: FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
],
<String, List<SrvResourceRecord>>{
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord('srv-bar', future, port: 321, weight: 1, priority: 1, target: 'target-bar'),
],
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'target-foo': <IPAddressResourceRecord>[
IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'srv-foo': <TxtResourceRecord>[
TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
],
},
),
logger: logger, logger: logger,
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
), ),
...@@ -979,9 +1288,16 @@ class FakeIOSDevice extends Fake implements IOSDevice { ...@@ -979,9 +1288,16 @@ class FakeIOSDevice extends Fake implements IOSDevice {
DevicePortForwarder? portForwarder, DevicePortForwarder? portForwarder,
DeviceLogReader? logReader, DeviceLogReader? logReader,
this.onGetLogReader, this.onGetLogReader,
this.interfaceType = IOSDeviceConnectionInterface.none,
this.majorSdkVersion = 0,
}) : _portForwarder = portForwarder, _logReader = logReader; }) : _portForwarder = portForwarder, _logReader = logReader;
final DevicePortForwarder? _portForwarder; final DevicePortForwarder? _portForwarder;
@override
int majorSdkVersion;
@override
final IOSDeviceConnectionInterface interfaceType;
@override @override
DevicePortForwarder get portForwarder => _portForwarder!; DevicePortForwarder get portForwarder => _portForwarder!;
...@@ -1029,12 +1345,14 @@ class FakeIOSDevice extends Fake implements IOSDevice { ...@@ -1029,12 +1345,14 @@ class FakeIOSDevice extends Fake implements IOSDevice {
class FakeMDnsClient extends Fake implements MDnsClient { class FakeMDnsClient extends Fake implements MDnsClient {
FakeMDnsClient(this.ptrRecords, this.srvResponse, { FakeMDnsClient(this.ptrRecords, this.srvResponse, {
this.txtResponse = const <String, List<TxtResourceRecord>>{}, this.txtResponse = const <String, List<TxtResourceRecord>>{},
this.ipResponse = const <String, List<IPAddressResourceRecord>>{},
this.osErrorOnStart = false, this.osErrorOnStart = false,
}); });
final List<PtrResourceRecord> ptrRecords; final List<PtrResourceRecord> ptrRecords;
final Map<String, List<SrvResourceRecord>> srvResponse; final Map<String, List<SrvResourceRecord>> srvResponse;
final Map<String, List<TxtResourceRecord>> txtResponse; final Map<String, List<TxtResourceRecord>> txtResponse;
final Map<String, List<IPAddressResourceRecord>> ipResponse;
final bool osErrorOnStart; final bool osErrorOnStart;
@override @override
...@@ -1054,7 +1372,7 @@ class FakeMDnsClient extends Fake implements MDnsClient { ...@@ -1054,7 +1372,7 @@ class FakeMDnsClient extends Fake implements MDnsClient {
ResourceRecordQuery query, { ResourceRecordQuery query, {
Duration timeout = const Duration(seconds: 5), Duration timeout = const Duration(seconds: 5),
}) { }) {
if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsObservatoryDiscovery.dartObservatoryName) { if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) {
return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>; return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>;
} }
if (T == SrvResourceRecord) { if (T == SrvResourceRecord) {
...@@ -1065,6 +1383,10 @@ class FakeMDnsClient extends Fake implements MDnsClient { ...@@ -1065,6 +1383,10 @@ class FakeMDnsClient extends Fake implements MDnsClient {
final String key = query.fullyQualifiedName; final String key = query.fullyQualifiedName;
return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) as Stream<T>; return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) as Stream<T>;
} }
if (T == IPAddressResourceRecord) {
final String key = query.fullyQualifiedName;
return Stream<IPAddressResourceRecord>.fromIterable(ipResponse[key] ?? <IPAddressResourceRecord>[]) as Stream<T>;
}
throw UnsupportedError('Unsupported query type $T'); throw UnsupportedError('Unsupported query type $T');
} }
......
...@@ -21,6 +21,8 @@ import 'package:flutter_tools/src/commands/drive.dart'; ...@@ -21,6 +21,8 @@ import 'package:flutter_tools/src/commands/drive.dart';
import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/drive/drive_service.dart'; import 'package:flutter_tools/src/drive/drive_service.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/iproxy.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
...@@ -406,6 +408,94 @@ void main() { ...@@ -406,6 +408,94 @@ void main() {
FileSystem: () => MemoryFileSystem.test(), FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('Port publication not disabled for network device', () async {
final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
final Device networkDevice = FakeIosDevice()
..interfaceType = IOSDeviceConnectionInterface.network;
fakeDeviceManager.devices = <Device>[networkDevice];
await expectLater(() => createTestCommandRunner(command).run(<String>[
'drive',
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.disablePortPublication, false);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => fakeDeviceManager,
});
testUsingContext('Port publication is disabled for wired device', () async {
final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
await expectLater(() => createTestCommandRunner(command).run(<String>[
'drive',
]), throwsToolExit());
final Device usbDevice = FakeIosDevice()
..interfaceType = IOSDeviceConnectionInterface.usb;
fakeDeviceManager.devices = <Device>[usbDevice];
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.disablePortPublication, true);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => fakeDeviceManager,
});
testUsingContext('Port publication does not default to enabled for network device if flag manually added', () async {
final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
final Device networkDevice = FakeIosDevice()
..interfaceType = IOSDeviceConnectionInterface.network;
fakeDeviceManager.devices = <Device>[networkDevice];
await expectLater(() => createTestCommandRunner(command).run(<String>[
'drive',
'--no-publish-port'
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.disablePortPublication, true);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => fakeDeviceManager,
});
} }
// Unfortunately Device, despite not being immutable, has an `operator ==`. // Unfortunately Device, despite not being immutable, has an `operator ==`.
...@@ -577,3 +667,14 @@ class FakeProcessSignal extends Fake implements io.ProcessSignal { ...@@ -577,3 +667,14 @@ class FakeProcessSignal extends Fake implements io.ProcessSignal {
@override @override
Stream<io.ProcessSignal> watch() => controller.stream; Stream<io.ProcessSignal> watch() => controller.stream;
} }
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeIosDevice extends Fake implements IOSDevice {
@override
IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.usb;
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
}
...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/utils.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/utils.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/iproxy.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
...@@ -554,6 +555,53 @@ void main() { ...@@ -554,6 +555,53 @@ void main() {
); );
}); });
testWithoutContext('Get launch arguments for physical device with iPv4 network connection', () {
final DebuggingOptions original = DebuggingOptions.enabled(
BuildInfo.debug,
);
final List<String> launchArguments = original.getIOSLaunchArguments(
EnvironmentType.physical,
null,
<String, Object?>{},
interfaceType: IOSDeviceConnectionInterface.network,
);
expect(
launchArguments.join(' '),
<String>[
'--enable-dart-profiling',
'--enable-checked-mode',
'--verify-entry-points',
'--observatory-host=0.0.0.0',
].join(' '),
);
});
testWithoutContext('Get launch arguments for physical device with iPv6 network connection', () {
final DebuggingOptions original = DebuggingOptions.enabled(
BuildInfo.debug,
);
final List<String> launchArguments = original.getIOSLaunchArguments(
EnvironmentType.physical,
null,
<String, Object?>{},
ipv6: true,
interfaceType: IOSDeviceConnectionInterface.network,
);
expect(
launchArguments.join(' '),
<String>[
'--enable-dart-profiling',
'--enable-checked-mode',
'--verify-entry-points',
'--observatory-host=::0',
].join(' '),
);
});
testWithoutContext('Get launch arguments for physical device with debugging disabled with available launch arguments', () { testWithoutContext('Get launch arguments for physical device with debugging disabled with available launch arguments', () {
final DebuggingOptions original = DebuggingOptions.disabled( final DebuggingOptions original = DebuggingOptions.disabled(
BuildInfo.debug, BuildInfo.debug,
......
...@@ -19,9 +19,11 @@ import 'package:flutter_tools/src/ios/devices.dart'; ...@@ -19,9 +19,11 @@ import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/ios_deploy.dart';
import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/ios/iproxy.dart';
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart'; import '../../src/fake_devices.dart';
import '../../src/fake_process_manager.dart'; import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart'; import '../../src/fakes.dart';
...@@ -66,9 +68,10 @@ const FakeCommand kLaunchDebugCommand = FakeCommand(command: <String>[ ...@@ -66,9 +68,10 @@ const FakeCommand kLaunchDebugCommand = FakeCommand(command: <String>[
FakeCommand attachDebuggerCommand({ FakeCommand attachDebuggerCommand({
IOSink? stdin, IOSink? stdin,
Completer<void>? completer, Completer<void>? completer,
bool isNetworkDevice = false,
}) { }) {
return FakeCommand( return FakeCommand(
command: const <String>[ command: <String>[
'script', 'script',
'-t', '-t',
'0', '0',
...@@ -79,8 +82,11 @@ FakeCommand attachDebuggerCommand({ ...@@ -79,8 +82,11 @@ FakeCommand attachDebuggerCommand({
'--bundle', '--bundle',
'/', '/',
'--debug', '--debug',
'--no-wifi', if (!isNetworkDevice) '--no-wifi',
'--args', '--args',
if (isNetworkDevice)
'--enable-dart-profiling --enable-checked-mode --verify-entry-points --observatory-host=0.0.0.0'
else
'--enable-dart-profiling --enable-checked-mode --verify-entry-points', '--enable-dart-profiling --enable-checked-mode --verify-entry-points',
], ],
completer: completer, completer: completer,
...@@ -188,7 +194,7 @@ void main() { ...@@ -188,7 +194,7 @@ void main() {
expect(await device.stopApp(iosApp), false); expect(await device.stopApp(iosApp), false);
}); });
testWithoutContext('IOSDevice.startApp prints warning message if discovery takes longer than configured timeout', () async { testWithoutContext('IOSDevice.startApp prints warning message if discovery takes longer than configured timeout for wired device', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test(); final BufferLogger logger = BufferLogger.test();
final CompleterIOSink stdin = CompleterIOSink(); final CompleterIOSink stdin = CompleterIOSink();
...@@ -226,12 +232,59 @@ void main() { ...@@ -226,12 +232,59 @@ void main() {
expect(launchResult.started, true); expect(launchResult.started, true);
expect(launchResult.hasObservatory, true); expect(launchResult.hasObservatory, true);
expect(await device.stopApp(iosApp), false); expect(await device.stopApp(iosApp), false);
expect(logger.errorText, contains('iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...')); expect(logger.errorText, contains('The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...'));
expect(utf8.decoder.convert(stdin.writes.first), contains('process interrupt')); expect(utf8.decoder.convert(stdin.writes.first), contains('process interrupt'));
completer.complete(); completer.complete();
expect(processManager, hasNoRemainingExpectations); expect(processManager, hasNoRemainingExpectations);
}); });
testUsingContext('IOSDevice.startApp prints warning message if discovery takes longer than configured timeout for wireless device', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
final CompleterIOSink stdin = CompleterIOSink();
final Completer<void> completer = Completer<void>();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
attachDebuggerCommand(stdin: stdin, completer: completer, isNetworkDevice: true),
]);
final IOSDevice device = setUpIOSDevice(
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
interfaceType: IOSDeviceConnectionInterface.network,
);
final IOSApp iosApp = PrebuiltIOSApp(
projectBundleId: 'app',
bundleName: 'Runner',
uncompressedBundle: fileSystem.currentDirectory,
applicationPackage: fileSystem.currentDirectory,
);
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
device.portForwarder = const NoOpDevicePortForwarder();
device.setLogReader(iosApp, deviceLogReader);
// Start writing messages to the log reader.
deviceLogReader.addLine('Foo');
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
final LaunchResult launchResult = await device.startApp(iosApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
platformArgs: <String, dynamic>{},
discoveryTimeout: Duration.zero,
);
expect(launchResult.started, true);
expect(launchResult.hasObservatory, true);
expect(await device.stopApp(iosApp), false);
expect(logger.errorText, contains('The Dart VM Service was not discovered after 45 seconds. This is taking much longer than expected...'));
expect(logger.errorText, contains('Click "Allow" to the prompt asking if you would like to find and connect devices on your local network.'));
completer.complete();
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(),
});
testWithoutContext('IOSDevice.startApp succeeds in release mode', () async { testWithoutContext('IOSDevice.startApp succeeds in release mode', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
...@@ -505,6 +558,7 @@ IOSDevice setUpIOSDevice({ ...@@ -505,6 +558,7 @@ IOSDevice setUpIOSDevice({
Logger? logger, Logger? logger,
ProcessManager? processManager, ProcessManager? processManager,
IOSDeploy? iosDeploy, IOSDeploy? iosDeploy,
IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.usb,
}) { }) {
final Artifacts artifacts = Artifacts.test(); final Artifacts artifacts = Artifacts.test();
final FakePlatform macPlatform = FakePlatform( final FakePlatform macPlatform = FakePlatform(
...@@ -542,7 +596,7 @@ IOSDevice setUpIOSDevice({ ...@@ -542,7 +596,7 @@ IOSDevice setUpIOSDevice({
cache: cache, cache: cache,
), ),
cpuArchitecture: DarwinArch.arm64, cpuArchitecture: DarwinArch.arm64,
interfaceType: IOSDeviceConnectionInterface.usb, interfaceType: interfaceType,
); );
} }
...@@ -554,3 +608,18 @@ class FakeDevicePortForwarder extends Fake implements DevicePortForwarder { ...@@ -554,3 +608,18 @@ class FakeDevicePortForwarder extends Fake implements DevicePortForwarder {
disposed = true; disposed = true;
} }
} }
class FakeMDnsVmServiceDiscovery extends Fake implements MDnsVmServiceDiscovery {
@override
Future<Uri?> getVMServiceUriForLaunch(
String applicationId,
Device device, {
bool usesIpv6 = false,
int? hostVmservicePort,
required int deviceVmservicePort,
bool isNetworkDevice = false,
Duration timeout = Duration.zero,
}) async {
return Uri.tryParse('http://0.0.0.0:1234');
}
}
...@@ -479,7 +479,7 @@ void main() { ...@@ -479,7 +479,7 @@ void main() {
stdout: devicesOutput, stdout: devicesOutput,
)); ));
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices(); final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(3)); expect(devices, hasLength(4));
expect(devices[0].id, '00008027-00192736010F802E'); expect(devices[0].id, '00008027-00192736010F802E');
expect(devices[0].name, 'An iPhone (Space Gray)'); expect(devices[0].name, 'An iPhone (Space Gray)');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54'); expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
...@@ -488,10 +488,14 @@ void main() { ...@@ -488,10 +488,14 @@ void main() {
expect(devices[1].name, 'iPad 1'); expect(devices[1].name, 'iPad 1');
expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54'); expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[1].cpuArchitecture, DarwinArch.armv7); expect(devices[1].cpuArchitecture, DarwinArch.armv7);
expect(devices[2].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); expect(devices[2].id, '234234234234234234345445687594e089dede3c44');
expect(devices[2].name, 'iPad 2'); expect(devices[2].name, 'A networked iPad');
expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54'); expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[2].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture. expect(devices[2].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
expect(devices[3].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
expect(devices[3].name, 'iPad 2');
expect(await devices[3].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[3].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
expect(fakeProcessManager, hasNoRemainingExpectations); expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Platform: () => macPlatform, Platform: () => macPlatform,
......
...@@ -17,7 +17,7 @@ import '../src/common.dart'; ...@@ -17,7 +17,7 @@ import '../src/common.dart';
void main() { void main() {
group('mDNS Discovery', () { group('mDNS Discovery', () {
final int year3000 = DateTime(3000).millisecondsSinceEpoch; final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch;
setUp(() { setUp(() {
setNetworkInterfaceLister( setNetworkInterfaceLister(
...@@ -33,28 +33,106 @@ void main() { ...@@ -33,28 +33,106 @@ void main() {
resetNetworkInterfaceLister(); resetNetworkInterfaceLister();
}); });
group('for attach', () {
late MDnsClient emptyClient;
testWithoutContext('No ports available', () async { setUp(() {
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}); emptyClient = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
});
testWithoutContext('Find result in preliminary client', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
expect(result, isNotNull);
});
testWithoutContext('Do not find result in preliminary client, but find in main client', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final int? port = (await portDiscovery.query())?.port;
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
expect(result, isNotNull);
});
testWithoutContext('Find multiple in preliminary client', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('baz', future, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
expect(portDiscovery.queryForAttach, throwsToolExit());
});
testWithoutContext('No ports available', () async {
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final int? port = (await portDiscovery.queryForAttach())?.port;
expect(port, isNull); expect(port, isNull);
}); });
testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async { testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async {
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
final BufferLogger logger = BufferLogger.test(); final BufferLogger logger = BufferLogger.test();
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: emptyClient,
preliminaryMDnsClient: emptyClient,
logger: logger, logger: logger,
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final Uri? uri = await portDiscovery.getObservatoryUri( final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
'', '',
FakeIOSDevice(), FakeIOSDevice(),
); );
...@@ -65,190 +143,557 @@ void main() { ...@@ -65,190 +143,557 @@ void main() {
testWithoutContext('One port available, no appId', () async { testWithoutContext('One port available, no appId', () async {
final MDnsClient client = FakeMDnsClient( final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[ <PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar'),
], ],
<String, List<SrvResourceRecord>>{ <String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[ 'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
], ],
}, },
); );
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final int? port = (await portDiscovery.query())?.port; final int? port = (await portDiscovery.queryForAttach())?.port;
expect(port, 123); expect(port, 123);
}); });
testWithoutContext('One port available, no appId, with authCode', () async { testWithoutContext('One port available, no appId, with authCode', () async {
final MDnsClient client = FakeMDnsClient( final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[ <PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar'),
], ],
<String, List<SrvResourceRecord>>{ <String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[ 'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
], ],
}, },
txtResponse: <String, List<TxtResourceRecord>>{ txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[ 'bar': <TxtResourceRecord>[
TxtResourceRecord('bar', year3000, text: 'authCode=xyz\n'), TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
], ],
}, },
); );
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final MDnsObservatoryDiscoveryResult? result = await portDiscovery.query(); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
expect(result?.port, 123); expect(result?.port, 123);
expect(result?.authCode, 'xyz/'); expect(result?.authCode, 'xyz/');
}); });
testWithoutContext('Multiple ports available, without appId', () async { testWithoutContext('Multiple ports available, with appId', () async {
final MDnsClient client = FakeMDnsClient( final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[ <PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'), PtrResourceRecord('baz', future, domainName: 'fiz'),
], ],
<String, List<SrvResourceRecord>>{ <String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[ 'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
], ],
'fiz': <SrvResourceRecord>[ 'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
], ],
}, },
); );
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
expect(portDiscovery.query, throwsToolExit()); final int? port = (await portDiscovery.queryForAttach(applicationId: 'fiz'))?.port;
expect(port, 321);
}); });
testWithoutContext('Multiple ports available, with appId', () async { testWithoutContext('Multiple ports available per process, with appId', () async {
final MDnsClient client = FakeMDnsClient( final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[ <PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'), PtrResourceRecord('baz', future, domainName: 'fiz'),
], ],
<String, List<SrvResourceRecord>>{ <String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[ 'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
], ],
'fiz': <SrvResourceRecord>[ 'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), SrvResourceRecord('fiz', future, port: 4321, weight: 1, priority: 1, target: 'local'),
SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
], ],
}, },
); );
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final int? port = (await portDiscovery.query(applicationId: 'fiz'))?.port; final int? port = (await portDiscovery.queryForAttach(applicationId: 'bar'))?.port;
expect(port, 321); expect(port, 1234);
}); });
testWithoutContext('Multiple ports available per process, with appId', () async { testWithoutContext('Throws Exception when client throws OSError on start', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{},
osErrorOnStart: true,
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
expect(
() async => portDiscovery.queryForAttach(),
throwsException,
);
});
testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
final MDnsClient client = FakeMDnsClient( final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[ <PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
], ],
<String, List<SrvResourceRecord>>{ <String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[ 'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
], ],
'fiz': <SrvResourceRecord>[ },
SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'), );
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForAttach('bar', device, hostVmservicePort: 0);
expect(uri.toString(), 'http://127.0.0.1:123/');
});
testWithoutContext('Get network device IP (iPv4)', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[
TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
], ],
}, },
); );
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final int? port = (await portDiscovery.query(applicationId: 'bar'))?.port; final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
expect(port, 1234); 'bar',
device,
isNetworkDevice: true,
);
expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
}); });
testWithoutContext('Query returns null', () async { testWithoutContext('Get network device IP (iPv6)', () async {
final MDnsClient client = FakeMDnsClient( final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[], <PtrResourceRecord>[
<String, List<SrvResourceRecord>>{}, PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[
TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
],
},
); );
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final int? port = (await portDiscovery.query(applicationId: 'bar'))?.port; final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
expect(port, isNull); 'bar',
device,
isNetworkDevice: true,
);
expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
}); });
testWithoutContext('Throws Exception when client throws OSError on start', () async { testWithoutContext('Throw error if unable to find VM service with app id and device port', () async {
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}, osErrorOnStart: true); final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
expect(
portDiscovery.getVMServiceUriForAttach(
'srv-bar',
device,
deviceVmservicePort: 321,
),
throwsToolExit(
message: 'Did not find a Dart VM Service advertised for srv-bar on port 321.'
),
);
});
testWithoutContext('Throw error if unable to find VM Service with app id', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
expect(
portDiscovery.getVMServiceUriForAttach(
'srv-asdf',
device,
),
throwsToolExit(
message: 'Did not find a Dart VM Service advertised for srv-asdf.'
),
);
});
});
group('for launch', () {
testWithoutContext('No ports available', () async {
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForLaunch(
applicationId: 'app-id',
deviceVmservicePort: 123,
);
expect(result, null);
});
testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async {
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
final BufferLogger logger = BufferLogger.test();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: logger,
flutterUsage: TestUsage(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'',
FakeIOSDevice(),
deviceVmservicePort: 0,
);
expect(uri, isNull);
expect(logger.errorText, contains('Personal Hotspot'));
});
testWithoutContext('Throws Exception when client throws OSError on start', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{},
osErrorOnStart: true,
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
expect( expect(
() async => portDiscovery.query(), () async => portDiscovery.queryForLaunch(applicationId: 'app-id', deviceVmservicePort: 123),
throwsException, throwsException,
); );
}); });
testWithoutContext('Correctly builds Observatory URI with hostVmservicePort == 0', () async { testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
final MDnsClient client = FakeMDnsClient( final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[ <PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar'),
], ],
<String, List<SrvResourceRecord>>{ <String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[ 'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
], ],
}, },
); );
final FakeIOSDevice device = FakeIOSDevice(); final FakeIOSDevice device = FakeIOSDevice();
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client, mdnsClient: client,
logger: BufferLogger.test(), logger: BufferLogger.test(),
flutterUsage: TestUsage(), flutterUsage: TestUsage(),
); );
final Uri? uri = await portDiscovery.getObservatoryUri('bar', device, hostVmservicePort: 0); final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'bar',
device,
hostVmservicePort: 0,
deviceVmservicePort: 123,
);
expect(uri.toString(), 'http://127.0.0.1:123/'); expect(uri.toString(), 'http://127.0.0.1:123/');
}); });
testWithoutContext('Get network device IP (iPv4)', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[
TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'bar',
device,
isNetworkDevice: true,
deviceVmservicePort: 1234,
);
expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
});
testWithoutContext('Get network device IP (iPv6)', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[
TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'bar',
device,
isNetworkDevice: true,
deviceVmservicePort: 1234,
);
expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
});
testWithoutContext('Throw error if unable to find VM Service with app id and device port', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
expect(
portDiscovery.getVMServiceUriForLaunch(
'srv-bar',
device,
deviceVmservicePort: 321,
),
throwsToolExit(
message:'Did not find a Dart VM Service advertised for srv-bar on port 321.'),
);
});
});
testWithoutContext('Find firstMatchingVmService with many available and no application id', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(client);
expect(result?.domainName, 'srv-foo');
});
testWithoutContext('Find firstMatchingVmService app id', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord('srv-bar', future, port: 222, weight: 1, priority: 1, target: 'target-bar'),
SrvResourceRecord('srv-bar', future, port: 333, weight: 1, priority: 1, target: 'target-bar-2'),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord('srv-baz', future, port: 444, weight: 1, priority: 1, target: 'target-baz'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
flutterUsage: TestUsage(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
client,
applicationId: 'srv-bar'
);
expect(result?.domainName, 'srv-bar');
expect(result?.port, 222);
});
}); });
} }
class FakeMDnsClient extends Fake implements MDnsClient { class FakeMDnsClient extends Fake implements MDnsClient {
FakeMDnsClient(this.ptrRecords, this.srvResponse, { FakeMDnsClient(this.ptrRecords, this.srvResponse, {
this.txtResponse = const <String, List<TxtResourceRecord>>{}, this.txtResponse = const <String, List<TxtResourceRecord>>{},
this.ipResponse = const <String, List<IPAddressResourceRecord>>{},
this.osErrorOnStart = false, this.osErrorOnStart = false,
}); });
final List<PtrResourceRecord> ptrRecords; final List<PtrResourceRecord> ptrRecords;
final Map<String, List<SrvResourceRecord>> srvResponse; final Map<String, List<SrvResourceRecord>> srvResponse;
final Map<String, List<TxtResourceRecord>> txtResponse; final Map<String, List<TxtResourceRecord>> txtResponse;
final Map<String, List<IPAddressResourceRecord>> ipResponse;
final bool osErrorOnStart; final bool osErrorOnStart;
@override @override
...@@ -268,7 +713,7 @@ class FakeMDnsClient extends Fake implements MDnsClient { ...@@ -268,7 +713,7 @@ class FakeMDnsClient extends Fake implements MDnsClient {
ResourceRecordQuery query, { ResourceRecordQuery query, {
Duration timeout = const Duration(seconds: 5), Duration timeout = const Duration(seconds: 5),
}) { }) {
if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsObservatoryDiscovery.dartObservatoryName) { if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) {
return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>; return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>;
} }
if (T == SrvResourceRecord) { if (T == SrvResourceRecord) {
...@@ -279,6 +724,10 @@ class FakeMDnsClient extends Fake implements MDnsClient { ...@@ -279,6 +724,10 @@ class FakeMDnsClient extends Fake implements MDnsClient {
final String key = query.fullyQualifiedName; final String key = query.fullyQualifiedName;
return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) as Stream<T>; return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) as Stream<T>;
} }
if (T == IPAddressResourceRecord) {
final String key = query.fullyQualifiedName;
return Stream<IPAddressResourceRecord>.fromIterable(ipResponse[key] ?? <IPAddressResourceRecord>[]) as Stream<T>;
}
throw UnsupportedError('Unsupported query type $T'); throw UnsupportedError('Unsupported query type $T');
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment