blob: a1d0717e1d2f639f06f1589a1b11997b84a97e51 [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({
Jenn Magder53e04de2021-10-29 17:18:03 -070025 MDnsClient? mdnsClient,
26 required Logger logger,
27 required Usage flutterUsage,
Jonah Williams8436c6a2020-11-13 13:23:03 -080028 }): _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
Jenn Magder53e04de2021-10-29 17:18:03 -070039 static MDnsObservatoryDiscovery? get instance => context.get<MDnsObservatoryDiscovery>();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070040
41 /// Executes an mDNS query for a Dart Observatory.
42 ///
43 /// The [applicationId] parameter may be used to specify which application
Pierre-Louis0c2f7bc2022-09-02 06:00:58 +020044 /// to find. For Android, it refers to the package name; on iOS, it refers to
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070045 /// 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.
Jenn Magderf8b1de32020-10-07 08:52:05 -070059 @visibleForTesting
Jenn Magder53e04de2021-10-29 17:18:03 -070060 Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async {
Jonah Williams8436c6a2020-11-13 13:23:03 -080061 _logger.printTrace('Checking for advertised Dart observatories...');
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070062 try {
Jonah Williams8436c6a2020-11-13 13:23:03 -080063 await _client.start();
64 final List<PtrResourceRecord> pointerRecords = await _client
Zachary Anderson0cd2ece2020-03-12 14:01:01 -070065 .lookup<PtrResourceRecord>(
66 ResourceRecordQuery.serverPointer(dartObservatoryName),
67 )
68 .toList();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070069 if (pointerRecords.isEmpty) {
Jonah Williams8436c6a2020-11-13 13:23:03 -080070 _logger.printTrace('No pointer records found.');
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070071 return null;
72 }
73 // We have no guarantee that we won't get multiple hits from the same
74 // service on this.
Jonah Williams69ecca52020-02-06 13:58:03 -080075 final Set<String> uniqueDomainNames = pointerRecords
Zachary Anderson0cd2ece2020-03-12 14:01:01 -070076 .map<String>((PtrResourceRecord record) => record.domainName)
77 .toSet();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070078
Jenn Magder53e04de2021-10-29 17:18:03 -070079 String? domainName;
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070080 if (applicationId != null) {
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +010081 for (final String name in uniqueDomainNames) {
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070082 if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
83 domainName = name;
84 break;
85 }
86 }
87 if (domainName == null) {
88 throwToolExit('Did not find a observatory port advertised for $applicationId.');
89 }
90 } else if (uniqueDomainNames.length > 1) {
91 final StringBuffer buffer = StringBuffer();
92 buffer.writeln('There are multiple observatory ports available.');
93 buffer.writeln('Rerun this command with one of the following passed in as the appId:');
Jenn Magder53e04de2021-10-29 17:18:03 -070094 buffer.writeln();
Alexandre Ardhuinb8731622019-09-24 21:03:37 +020095 for (final String uniqueDomainName in uniqueDomainNames) {
Christopher Fujino0b24a5a2019-09-18 11:01:08 -070096 buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
97 }
98 throwToolExit(buffer.toString());
99 } else {
100 domainName = pointerRecords[0].domainName;
101 }
Jonah Williams8436c6a2020-11-13 13:23:03 -0800102 _logger.printTrace('Checking for available port on $domainName');
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700103 // Here, if we get more than one, it should just be a duplicate.
Jonah Williams8436c6a2020-11-13 13:23:03 -0800104 final List<SrvResourceRecord> srv = await _client
Zachary Anderson0cd2ece2020-03-12 14:01:01 -0700105 .lookup<SrvResourceRecord>(
106 ResourceRecordQuery.service(domainName),
107 )
108 .toList();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700109 if (srv.isEmpty) {
110 return null;
111 }
112 if (srv.length > 1) {
Greg Spencer52ae1022021-11-10 16:13:04 -0800113 _logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700114 '- using first one (${srv.first.port}).');
115 }
Jonah Williams8436c6a2020-11-13 13:23:03 -0800116 _logger.printTrace('Checking for authentication code for $domainName');
117 final List<TxtResourceRecord> txt = await _client
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700118 .lookup<TxtResourceRecord>(
119 ResourceRecordQuery.text(domainName),
120 )
Jenn Magder53e04de2021-10-29 17:18:03 -0700121 .toList();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700122 if (txt == null || txt.isEmpty) {
123 return MDnsObservatoryDiscoveryResult(srv.first.port, '');
124 }
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700125 const String authCodePrefix = 'authCode=';
Jenn Magder53e04de2021-10-29 17:18:03 -0700126 String? raw;
127 for (final String record in txt.first.text.split('\n')) {
128 if (record.startsWith(authCodePrefix)) {
129 raw = record;
130 break;
131 }
132 }
Zachary Anderson3b66db62019-10-07 09:46:57 -0700133 if (raw == null) {
134 return MDnsObservatoryDiscoveryResult(srv.first.port, '');
135 }
136 String authCode = raw.substring(authCodePrefix.length);
137 // The Observatory currently expects a trailing '/' as part of the
138 // URI, otherwise an invalid authentication code response is given.
139 if (!authCode.endsWith('/')) {
140 authCode += '/';
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700141 }
142 return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
143 } finally {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800144 _client.stop();
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700145 }
146 }
147
Christopher Fujino1371b8d2022-07-18 16:10:06 -0700148 Future<Uri?> getObservatoryUri(String? applicationId, Device device, {
Jonah Williamse3cb2c32019-11-13 16:02:46 -0800149 bool usesIpv6 = false,
Jenn Magder53e04de2021-10-29 17:18:03 -0700150 int? hostVmservicePort,
151 int? deviceVmservicePort,
Jonah Williamse3cb2c32019-11-13 16:02:46 -0800152 }) async {
Jenn Magder53e04de2021-10-29 17:18:03 -0700153 final MDnsObservatoryDiscoveryResult? result = await query(
Jonah Williamse3cb2c32019-11-13 16:02:46 -0800154 applicationId: applicationId,
155 deviceVmservicePort: deviceVmservicePort,
156 );
Zachary Andersona72cca12019-12-16 14:57:29 -0800157 if (result == null) {
158 await _checkForIPv4LinkLocal(device);
159 return null;
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700160 }
Zachary Andersona72cca12019-12-16 14:57:29 -0800161
162 final String host = usesIpv6
163 ? InternetAddress.loopbackIPv6.address
164 : InternetAddress.loopbackIPv4.address;
Michael Goderbauercb867bb2021-03-05 18:38:15 -0800165 return buildObservatoryUri(
Zachary Andersona72cca12019-12-16 14:57:29 -0800166 device,
167 host,
168 result.port,
169 hostVmservicePort,
170 result.authCode,
171 );
172 }
173
174 // If there's not an ipv4 link local address in `NetworkInterfaces.list`,
175 // then request user interventions with a `printError()` if possible.
176 Future<void> _checkForIPv4LinkLocal(Device device) async {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800177 _logger.printTrace(
Zachary Andersona72cca12019-12-16 14:57:29 -0800178 'mDNS query failed. Checking for an interface with a ipv4 link local address.'
179 );
180 final List<NetworkInterface> interfaces = await listNetworkInterfaces(
181 includeLinkLocal: true,
182 type: InternetAddressType.IPv4,
183 );
Jonah Williams8436c6a2020-11-13 13:23:03 -0800184 if (_logger.isVerbose) {
Zachary Andersona72cca12019-12-16 14:57:29 -0800185 _logInterfaces(interfaces);
186 }
187 final bool hasIPv4LinkLocal = interfaces.any(
188 (NetworkInterface interface) => interface.addresses.any(
189 (InternetAddress address) => address.isLinkLocal,
190 ),
191 );
192 if (hasIPv4LinkLocal) {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800193 _logger.printTrace('An interface with an ipv4 link local address was found.');
Zachary Andersona72cca12019-12-16 14:57:29 -0800194 return;
195 }
196 final TargetPlatform targetPlatform = await device.targetPlatform;
197 switch (targetPlatform) {
198 case TargetPlatform.ios:
Jonah Williams8436c6a2020-11-13 13:23:03 -0800199 UsageEvent('ios-mdns', 'no-ipv4-link-local', flutterUsage: _flutterUsage).send();
200 _logger.printError(
Zachary Andersona72cca12019-12-16 14:57:29 -0800201 'The mDNS query for an attached iOS device failed. It may '
Zachary Andersond328e0c2019-12-18 09:23:01 -0800202 'be necessary to disable the "Personal Hotspot" on the device, and '
203 'to ensure that the "Disable unless needed" setting is unchecked '
Alexandre Ardhuin3800bb72020-01-22 01:43:03 +0100204 'under System Preferences > Network > iPhone USB. '
Zachary Andersona72cca12019-12-16 14:57:29 -0800205 'See https://github.com/flutter/flutter/issues/46698 for details.'
206 );
207 break;
Ian Hickson7b013462021-10-11 10:23:04 -0700208 case TargetPlatform.android:
209 case TargetPlatform.android_arm:
210 case TargetPlatform.android_arm64:
211 case TargetPlatform.android_x64:
212 case TargetPlatform.android_x86:
213 case TargetPlatform.darwin:
214 case TargetPlatform.fuchsia_arm64:
215 case TargetPlatform.fuchsia_x64:
216 case TargetPlatform.linux_arm64:
217 case TargetPlatform.linux_x64:
218 case TargetPlatform.tester:
219 case TargetPlatform.web_javascript:
Ian Hickson7b013462021-10-11 10:23:04 -0700220 case TargetPlatform.windows_x64:
Jonah Williams8436c6a2020-11-13 13:23:03 -0800221 _logger.printTrace('No interface with an ipv4 link local address was found.');
Zachary Andersona72cca12019-12-16 14:57:29 -0800222 break;
223 }
224 }
225
226 void _logInterfaces(List<NetworkInterface> interfaces) {
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100227 for (final NetworkInterface interface in interfaces) {
Jonah Williams8436c6a2020-11-13 13:23:03 -0800228 if (_logger.isVerbose) {
229 _logger.printTrace('Found interface "${interface.name}":');
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100230 for (final InternetAddress address in interface.addresses) {
Zachary Andersona72cca12019-12-16 14:57:29 -0800231 final String linkLocal = address.isLinkLocal ? 'link local' : '';
Jonah Williams8436c6a2020-11-13 13:23:03 -0800232 _logger.printTrace('\tBound address: "${address.address}" $linkLocal');
Zachary Andersona72cca12019-12-16 14:57:29 -0800233 }
234 }
235 }
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700236 }
237}
238
239class MDnsObservatoryDiscoveryResult {
240 MDnsObservatoryDiscoveryResult(this.port, this.authCode);
241 final int port;
242 final String authCode;
243}
244
Alexandre Ardhuin890b9392019-10-04 11:00:18 +0200245Future<Uri> buildObservatoryUri(
246 Device device,
247 String host,
248 int devicePort, [
Jenn Magder53e04de2021-10-29 17:18:03 -0700249 int? hostVmservicePort,
250 String? authCode,
Alexandre Ardhuin890b9392019-10-04 11:00:18 +0200251]) async {
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700252 String path = '/';
253 if (authCode != null) {
254 path = authCode;
255 }
256 // Not having a trailing slash can cause problems in some situations.
257 // Ensure that there's one present.
258 if (!path.endsWith('/')) {
259 path += '/';
260 }
Ben Konyia17b3302020-09-16 16:27:42 -0700261 hostVmservicePort ??= 0;
Jenn Magder53e04de2021-10-29 17:18:03 -0700262 final int? actualHostPort = hostVmservicePort == 0 ?
263 await device.portForwarder?.forward(devicePort) :
Ben Konyia17b3302020-09-16 16:27:42 -0700264 hostVmservicePort;
Jonah Williamse3cb2c32019-11-13 16:02:46 -0800265 return Uri(scheme: 'http', host: host, port: actualHostPort, path: path);
Christopher Fujino0b24a5a2019-09-18 11:01:08 -0700266}