mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
Reland automatic discovery of observatory port for iOS (#27908)
* Discover port over mDNS * Update BUILD.gn for tools
This commit is contained in:
parent
7390cc5cdb
commit
c90c3a18a2
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
@ -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<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 @@ import '../runner/flutter_command.dart';
|
|||
const Map<String, String> _kManuallyPinnedDependencies = <String, String>{
|
||||
// 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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {}
|
||||
|
|
Loading…
Reference in a new issue