diff --git a/dev/bots/test.dart b/dev/bots/test.dart index c546075ce78..aef335fd140 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -919,7 +919,7 @@ Future _verifyVersion(String filename) async { print('$redLine'); exit(1); } - final RegExp pattern = RegExp(r'^\d+\.\d+\.\d+(?:|-pre\.\d+|\+hotfix\.\d+)$'); + final RegExp pattern = RegExp(r'^\d+\.\d+\.\d+(?:|-pre\.\d+|\+hotfix\.\d+)(-pre\.\d+)?$'); if (!version.contains(pattern)) { print('$redLine'); print('The version logic generated an invalid version string.'); diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index dd68a476a86..20a23a31fa2 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -4,8 +4,6 @@ import 'dart:async'; -import 'package:multicast_dns/multicast_dns.dart'; - import '../artifacts.dart'; import '../base/common.dart'; import '../base/context.dart'; @@ -20,6 +18,7 @@ import '../fuchsia/fuchsia_device.dart'; import '../globals.dart'; import '../ios/devices.dart'; import '../ios/simulators.dart'; +import '../mdns_discovery.dart'; import '../project.dart'; import '../protocol_discovery.dart'; import '../resident_runner.dart'; @@ -204,7 +203,7 @@ class AttachCommand extends FlutterCommand { final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback; bool attachLogger = false; - if (devicePort == null && debugUri == null) { + if (devicePort == null && debugUri == null) { if (device is FuchsiaDevice) { attachLogger = true; final String module = argResults['module']; @@ -225,10 +224,11 @@ class AttachCommand extends FlutterCommand { rethrow; } } else if ((device is IOSDevice) || (device is IOSSimulator)) { - final MDnsObservatoryDiscoveryResult result = await MDnsObservatoryDiscovery().query(applicationId: appId); - if (result != null) { - observatoryUri = await _buildObservatoryUri(device, hostname, result.port, result.authCode); - } + observatoryUri = await MDnsObservatoryDiscovery.instance.getObservatoryUri( + appId, + device, + usesIpv6, + ); } // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery. if (observatoryUri == null) { @@ -250,8 +250,13 @@ class AttachCommand extends FlutterCommand { } } } else { - observatoryUri = await _buildObservatoryUri(device, - debugUri?.host ?? hostname, devicePort ?? debugUri.port, debugUri?.path); + observatoryUri = await buildObservatoryUri( + device, + debugUri?.host ?? hostname, + devicePort ?? debugUri.port, + observatoryPort, + debugUri?.path, + ); } try { final bool useHot = getBuildInfo().isDebug; @@ -333,22 +338,6 @@ class AttachCommand extends FlutterCommand { } Future _validateArguments() async { } - - Future _buildObservatoryUri(Device device, - String host, int devicePort, [String authCode]) async { - String path = '/'; - if (authCode != null) { - path = authCode; - } - // Not having a trailing slash can cause problems in some situations. - // Ensure that there's one present. - if (!path.endsWith('/')) { - path += '/'; - } - final int localPort = observatoryPort - ?? await device.portForwarder.forward(devicePort); - return Uri(scheme: 'http', host: host, port: localPort, path: path); - } } class HotRunnerFactory { @@ -381,132 +370,3 @@ class HotRunnerFactory { ipv6: ipv6, ); } - -class MDnsObservatoryDiscoveryResult { - MDnsObservatoryDiscoveryResult(this.port, this.authCode); - final int port; - final String authCode; -} - -/// A wrapper around [MDnsClient] to find a Dart observatory instance. -class MDnsObservatoryDiscovery { - /// Creates a new [MDnsObservatoryDiscovery] object. - /// - /// The [client] parameter will be defaulted to a new [MDnsClient] if null. - /// The [applicationId] parameter may be null, and can be used to - /// automatically select which application to use if multiple are advertising - /// Dart observatory ports. - MDnsObservatoryDiscovery({MDnsClient mdnsClient}) - : client = mdnsClient ?? MDnsClient(); - - /// The [MDnsClient] used to do a lookup. - final MDnsClient client; - - static const String dartObservatoryName = '_dartobservatory._tcp.local'; - - /// Executes an mDNS query for a Dart Observatory. - /// - /// 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 - /// the bundle ID. - /// - /// If it is not null, this method will find the port and authentication code - /// of the Dart Observatory for that application. If it cannot find a Dart - /// Observatory matching that application identifier, it will call - /// [throwToolExit]. - /// - /// If it is null and there are multiple ports available, the user will be - /// prompted with a list of available observatory ports and asked to select - /// one. - /// - /// If it is null and there is only one available instance of Observatory, - /// it will return that instance's information regardless of what application - /// the Observatory instance is for. - Future query({String applicationId}) async { - printStatus('Checking for advertised Dart observatories...'); - try { - await client.start(); - final List pointerRecords = await client - .lookup( - ResourceRecordQuery.serverPointer(dartObservatoryName), - ) - .toList(); - if (pointerRecords.isEmpty) { - return null; - } - // We have no guarantee that we won't get multiple hits from the same - // service on this. - final List uniqueDomainNames = pointerRecords - .map((PtrResourceRecord record) => record.domainName) - .toSet() - .toList(); - - String domainName; - if (applicationId != null) { - for (String name in uniqueDomainNames) { - if (name.toLowerCase().startsWith(applicationId.toLowerCase())) { - domainName = name; - break; - } - } - if (domainName == null) { - throwToolExit('Did not find a observatory port advertised for $applicationId.'); - } - } else if (uniqueDomainNames.length > 1) { - final StringBuffer buffer = StringBuffer(); - buffer.writeln('There are multiple observatory ports available.'); - buffer.writeln('Rerun this command with one of the following passed in as the appId:'); - buffer.writeln(''); - for (final String uniqueDomainName in uniqueDomainNames) { - buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}'); - } - throwToolExit(buffer.toString()); - } else { - domainName = pointerRecords[0].domainName; - } - printStatus('Checking for available port on $domainName'); - // Here, if we get more than one, it should just be a duplicate. - final List srv = await client - .lookup( - ResourceRecordQuery.service(domainName), - ) - .toList(); - if (srv.isEmpty) { - return null; - } - if (srv.length > 1) { - printError('Unexpectedly found more than one observatory report for $domainName ' - '- using first one (${srv.first.port}).'); - } - printStatus('Checking for authentication code for $domainName'); - final List txt = await client - .lookup( - ResourceRecordQuery.text(domainName), - ) - ?.toList(); - if (txt == null || txt.isEmpty) { - return MDnsObservatoryDiscoveryResult(srv.first.port, ''); - } - String authCode = ''; - const String authCodePrefix = 'authCode='; - String raw = txt.first.text; - // TXT has a format of [, text], so if the length is 2, - // that means that TXT is empty. - if (raw.length > 2) { - // Remove length byte from raw txt. - raw = raw.substring(1); - if (raw.startsWith(authCodePrefix)) { - authCode = raw.substring(authCodePrefix.length); - // The Observatory currently expects a trailing '/' as part of the - // URI, otherwise an invalid authentication code response is given. - if (!authCode.endsWith('/')) { - authCode += '/'; - } - } - } - return MDnsObservatoryDiscoveryResult(srv.first.port, authCode); - } finally { - client.stop(); - } - } -} diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 83985e5fce6..1207434e49b 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -42,6 +42,7 @@ import 'macos/cocoapods_validator.dart'; import 'macos/macos_workflow.dart'; import 'macos/xcode.dart'; import 'macos/xcode_validator.dart'; +import 'mdns_discovery.dart'; import 'reporting/reporting.dart'; import 'run_hot.dart'; import 'version.dart'; @@ -96,6 +97,7 @@ Future runInContext( LinuxWorkflow: () => const LinuxWorkflow(), Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(), MacOSWorkflow: () => const MacOSWorkflow(), + MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(), OperatingSystemUtils: () => OperatingSystemUtils(), SimControl: () => SimControl(), Stdio: () => const Stdio(), diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 51d8f85cbcd..6653c0c3cdb 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -18,6 +18,7 @@ import '../build_info.dart'; import '../convert.dart'; import '../device.dart'; import '../globals.dart'; +import '../mdns_discovery.dart'; import '../project.dart'; import '../protocol_discovery.dart'; import '../reporting/reporting.dart'; @@ -382,16 +383,36 @@ class IOSDevice extends Device { return LaunchResult.succeeded(); } + Uri localUri; try { printTrace('Application launched on the device. Waiting for observatory port.'); - final Uri localUri = await observatoryDiscovery.uri; - return LaunchResult.succeeded(observatoryUri: localUri); + localUri = await MDnsObservatoryDiscovery.instance.getObservatoryUri( + package.id, + this, + ipv6, + debuggingOptions.observatoryPort, + ); + if (localUri != null) { + return LaunchResult.succeeded(observatoryUri: localUri); + } + } catch (error) { + printError('Failed to establish a debug connection with $id: $error'); + } + + // Fallback to manual protocol discovery + printTrace('mDNS lookup failed, attempting fallback to reading device log.'); + try { + printTrace('Waiting for observatory port.'); + localUri = await observatoryDiscovery.uri; + if (localUri != null) { + return LaunchResult.succeeded(observatoryUri: localUri); + } } catch (error) { printError('Failed to establish a debug connection with $id: $error'); - return LaunchResult.failed(); } finally { await observatoryDiscovery?.cancel(); } + return LaunchResult.failed(); } finally { installStatus.stop(); } diff --git a/packages/flutter_tools/lib/src/mdns_discovery.dart b/packages/flutter_tools/lib/src/mdns_discovery.dart new file mode 100644 index 00000000000..72aa60a0bf0 --- /dev/null +++ b/packages/flutter_tools/lib/src/mdns_discovery.dart @@ -0,0 +1,174 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:multicast_dns/multicast_dns.dart'; + +import 'base/common.dart'; +import 'base/context.dart'; +import 'base/io.dart'; +import 'device.dart'; +import 'globals.dart'; + +/// A wrapper around [MDnsClient] to find a Dart observatory instance. +class MDnsObservatoryDiscovery { + /// Creates a new [MDnsObservatoryDiscovery] object. + /// + /// The [client] parameter will be defaulted to a new [MDnsClient] if null. + /// The [applicationId] parameter may be null, and can be used to + /// automatically select which application to use if multiple are advertising + /// Dart observatory ports. + MDnsObservatoryDiscovery({MDnsClient mdnsClient}) + : client = mdnsClient ?? MDnsClient(); + + /// The [MDnsClient] used to do a lookup. + final MDnsClient client; + + @visibleForTesting + static const String dartObservatoryName = '_dartobservatory._tcp.local'; + + static MDnsObservatoryDiscovery get instance => context.get(); + + /// Executes an mDNS query for a Dart Observatory. + /// + /// 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 + /// the bundle ID. + /// + /// If it is not null, this method will find the port and authentication code + /// of the Dart Observatory for that application. If it cannot find a Dart + /// Observatory matching that application identifier, it will call + /// [throwToolExit]. + /// + /// If it is null and there are multiple ports available, the user will be + /// prompted with a list of available observatory ports and asked to select + /// one. + /// + /// If it is null and there is only one available instance of Observatory, + /// it will return that instance's information regardless of what application + /// the Observatory instance is for. + Future query({String applicationId}) async { + printStatus('Checking for advertised Dart observatories...'); + try { + await client.start(); + final List pointerRecords = await client + .lookup( + ResourceRecordQuery.serverPointer(dartObservatoryName), + ) + .toList(); + if (pointerRecords.isEmpty) { + return null; + } + // We have no guarantee that we won't get multiple hits from the same + // service on this. + final List uniqueDomainNames = pointerRecords + .map((PtrResourceRecord record) => record.domainName) + .toSet() + .toList(); + + String domainName; + if (applicationId != null) { + for (String name in uniqueDomainNames) { + if (name.toLowerCase().startsWith(applicationId.toLowerCase())) { + domainName = name; + break; + } + } + if (domainName == null) { + throwToolExit('Did not find a observatory port advertised for $applicationId.'); + } + } else if (uniqueDomainNames.length > 1) { + final StringBuffer buffer = StringBuffer(); + buffer.writeln('There are multiple observatory ports available.'); + buffer.writeln('Rerun this command with one of the following passed in as the appId:'); + buffer.writeln(''); + for (final String uniqueDomainName in uniqueDomainNames) { + buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}'); + } + throwToolExit(buffer.toString()); + } else { + domainName = pointerRecords[0].domainName; + } + printStatus('Checking for available port on $domainName'); + // Here, if we get more than one, it should just be a duplicate. + final List srv = await client + .lookup( + ResourceRecordQuery.service(domainName), + ) + .toList(); + if (srv.isEmpty) { + return null; + } + if (srv.length > 1) { + printError('Unexpectedly found more than one observatory report for $domainName ' + '- using first one (${srv.first.port}).'); + } + printStatus('Checking for authentication code for $domainName'); + final List txt = await client + .lookup( + ResourceRecordQuery.text(domainName), + ) + ?.toList(); + if (txt == null || txt.isEmpty) { + return MDnsObservatoryDiscoveryResult(srv.first.port, ''); + } + String authCode = ''; + const String authCodePrefix = 'authCode='; + String raw = txt.first.text; + // TXT has a format of [, text], so if the length is 2, + // that means that TXT is empty. + if (raw.length > 2) { + // Remove length byte from raw txt. + raw = raw.substring(1); + if (raw.startsWith(authCodePrefix)) { + authCode = raw.substring(authCodePrefix.length); + // The Observatory currently expects a trailing '/' as part of the + // URI, otherwise an invalid authentication code response is given. + if (!authCode.endsWith('/')) { + authCode += '/'; + } + } + } + return MDnsObservatoryDiscoveryResult(srv.first.port, authCode); + } finally { + client.stop(); + } + } + + Future getObservatoryUri(String applicationId, Device device, [bool usesIpv6 = false, int observatoryPort]) async { + final MDnsObservatoryDiscoveryResult result = await query(applicationId: applicationId); + Uri observatoryUri; + if (result != null) { + final String host = usesIpv6 + ? InternetAddress.loopbackIPv6.address + : InternetAddress.loopbackIPv4.address; + observatoryUri = await buildObservatoryUri(device, host, result.port, observatoryPort, result.authCode); + } + return observatoryUri; + } +} + +class MDnsObservatoryDiscoveryResult { + MDnsObservatoryDiscoveryResult(this.port, this.authCode); + final int port; + final String authCode; +} + +Future buildObservatoryUri(Device device, + String host, int devicePort, [int observatoryPort, String authCode]) async { + String path = '/'; + if (authCode != null) { + path = authCode; + } + // Not having a trailing slash can cause problems in some situations. + // Ensure that there's one present. + if (!path.endsWith('/')) { + path += '/'; + } + final int localPort = observatoryPort + ?? await device.portForwarder.forward(devicePort); + return Uri(scheme: 'http', host: host, port: localPort, path: path); +} diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 8184e641307..58a2692e17b 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: json_rpc_2: 2.1.0 linter: 0.1.93 meta: 1.1.7 - multicast_dns: 0.2.0 + multicast_dns: 0.2.1 mustache: 1.1.1 package_config: 1.0.5 platform: 2.2.1 @@ -135,4 +135,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: 3394 +# PUBSPEC CHECKSUM: 7d95 diff --git a/packages/flutter_tools/test/general.shard/commands/attach_test.dart b/packages/flutter_tools/test/general.shard/commands/attach_test.dart index 23a1a478b13..38cd4b44462 100644 --- a/packages/flutter_tools/test/general.shard/commands/attach_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/attach_test.dart @@ -13,11 +13,12 @@ import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/attach.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/ios/devices.dart'; +import 'package:flutter_tools/src/mdns_discovery.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:meta/meta.dart'; import 'package:mockito/mockito.dart'; -import 'package:multicast_dns/multicast_dns.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -263,7 +264,7 @@ void main() { when(portForwarder.unforward(any)) .thenAnswer((_) async => null); when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'))) - .thenAnswer((_) async => 0); + .thenAnswer((_) async => 0); when(mockHotRunnerFactory.build( any, target: anyNamed('target'), @@ -470,137 +471,13 @@ void main() { FileSystem: () => testFileSystem, }); }); - - group('mDNS Discovery', () { - final int year3000 = DateTime(3000).millisecondsSinceEpoch; - - MDnsClient getMockClient( - List ptrRecords, - Map> srvResponse, - ) { - final MDnsClient client = MockMDnsClient(); - - when(client.lookup( - ResourceRecordQuery.serverPointer(MDnsObservatoryDiscovery.dartObservatoryName), - )).thenAnswer((_) => Stream.fromIterable(ptrRecords)); - - for (final MapEntry> entry in srvResponse.entries) { - when(client.lookup( - ResourceRecordQuery.service(entry.key), - )).thenAnswer((_) => Stream.fromIterable(entry.value)); - } - return client; - } - - testUsingContext('No ports available', () async { - final MDnsClient client = getMockClient([], >{}); - - final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); - final int port = (await portDiscovery.query())?.port; - expect(port, isNull); - }); - - testUsingContext('One port available, no appId', () async { - final MDnsClient client = getMockClient( - [ - PtrResourceRecord('foo', year3000, domainName: 'bar'), - ], - >{ - 'bar': [ - SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), - ], - }, - ); - - final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); - final int port = (await portDiscovery.query())?.port; - expect(port, 123); - }); - - testUsingContext('Multiple ports available, without appId', () async { - final MDnsClient client = getMockClient( - [ - PtrResourceRecord('foo', year3000, domainName: 'bar'), - PtrResourceRecord('baz', year3000, domainName: 'fiz'), - ], - >{ - 'bar': [ - SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), - ], - 'fiz': [ - SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), - ], - }, - ); - - final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); - expect(() => portDiscovery.query(), throwsToolExit()); - }); - - testUsingContext('Multiple ports available, with appId', () async { - final MDnsClient client = getMockClient( - [ - PtrResourceRecord('foo', year3000, domainName: 'bar'), - PtrResourceRecord('baz', year3000, domainName: 'fiz'), - ], - >{ - 'bar': [ - SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), - ], - 'fiz': [ - SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), - ], - }, - ); - - final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); - final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port; - expect(port, 321); - }); - - testUsingContext('Multiple ports available per process, with appId', () async { - final MDnsClient client = getMockClient( - [ - PtrResourceRecord('foo', year3000, domainName: 'bar'), - PtrResourceRecord('baz', year3000, domainName: 'fiz'), - ], - >{ - 'bar': [ - SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'), - SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), - ], - 'fiz': [ - SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'), - SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), - ], - }, - ); - - final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); - final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; - expect(port, 1234); - }); - - testUsingContext('Query returns null', () async { - final MDnsClient client = getMockClient( - [], - >{}, - ); - - final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); - final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; - expect(port, isNull); - }); - }); } -class MockMDnsClient extends Mock implements MDnsClient {} - -class MockPortForwarder extends Mock implements DevicePortForwarder {} - class MockHotRunner extends Mock implements HotRunner {} - class MockHotRunnerFactory extends Mock implements HotRunnerFactory {} +class MockIOSDevice extends Mock implements IOSDevice {} +class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {} +class MockPortForwarder extends Mock implements DevicePortForwarder {} class StreamLogger extends Logger { @override diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 3e31c32c717..dba51cf17d2 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -16,6 +16,7 @@ import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; +import 'package:flutter_tools/src/mdns_discovery.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -31,6 +32,7 @@ class MockCache extends Mock implements Cache {} class MockDirectory extends Mock implements Directory {} class MockFileSystem extends Mock implements FileSystem {} class MockIMobileDevice extends Mock implements IMobileDevice {} +class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {} class MockXcode extends Mock implements Xcode {} class MockFile extends Mock implements File {} class MockPortForwarder extends Mock implements DevicePortForwarder {} @@ -70,6 +72,7 @@ void main() { MockFileSystem mockFileSystem; MockProcessManager mockProcessManager; MockDeviceLogReader mockLogReader; + MockMDnsObservatoryDiscovery mockMDnsObservatoryDiscovery; MockPortForwarder mockPortForwarder; const int devicePort = 499; @@ -91,6 +94,7 @@ void main() { mockCache = MockCache(); when(mockCache.dyLdLibEntry).thenReturn(libraryEntry); mockFileSystem = MockFileSystem(); + mockMDnsObservatoryDiscovery = MockMDnsObservatoryDiscovery(); mockProcessManager = MockProcessManager(); mockLogReader = MockDeviceLogReader(); mockPortForwarder = MockPortForwarder(); @@ -132,16 +136,18 @@ void main() { mockLogReader.dispose(); }); - testUsingContext(' succeeds in debug mode', () async { + testUsingContext(' succeeds in debug mode via mDNS', () async { final IOSDevice device = IOSDevice('123'); device.portForwarder = mockPortForwarder; device.setLogReader(mockApp, mockLogReader); - - // Now that the reader is used, start writing messages to it. - Timer.run(() { - mockLogReader.addLine('Foo'); - mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort'); - }); + final Uri uri = Uri( + scheme: 'http', + host: '127.0.0.1', + port: 1234, + path: 'observatory', + ); + when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any)) + .thenAnswer((Invocation invocation) => Future.value(uri)); final LaunchResult launchResult = await device.startApp(mockApp, prebuiltApplication: true, @@ -155,6 +161,65 @@ void main() { Artifacts: () => mockArtifacts, Cache: () => mockCache, FileSystem: () => mockFileSystem, + MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery, + Platform: () => macPlatform, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext(' succeeds in debug mode when mDNS fails by falling back to manual protocol discovery', () async { + final IOSDevice device = IOSDevice('123'); + device.portForwarder = mockPortForwarder; + device.setLogReader(mockApp, mockLogReader); + // Now that the reader is used, start writing messages to it. + Timer.run(() { + mockLogReader.addLine('Foo'); + mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort'); + }); + when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any)) + .thenAnswer((Invocation invocation) => Future.value(null)); + + final LaunchResult launchResult = await device.startApp(mockApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)), + platformArgs: {}, + ); + expect(launchResult.started, isTrue); + expect(launchResult.hasObservatory, isTrue); + expect(await device.stopApp(mockApp), isFalse); + }, overrides: { + Artifacts: () => mockArtifacts, + Cache: () => mockCache, + FileSystem: () => mockFileSystem, + MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery, + Platform: () => macPlatform, + ProcessManager: () => mockProcessManager, + }); + + testUsingContext(' fails in debug mode when mDNS fails and when Observatory URI is malformed', () async { + final IOSDevice device = IOSDevice('123'); + device.portForwarder = mockPortForwarder; + device.setLogReader(mockApp, mockLogReader); + + // Now that the reader is used, start writing messages to it. + Timer.run(() { + mockLogReader.addLine('Foo'); + mockLogReader.addLine('Observatory listening on http:/:/127.0.0.1:$devicePort'); + }); + when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any)) + .thenAnswer((Invocation invocation) => Future.value(null)); + + final LaunchResult launchResult = await device.startApp(mockApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)), + platformArgs: {}, + ); + expect(launchResult.started, isFalse); + expect(launchResult.hasObservatory, isFalse); + }, overrides: { + Artifacts: () => mockArtifacts, + Cache: () => mockCache, + FileSystem: () => mockFileSystem, + MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery, Platform: () => macPlatform, ProcessManager: () => mockProcessManager, }); @@ -176,32 +241,6 @@ void main() { Platform: () => macPlatform, ProcessManager: () => mockProcessManager, }); - - testUsingContext(' fails in debug mode when Observatory URI is malformed', () async { - final IOSDevice device = IOSDevice('123'); - device.portForwarder = mockPortForwarder; - device.setLogReader(mockApp, mockLogReader); - - // Now that the reader is used, start writing messages to it. - Timer.run(() { - mockLogReader.addLine('Foo'); - mockLogReader.addLine('Observatory listening on http:/:/127.0.0.1:$devicePort'); - }); - - final LaunchResult launchResult = await device.startApp(mockApp, - prebuiltApplication: true, - debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)), - platformArgs: {}, - ); - expect(launchResult.started, isFalse); - expect(launchResult.hasObservatory, isFalse); - }, overrides: { - Artifacts: () => mockArtifacts, - Cache: () => mockCache, - FileSystem: () => mockFileSystem, - Platform: () => macPlatform, - ProcessManager: () => mockProcessManager, - }); }); group('Process calls', () { diff --git a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart new file mode 100644 index 00000000000..a56b12e462c --- /dev/null +++ b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart @@ -0,0 +1,138 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_tools/src/mdns_discovery.dart'; +import 'package:mockito/mockito.dart'; +import 'package:multicast_dns/multicast_dns.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; + +void main() { + group('mDNS Discovery', () { + final int year3000 = DateTime(3000).millisecondsSinceEpoch; + + MDnsClient getMockClient( + List ptrRecords, + Map> srvResponse, + ) { + final MDnsClient client = MockMDnsClient(); + + when(client.lookup( + ResourceRecordQuery.serverPointer(MDnsObservatoryDiscovery.dartObservatoryName), + )).thenAnswer((_) => Stream.fromIterable(ptrRecords)); + + for (final MapEntry> entry in srvResponse.entries) { + when(client.lookup( + ResourceRecordQuery.service(entry.key), + )).thenAnswer((_) => Stream.fromIterable(entry.value)); + } + return client; + } + + testUsingContext('No ports available', () async { + final MDnsClient client = getMockClient([], >{}); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query())?.port; + expect(port, isNull); + }); + + testUsingContext('One port available, no appId', () async { + final MDnsClient client = getMockClient( + [ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + ], + >{ + 'bar': [ + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query())?.port; + expect(port, 123); + }); + + testUsingContext('Multiple ports available, without appId', () async { + final MDnsClient client = getMockClient( + [ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + PtrResourceRecord('baz', year3000, domainName: 'fiz'), + ], + >{ + 'bar': [ + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + 'fiz': [ + SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + expect(() => portDiscovery.query(), throwsToolExit()); + }); + + testUsingContext('Multiple ports available, with appId', () async { + final MDnsClient client = getMockClient( + [ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + PtrResourceRecord('baz', year3000, domainName: 'fiz'), + ], + >{ + 'bar': [ + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + 'fiz': [ + SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port; + expect(port, 321); + }); + + testUsingContext('Multiple ports available per process, with appId', () async { + final MDnsClient client = getMockClient( + [ + PtrResourceRecord('foo', year3000, domainName: 'bar'), + PtrResourceRecord('baz', year3000, domainName: 'fiz'), + ], + >{ + 'bar': [ + SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'), + SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + 'fiz': [ + SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'), + SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), + ], + }, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; + expect(port, 1234); + }); + + testUsingContext('Query returns null', () async { + final MDnsClient client = getMockClient( + [], + >{}, + ); + + final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client); + final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; + expect(port, isNull); + }); + }); +} + +class MockMDnsClient extends Mock implements MDnsClient {}