Unverified Commit f9e6242d authored by Dan Field's avatar Dan Field Committed by GitHub

Use mDNS to discover the device port (#26944)

* Discover port over mDNS

* opt in, only for iOS for now
parent 06b979c4
......@@ -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<void> validateCommand() async {
await super.validateCommand();
......@@ -114,6 +127,9 @@ class AttachCommand extends FlutterCommand {
@override
Future<FlutterCommandResult> 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<int> 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,
......@@ -273,3 +301,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<int> queryForPort({String applicationId}) async {
printStatus('Checking for advertised Dart observatories...');
try {
await client.start();
final List<PtrResourceRecord> pointerRecords = await client
.lookup<PtrResourceRecord>(
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<String> uniqueDomainNames = pointerRecords
.map<String>((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<SrvResourceRecord> srv = await client
.lookup<SrvResourceRecord>(
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();
}
}
}
......@@ -23,6 +23,7 @@ dependencies:
json_schema: 1.0.10
linter: 0.1.74
meta: 1.1.6
multicast_dns: 0.1.0+1
mustache: 1.1.0
package_config: 1.0.5
platform: 2.2.0
......@@ -95,4 +96,4 @@ dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: 3fcc
# PUBSPEC CHECKSUM: 78f0
......@@ -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<PtrResourceRecord> ptrRecords,
Map<String, List<SrvResourceRecord>> srvResponse,
) {
final MDnsClient client = MockMDnsClient();
when(client.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(MDnsObservatoryPortDiscovery.dartObservatoryName),
)).thenAnswer((_) => Stream<PtrResourceRecord>.fromIterable(ptrRecords));
for (final MapEntry<String, List<SrvResourceRecord>> entry in srvResponse.entries) {
when(client.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(entry.key),
)).thenAnswer((_) => Stream<SrvResourceRecord>.fromIterable(entry.value));
}
return client;
}
testUsingContext('No ports available', () async {
final MDnsClient client = getMockClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
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>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
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>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
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>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
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>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 1234, 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 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 {}
......
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