blob: a1d0717e1d2f639f06f1589a1b11997b84a97e51 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:meta/meta.dart';
import 'package:multicast_dns/multicast_dns.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'build_info.dart';
import 'device.dart';
import 'reporting/reporting.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,
required Logger logger,
required Usage flutterUsage,
}): _client = mdnsClient ?? MDnsClient(),
_logger = logger,
_flutterUsage = flutterUsage;
final MDnsClient _client;
final Logger _logger;
final Usage _flutterUsage;
@visibleForTesting
static const String dartObservatoryName = '_dartobservatory._tcp.local';
static MDnsObservatoryDiscovery? get instance => context.get<MDnsObservatoryDiscovery>();
/// 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.
@visibleForTesting
Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async {
_logger.printTrace('Checking for advertised Dart observatories...');
try {
await _client.start();
final List<PtrResourceRecord> pointerRecords = await _client
.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(dartObservatoryName),
)
.toList();
if (pointerRecords.isEmpty) {
_logger.printTrace('No pointer records found.');
return null;
}
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final Set<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet();
String? domainName;
if (applicationId != null) {
for (final 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;
}
_logger.printTrace('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) {
_logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
}
_logger.printTrace('Checking for authentication code for $domainName');
final List<TxtResourceRecord> txt = await _client
.lookup<TxtResourceRecord>(
ResourceRecordQuery.text(domainName),
)
.toList();
if (txt == null || txt.isEmpty) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
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) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
String 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<Uri?> getObservatoryUri(String? applicationId, Device device, {
bool usesIpv6 = false,
int? hostVmservicePort,
int? deviceVmservicePort,
}) async {
final MDnsObservatoryDiscoveryResult? result = await query(
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
);
if (result == null) {
await _checkForIPv4LinkLocal(device);
return null;
}
final String host = usesIpv6
? InternetAddress.loopbackIPv6.address
: InternetAddress.loopbackIPv4.address;
return buildObservatoryUri(
device,
host,
result.port,
hostVmservicePort,
result.authCode,
);
}
// If there's not an ipv4 link local address in `NetworkInterfaces.list`,
// then request user interventions with a `printError()` if possible.
Future<void> _checkForIPv4LinkLocal(Device device) async {
_logger.printTrace(
'mDNS query failed. Checking for an interface with a ipv4 link local address.'
);
final List<NetworkInterface> interfaces = await listNetworkInterfaces(
includeLinkLocal: true,
type: InternetAddressType.IPv4,
);
if (_logger.isVerbose) {
_logInterfaces(interfaces);
}
final bool hasIPv4LinkLocal = interfaces.any(
(NetworkInterface interface) => interface.addresses.any(
(InternetAddress address) => address.isLinkLocal,
),
);
if (hasIPv4LinkLocal) {
_logger.printTrace('An interface with an ipv4 link local address was found.');
return;
}
final TargetPlatform targetPlatform = await device.targetPlatform;
switch (targetPlatform) {
case TargetPlatform.ios:
UsageEvent('ios-mdns', 'no-ipv4-link-local', flutterUsage: _flutterUsage).send();
_logger.printError(
'The mDNS query for an attached iOS device failed. It may '
'be necessary to disable the "Personal Hotspot" on the device, and '
'to ensure that the "Disable unless needed" setting is unchecked '
'under System Preferences > Network > iPhone USB. '
'See https://github.com/flutter/flutter/issues/46698 for details.'
);
break;
case TargetPlatform.android:
case TargetPlatform.android_arm:
case TargetPlatform.android_arm64:
case TargetPlatform.android_x64:
case TargetPlatform.android_x86:
case TargetPlatform.darwin:
case TargetPlatform.fuchsia_arm64:
case TargetPlatform.fuchsia_x64:
case TargetPlatform.linux_arm64:
case TargetPlatform.linux_x64:
case TargetPlatform.tester:
case TargetPlatform.web_javascript:
case TargetPlatform.windows_x64:
_logger.printTrace('No interface with an ipv4 link local address was found.');
break;
}
}
void _logInterfaces(List<NetworkInterface> interfaces) {
for (final NetworkInterface interface in interfaces) {
if (_logger.isVerbose) {
_logger.printTrace('Found interface "${interface.name}":');
for (final InternetAddress address in interface.addresses) {
final String linkLocal = address.isLinkLocal ? 'link local' : '';
_logger.printTrace('\tBound address: "${address.address}" $linkLocal');
}
}
}
}
}
class MDnsObservatoryDiscoveryResult {
MDnsObservatoryDiscoveryResult(this.port, this.authCode);
final int port;
final String authCode;
}
Future<Uri> buildObservatoryUri(
Device device,
String host,
int devicePort, [
int? hostVmservicePort,
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 += '/';
}
hostVmservicePort ??= 0;
final int? actualHostPort = hostVmservicePort == 0 ?
await device.portForwarder?.forward(devicePort) :
hostVmservicePort;
return Uri(scheme: 'http', host: host, port: actualHostPort, path: path);
}