diff --git a/packages/flutter_tools/lib/src/mdns_discovery.dart b/packages/flutter_tools/lib/src/mdns_discovery.dart index 4bb537854e..c4b1f8f421 100644 --- a/packages/flutter_tools/lib/src/mdns_discovery.dart +++ b/packages/flutter_tools/lib/src/mdns_discovery.dart @@ -10,6 +10,7 @@ import 'base/context.dart'; import 'base/io.dart'; import 'base/logger.dart'; import 'build_info.dart'; +import 'convert.dart'; import 'device.dart'; import 'reporting/reporting.dart'; @@ -201,7 +202,16 @@ class MDnsVmServiceDiscovery { final List results = []; + + // uniqueDomainNames is used to track all domain names of Dart VM services + // It is later used in this function to determine whether or not to throw an error. + // We do not want to throw the error if it was unable to find any domain + // names because that indicates it may be a problem with mDNS, which has + // a separate error message in _checkForIPv4LinkLocal. final Set uniqueDomainNames = {}; + // uniqueDomainNamesInResults is used to filter out duplicates with exactly + // the same domain name from the results. + final Set uniqueDomainNamesInResults = {}; // Listen for mDNS connections until timeout. final Stream ptrResourceStream = client.lookup( @@ -223,6 +233,11 @@ class MDnsVmServiceDiscovery { domainName = ptr.domainName; } + // Result with same domain name was already found, skip it. + if (uniqueDomainNamesInResults.contains(domainName)) { + continue; + } + _logger.printTrace('Checking for available port on $domainName'); final List srvRecords = await client .lookup( @@ -279,41 +294,18 @@ class MDnsVmServiceDiscovery { ResourceRecordQuery.text(domainName), ) .toList(); - if (txt.isEmpty) { - results.add(MDnsVmServiceDiscoveryResult(domainName, srvRecord.port, '')); - if (quitOnFind) { - return results; - } - continue; - } - const String authCodePrefix = 'authCode='; - String? raw; - for (final String record in txt.first.text.split('\n')) { - if (record.startsWith(authCodePrefix)) { - raw = record; - break; - } - } - if (raw == null) { - results.add(MDnsVmServiceDiscoveryResult(domainName, srvRecord.port, '')); - if (quitOnFind) { - return results; - } - continue; - } - String authCode = raw.substring(authCodePrefix.length); - // The Dart VM Service currently expects a trailing '/' as part of the - // URI, otherwise an invalid authentication code response is given. - if (!authCode.endsWith('/')) { - authCode += '/'; - } + String authCode = ''; + if (txt.isNotEmpty) { + authCode = _getAuthCode(txt.first.text); + } results.add(MDnsVmServiceDiscoveryResult( domainName, srvRecord.port, authCode, ipAddress: ipAddress )); + uniqueDomainNamesInResults.add(domainName); if (quitOnFind) { return results; } @@ -338,6 +330,22 @@ class MDnsVmServiceDiscovery { } } + String _getAuthCode(String txtRecord) { + const String authCodePrefix = 'authCode='; + final Iterable matchingRecords = + LineSplitter.split(txtRecord).where((String record) => record.startsWith(authCodePrefix)); + if (matchingRecords.isEmpty) { + return ''; + } + String authCode = matchingRecords.first.substring(authCodePrefix.length); + // The Dart VM Service currently expects a trailing '/' as part of the + // URI, otherwise an invalid authentication code response is given. + if (!authCode.endsWith('/')) { + authCode += '/'; + } + return authCode; + } + /// Gets Dart VM Service Uri for `flutter attach`. /// Executes an mDNS query and waits until a Dart VM Service is found. /// diff --git a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart index 8bba83fa5d..6d9a10f82b 100644 --- a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart +++ b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart @@ -112,6 +112,56 @@ void main() { expect(portDiscovery.queryForAttach, throwsToolExit()); }); + testWithoutContext('Find duplicates in preliminary client', () async { + final MDnsClient client = FakeMDnsClient( + [ + PtrResourceRecord('foo', future, domainName: 'bar'), + PtrResourceRecord('foo', future, domainName: 'bar'), + ], + >{ + 'bar': [ + 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('Find similar named in preliminary client', () async { + final MDnsClient client = FakeMDnsClient( + [ + PtrResourceRecord('foo', future, domainName: 'bar'), + PtrResourceRecord('foo', future, domainName: 'bar (2)'), + ], + >{ + 'bar': [ + SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + 'bar (2)': [ + SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), + ], + }, + ); + + 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, @@ -680,6 +730,112 @@ void main() { expect(result?.domainName, 'srv-bar'); expect(result?.port, 222); }); + testWithoutContext('find with no txt record', () async { + final MDnsClient client = FakeMDnsClient( + [ + PtrResourceRecord('foo', future, domainName: 'srv-foo'), + ], + >{ + 'srv-foo': [ + SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'), + ], + }, + ipResponse: >{ + 'target-foo': [ + IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!), + ], + }, + ); + + final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( + mdnsClient: client, + logger: BufferLogger.test(), + flutterUsage: TestUsage(), + ); + final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( + client, + applicationId: 'srv-foo', + isNetworkDevice: true, + ); + expect(result?.domainName, 'srv-foo'); + expect(result?.port, 111); + expect(result?.authCode, ''); + expect(result?.ipAddress?.address, '111.111.111.111'); + }); + testWithoutContext('find with empty txt record', () async { + final MDnsClient client = FakeMDnsClient( + [ + PtrResourceRecord('foo', future, domainName: 'srv-foo'), + ], + >{ + 'srv-foo': [ + SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'), + ], + }, + txtResponse: >{ + 'srv-foo': [ + TxtResourceRecord('srv-foo', future, text: ''), + ], + }, + ipResponse: >{ + 'target-foo': [ + IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!), + ], + }, + ); + + final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( + mdnsClient: client, + logger: BufferLogger.test(), + flutterUsage: TestUsage(), + ); + final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( + client, + applicationId: 'srv-foo', + isNetworkDevice: true, + ); + expect(result?.domainName, 'srv-foo'); + expect(result?.port, 111); + expect(result?.authCode, ''); + expect(result?.ipAddress?.address, '111.111.111.111'); + }); + testWithoutContext('find with valid txt record', () async { + final MDnsClient client = FakeMDnsClient( + [ + PtrResourceRecord('foo', future, domainName: 'srv-foo'), + ], + >{ + 'srv-foo': [ + SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'), + ], + }, + txtResponse: >{ + 'srv-foo': [ + TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'), + ], + }, + ipResponse: >{ + 'target-foo': [ + IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!), + ], + }, + ); + + final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( + mdnsClient: client, + logger: BufferLogger.test(), + flutterUsage: TestUsage(), + ); + final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( + client, + applicationId: 'srv-foo', + isNetworkDevice: true, + ); + expect(result?.domainName, 'srv-foo'); + expect(result?.port, 111); + expect(result?.authCode, 'xyz/'); + expect(result?.ipAddress?.address, '111.111.111.111'); + }); }); }