blob: 695ccb8a1fe240a08f7b418e2839e734570a2445 [file] [log] [blame]
Ian Hickson449f4a62019-11-27 15:04:02 -08001// Copyright 2014 The Flutter Authors. All rights reserved.
Christopher Fujino0b24a5a2019-09-18 11:01:08 -07002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Christopher Fujino0b24a5a2019-09-18 11:01:08 -07005import 'package:meta/meta.dart';
6import 'package:multicast_dns/multicast_dns.dart';
7
8import 'base/common.dart';
9import 'base/context.dart';
10import 'base/io.dart';
Jonah Williams8436c6a2020-11-13 13:23:03 -080011import 'base/logger.dart';
Zachary Andersona72cca12019-12-16 14:57:29 -080012import 'build_info.dart';
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070013import 'device.dart';
Zachary Andersona72cca12019-12-16 14:57:29 -080014import 'reporting/reporting.dart';
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070015
16/// A wrapper around [MDnsClient] to find a Dart observatory instance.
17class MDnsObservatoryDiscovery {
18 /// Creates a new [MDnsObservatoryDiscovery] object.
19 ///
Jonah Williams8436c6a2020-11-13 13:23:03 -080020 /// The [_client] parameter will be defaulted to a new [MDnsClient] if null.
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070021 /// The [applicationId] parameter may be null, and can be used to
22 /// automatically select which application to use if multiple are advertising
23 /// Dart observatory ports.
Jonah Williams8436c6a2020-11-13 13:23:03 -080024 MDnsObservatoryDiscovery({
25 MDnsClient mdnsClient,
26 @required Logger logger,
27 @required Usage flutterUsage,
28 }): _client = mdnsClient ?? MDnsClient(),
29 _logger = logger,
30 _flutterUsage = flutterUsage;
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070031
Jonah Williams8436c6a2020-11-13 13:23:03 -080032 final MDnsClient _client;
33 final Logger _logger;
34 final Usage _flutterUsage;
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070035
36 @visibleForTesting
37 static const String dartObservatoryName = '_dartobservatory._tcp.local';
38
39 static MDnsObservatoryDiscovery get instance => context.get<MDnsObservatoryDiscovery>();
40
41 /// Executes an mDNS query for a Dart Observatory.
42 ///
43 /// The [applicationId] parameter may be used to specify which application
44 /// to find. For Android, it refers to the package name; on iOS, it refers to
45 /// the bundle ID.
46 ///
47 /// If it is not null, this method will find the port and authentication code
48 /// of the Dart Observatory for that application. If it cannot find a Dart
49 /// Observatory matching that application identifier, it will call
50 /// [throwToolExit].
51 ///
52 /// If it is null and there are multiple ports available, the user will be
53 /// prompted with a list of available observatory ports and asked to select
54 /// one.
55 ///
56 /// If it is null and there is only one available instance of Observatory,
57 /// it will return that instance's information regardless of what application
58 /// the Observatory instance is for.
Jonah Williamse3cb2c32019-11-13 16:02:46 -080059 // TODO(jonahwilliams): use `deviceVmservicePort` to filter mdns results.
Jenn Magderf8b1de32020-10-07 08:52:05 -070060 @visibleForTesting
Jonah Williamse3cb2c32019-11-13 16:02:46 -080061 Future<MDnsObservatoryDiscoveryResult> query({String applicationId, int deviceVmservicePort}) async {
Jonah Williams8436c6a2020-11-13 13:23:03 -080062 _logger.printTrace('Checking for advertised Dart observatories...');
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070063 try {
Jonah Williams8436c6a2020-11-13 13:23:03 -080064 await _client.start();
65 final List<PtrResourceRecord> pointerRecords = await _client
Zachary Anderson0cd2ece2020-03-12 14:01:01 -070066 .lookup<PtrResourceRecord>(
67 ResourceRecordQuery.serverPointer(dartObservatoryName),
68 )
69 .toList();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070070 if (pointerRecords.isEmpty) {
Jonah Williams8436c6a2020-11-13 13:23:03 -080071 _logger.printTrace('No pointer records found.');
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070072 return null;
73 }
74 // We have no guarantee that we won't get multiple hits from the same
75 // service on this.
Jonah Williams69ecca52020-02-06 13:58:03 -080076 final Set<String> uniqueDomainNames = pointerRecords
Zachary Anderson0cd2ece2020-03-12 14:01:01 -070077 .map<String>((PtrResourceRecord record) => record.domainName)
78 .toSet();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070079
80 String domainName;
81 if (applicationId != null) {
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +010082 for (final String name in uniqueDomainNames) {
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070083 if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
84 domainName = name;
85 break;
86 }
87 }
88 if (domainName == null) {
89 throwToolExit('Did not find a observatory port advertised for $applicationId.');
90 }
91 } else if (uniqueDomainNames.length > 1) {
92 final StringBuffer buffer = StringBuffer();
93 buffer.writeln('There are multiple observatory ports available.');
94 buffer.writeln('Rerun this command with one of the following passed in as the appId:');
95 buffer.writeln('');
Alexandre Ardhuinb8731622019-09-24 21:03:37 +020096 for (final String uniqueDomainName in uniqueDomainNames) {
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070097 buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
98 }
99 throwToolExit(buffer.toString());
100 } else {
101 domainName = pointerRecords[0].domainName;
102 }
Jonah Williams8436c6a2020-11-13 13:23:03 -0800103 _logger.printTrace('Checking for available port on $domainName');
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700104 // Here, if we get more than one, it should just be a duplicate.
Jonah Williams8436c6a2020-11-13 13:23:03 -0800105 final List<SrvResourceRecord> srv = await _client
Zachary Anderson0cd2ece2020-03-12 14:01:01 -0700106 .lookup<SrvResourceRecord>(
107 ResourceRecordQuery.service(domainName),
108 )
109 .toList();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700110 if (srv.isEmpty) {
111 return null;
112 }
113 if (srv.length > 1) {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800114 _logger.printError('Unexpectedly found more than one observatory report for $domainName '
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700115 '- using first one (${srv.first.port}).');
116 }
Jonah Williams8436c6a2020-11-13 13:23:03 -0800117 _logger.printTrace('Checking for authentication code for $domainName');
118 final List<TxtResourceRecord> txt = await _client
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700119 .lookup<TxtResourceRecord>(
120 ResourceRecordQuery.text(domainName),
121 )
122 ?.toList();
123 if (txt == null || txt.isEmpty) {
124 return MDnsObservatoryDiscoveryResult(srv.first.port, '');
125 }
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700126 const String authCodePrefix = 'authCode=';
Zachary Anderson3b66db62019-10-07 09:46:57 -0700127 final String raw = txt.first.text.split('\n').firstWhere(
128 (String s) => s.startsWith(authCodePrefix),
129 orElse: () => null,
130 );
131 if (raw == null) {
132 return MDnsObservatoryDiscoveryResult(srv.first.port, '');
133 }
134 String authCode = raw.substring(authCodePrefix.length);
135 // The Observatory currently expects a trailing '/' as part of the
136 // URI, otherwise an invalid authentication code response is given.
137 if (!authCode.endsWith('/')) {
138 authCode += '/';
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700139 }
140 return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
141 } finally {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800142 _client.stop();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700143 }
144 }
145
Jonah Williamse3cb2c32019-11-13 16:02:46 -0800146 Future<Uri> getObservatoryUri(String applicationId, Device device, {
147 bool usesIpv6 = false,
148 int hostVmservicePort,
149 int deviceVmservicePort,
150 }) async {
151 final MDnsObservatoryDiscoveryResult result = await query(
152 applicationId: applicationId,
153 deviceVmservicePort: deviceVmservicePort,
154 );
Zachary Andersona72cca12019-12-16 14:57:29 -0800155 if (result == null) {
156 await _checkForIPv4LinkLocal(device);
157 return null;
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700158 }
Zachary Andersona72cca12019-12-16 14:57:29 -0800159
160 final String host = usesIpv6
161 ? InternetAddress.loopbackIPv6.address
162 : InternetAddress.loopbackIPv4.address;
163 return await buildObservatoryUri(
164 device,
165 host,
166 result.port,
167 hostVmservicePort,
168 result.authCode,
169 );
170 }
171
172 // If there's not an ipv4 link local address in `NetworkInterfaces.list`,
173 // then request user interventions with a `printError()` if possible.
174 Future<void> _checkForIPv4LinkLocal(Device device) async {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800175 _logger.printTrace(
Zachary Andersona72cca12019-12-16 14:57:29 -0800176 'mDNS query failed. Checking for an interface with a ipv4 link local address.'
177 );
178 final List<NetworkInterface> interfaces = await listNetworkInterfaces(
179 includeLinkLocal: true,
180 type: InternetAddressType.IPv4,
181 );
Jonah Williams8436c6a2020-11-13 13:23:03 -0800182 if (_logger.isVerbose) {
Zachary Andersona72cca12019-12-16 14:57:29 -0800183 _logInterfaces(interfaces);
184 }
185 final bool hasIPv4LinkLocal = interfaces.any(
186 (NetworkInterface interface) => interface.addresses.any(
187 (InternetAddress address) => address.isLinkLocal,
188 ),
189 );
190 if (hasIPv4LinkLocal) {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800191 _logger.printTrace('An interface with an ipv4 link local address was found.');
Zachary Andersona72cca12019-12-16 14:57:29 -0800192 return;
193 }
194 final TargetPlatform targetPlatform = await device.targetPlatform;
195 switch (targetPlatform) {
196 case TargetPlatform.ios:
Jonah Williams8436c6a2020-11-13 13:23:03 -0800197 UsageEvent('ios-mdns', 'no-ipv4-link-local', flutterUsage: _flutterUsage).send();
198 _logger.printError(
Zachary Andersona72cca12019-12-16 14:57:29 -0800199 'The mDNS query for an attached iOS device failed. It may '
Zachary Andersond328e0c2019-12-18 09:23:01 -0800200 'be necessary to disable the "Personal Hotspot" on the device, and '
201 'to ensure that the "Disable unless needed" setting is unchecked '
Alexandre Ardhuin3800bb72020-01-22 01:43:03 +0100202 'under System Preferences > Network > iPhone USB. '
Zachary Andersona72cca12019-12-16 14:57:29 -0800203 'See https://github.com/flutter/flutter/issues/46698 for details.'
204 );
205 break;
206 default:
Jonah Williams8436c6a2020-11-13 13:23:03 -0800207 _logger.printTrace('No interface with an ipv4 link local address was found.');
Zachary Andersona72cca12019-12-16 14:57:29 -0800208 break;
209 }
210 }
211
212 void _logInterfaces(List<NetworkInterface> interfaces) {
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100213 for (final NetworkInterface interface in interfaces) {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800214 if (_logger.isVerbose) {
215 _logger.printTrace('Found interface "${interface.name}":');
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100216 for (final InternetAddress address in interface.addresses) {
Zachary Andersona72cca12019-12-16 14:57:29 -0800217 final String linkLocal = address.isLinkLocal ? 'link local' : '';
Jonah Williams8436c6a2020-11-13 13:23:03 -0800218 _logger.printTrace('\tBound address: "${address.address}" $linkLocal');
Zachary Andersona72cca12019-12-16 14:57:29 -0800219 }
220 }
221 }
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700222 }
223}
224
225class MDnsObservatoryDiscoveryResult {
226 MDnsObservatoryDiscoveryResult(this.port, this.authCode);
227 final int port;
228 final String authCode;
229}
230
Alexandre Ardhuin890b9392019-10-04 11:00:18 +0200231Future<Uri> buildObservatoryUri(
232 Device device,
233 String host,
234 int devicePort, [
Jonah Williamse3cb2c32019-11-13 16:02:46 -0800235 int hostVmservicePort,
Alexandre Ardhuin890b9392019-10-04 11:00:18 +0200236 String authCode,
237]) async {
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700238 String path = '/';
239 if (authCode != null) {
240 path = authCode;
241 }
242 // Not having a trailing slash can cause problems in some situations.
243 // Ensure that there's one present.
244 if (!path.endsWith('/')) {
245 path += '/';
246 }
Ben Konyia17b3302020-09-16 16:27:42 -0700247 hostVmservicePort ??= 0;
248 final int actualHostPort = hostVmservicePort == 0 ?
249 await device.portForwarder.forward(devicePort) :
250 hostVmservicePort;
Jonah Williamse3cb2c32019-11-13 16:02:46 -0800251 return Uri(scheme: 'http', host: host, port: actualHostPort, path: path);
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700252}