diff --git a/packages/flutter_tools/BUILD.gn b/packages/flutter_tools/BUILD.gn index 6ba47615f1..8221c0dae2 100644 --- a/packages/flutter_tools/BUILD.gn +++ b/packages/flutter_tools/BUILD.gn @@ -30,6 +30,7 @@ dart_library("flutter_tools") { "//third_party/dart-pkg/pub/json_rpc_2", "//third_party/dart-pkg/pub/json_schema", "//third_party/dart-pkg/pub/meta", + "//third_party/dart-pkg/pub/multicast_dns", "//third_party/dart-pkg/pub/mustache", "//third_party/dart-pkg/pub/package_config", "//third_party/dart-pkg/pub/path", diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 6a354e53b2..81cfd2b20a 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -4,6 +4,8 @@ import 'dart:async'; +import 'package:multicast_dns/multicast_dns.dart'; + import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; @@ -14,16 +16,14 @@ import '../compile.dart'; import '../device.dart'; import '../fuchsia/fuchsia_device.dart'; import '../globals.dart'; +import '../ios/devices.dart'; +import '../ios/simulators.dart'; import '../protocol_discovery.dart'; import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; -final String ipv4Loopback = InternetAddress.loopbackIPv4.address; - -final String ipv6Loopback = InternetAddress.loopbackIPv6.address; - /// A Flutter-command that attaches to applications that have been launched /// without `flutter run`. /// @@ -56,7 +56,16 @@ class AttachCommand extends FlutterCommand { ..addOption( 'debug-port', help: 'Device port where the observatory is listening.', - )..addOption('pid-file', + )..addOption( + 'app-id', + help: 'The package name (Android) or bundle identifier (iOS) for the application. ' + 'This can be specified to avoid being prompted if multiple observatory ports ' + 'are advertised.\n' + 'If you have multiple devices or emulators running, you should include the ' + 'device hostname as well, e.g. "com.example.myApp@my-iphone".\n' + 'This parameter is case-insensitive.', + )..addOption( + 'pid-file', help: 'Specify a file to write the process id to. ' 'You can send SIGUSR1 to trigger a hot reload ' 'and SIGUSR2 to trigger a hot restart.', @@ -92,6 +101,10 @@ class AttachCommand extends FlutterCommand { return null; } + String get appId { + return argResults['app-id']; + } + @override Future validateCommand() async { await super.validateCommand(); @@ -114,6 +127,9 @@ class AttachCommand extends FlutterCommand { @override Future runCommand() async { + final String ipv4Loopback = InternetAddress.loopbackIPv4.address; + final String ipv6Loopback = InternetAddress.loopbackIPv6.address; + Cache.releaseLockEarly(); await _validateArguments(); @@ -121,7 +137,19 @@ class AttachCommand extends FlutterCommand { writePidFile(argResults['pid-file']); final Device device = await findTargetDevice(); - final int devicePort = debugPort; + Future getDevicePort() async { + if (debugPort != null) { + return debugPort; + } + // This call takes a non-trivial amount of time, and only iOS devices and + // simulators support it. + // If/when we do this on Android or other platforms, we can update it here. + if (device is IOSDevice || device is IOSSimulator) { + return MDnsObservatoryPortDiscovery().queryForPort(applicationId: appId); + } + return null; + } + final int devicePort = await getDevicePort(); final Daemon daemon = argResults['machine'] ? Daemon(stdinCommandStream, stdoutCommandResponse, @@ -274,3 +302,99 @@ class HotRunnerFactory { ipv6: ipv6, ); } + +/// A wrapper around [MDnsClient] to find a Dart observatory port. +class MDnsObservatoryPortDiscovery { + /// Creates a new [MDnsObservatoryPortDiscovery] 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. + MDnsObservatoryPortDiscovery({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 port. + /// + /// 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 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 port, it will return that + /// port regardless of what application the port is for. + Future queryForPort({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}).'); + } + return srv.first.port; + } finally { + client.stop(); + } + } +} diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart index 5f7c5d8738..6eb5e43e1a 100644 --- a/packages/flutter_tools/lib/src/commands/update_packages.dart +++ b/packages/flutter_tools/lib/src/commands/update_packages.dart @@ -23,6 +23,7 @@ import '../runner/flutter_command.dart'; const Map _kManuallyPinnedDependencies = { // Add pinned packages here. 'flutter_gallery_assets': '0.1.6', // See //examples/flutter_gallery/pubspec.yaml + 'json_schema': '1.0.10', }; class UpdatePackagesCommand extends FlutterCommand { diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 35ec21c0ce..7fff92e8e4 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: json_schema: 1.0.10 linter: 0.1.82 meta: 1.1.6 + multicast_dns: 0.1.0+1 mustache: 1.1.1 package_config: 1.0.5 platform: 2.2.0 @@ -116,4 +117,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: 6c3e +# PUBSPEC CHECKSUM: 6362 diff --git a/packages/flutter_tools/test/commands/attach_test.dart b/packages/flutter_tools/test/commands/attach_test.dart index 68de5038cb..f2a7020b60 100644 --- a/packages/flutter_tools/test/commands/attach_test.dart +++ b/packages/flutter_tools/test/commands/attach_test.dart @@ -17,6 +17,7 @@ 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'; @@ -422,8 +423,121 @@ 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(MDnsObservatoryPortDiscovery.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 MDnsObservatoryPortDiscovery portDiscovery = MDnsObservatoryPortDiscovery(mdnsClient: client); + final int port = await portDiscovery.queryForPort(); + 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 MDnsObservatoryPortDiscovery portDiscovery = MDnsObservatoryPortDiscovery(mdnsClient: client); + final int port = await portDiscovery.queryForPort(); + 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 MDnsObservatoryPortDiscovery portDiscovery = MDnsObservatoryPortDiscovery(mdnsClient: client); + expect(() => portDiscovery.queryForPort(), 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 MDnsObservatoryPortDiscovery portDiscovery = MDnsObservatoryPortDiscovery(mdnsClient: client); + final int port = await portDiscovery.queryForPort(applicationId: 'fiz'); + 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 MDnsObservatoryPortDiscovery portDiscovery = MDnsObservatoryPortDiscovery(mdnsClient: client); + final int port = await portDiscovery.queryForPort(applicationId: 'bar'); + expect(port, 1234); + }); + }); } +class MockMDnsClient extends Mock implements MDnsClient {} + class MockPortForwarder extends Mock implements DevicePortForwarder {} class MockHotRunner extends Mock implements HotRunner {}