1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:async'; |
6 | |
7 | import 'package:meta/meta.dart' ; |
8 | import 'package:multicast_dns/multicast_dns.dart' ; |
9 | import 'package:unified_analytics/unified_analytics.dart' ; |
10 | |
11 | import 'base/common.dart'; |
12 | import 'base/context.dart'; |
13 | import 'base/io.dart'; |
14 | import 'base/logger.dart'; |
15 | import 'build_info.dart'; |
16 | import 'convert.dart'; |
17 | import 'device.dart'; |
18 | import 'globals.dart' as globals; |
19 | |
20 | String _missingLocalNetworkPermissionsInstructions(String err) => |
21 | ''' |
22 | Flutter could not access the local network. |
23 | |
24 | Please ensure your IDE or terminal app has permission to access devices on the local network. This allows Flutter to connect to the Dart VM. |
25 | |
26 | You can grant this permission in System Settings > Privacy & Security > Local Network. |
27 | |
28 | $err |
29 | ''' ; |
30 | |
31 | /// A wrapper around [MDnsClient] to find a Dart VM Service instance. |
32 | class MDnsVmServiceDiscovery { |
33 | /// Creates a new [MDnsVmServiceDiscovery] object. |
34 | /// |
35 | /// The [_client] parameter will be defaulted to a new [MDnsClient] if null. |
36 | MDnsVmServiceDiscovery({ |
37 | MDnsClient? mdnsClient, |
38 | MDnsClient? preliminaryMDnsClient, |
39 | required Logger logger, |
40 | required Analytics analytics, |
41 | }) : _client = mdnsClient ?? MDnsClient(), |
42 | _preliminaryClient = preliminaryMDnsClient, |
43 | _logger = logger, |
44 | _analytics = analytics; |
45 | |
46 | final MDnsClient _client; |
47 | |
48 | // Used when discovering VM services with `queryForAttach` to do a preliminary |
49 | // check for already running services so that results are not cached in _client. |
50 | final MDnsClient? _preliminaryClient; |
51 | |
52 | final Logger _logger; |
53 | final Analytics _analytics; |
54 | |
55 | @visibleForTesting |
56 | static const dartVmServiceName = '_dartVmService._tcp.local' ; |
57 | |
58 | static MDnsVmServiceDiscovery? get instance => context.get<MDnsVmServiceDiscovery>(); |
59 | |
60 | /// Executes an mDNS query for Dart VM Services. |
61 | /// Checks for services that have already been launched. |
62 | /// If none are found, it will listen for new services to become active |
63 | /// and return the first it finds that match the parameters. |
64 | /// |
65 | /// The [applicationId] parameter may be used to specify which application |
66 | /// to find. For Android, it refers to the package name; on iOS, it refers to |
67 | /// the bundle ID. |
68 | /// |
69 | /// The [deviceVmservicePort] parameter may be used to specify which port |
70 | /// to find. |
71 | /// |
72 | /// The [useDeviceIPAsHost] parameter flags whether to get the device IP |
73 | /// and the [ipv6] parameter flags whether to get an iPv6 address |
74 | /// (otherwise it will get iPv4). |
75 | /// |
76 | /// The [timeout] parameter determines how long to continue to wait for |
77 | /// services to become active. |
78 | /// |
79 | /// If [applicationId] is not null, this method will find the port and authentication code |
80 | /// of the Dart VM Service for that application. If it cannot find a service matching |
81 | /// that application identifier after the [timeout], it will call [throwToolExit]. |
82 | /// |
83 | /// If [applicationId] is null and there are multiple Dart VM Services available, |
84 | /// the user will be prompted with a list of available services with the respective |
85 | /// app-id and device-vmservice-port to use and asked to select one. |
86 | /// |
87 | /// If it is null and there is only one available or it's the first found instance |
88 | /// of Dart VM Service, it will return that instance's information regardless of |
89 | /// what application the service instance is for. |
90 | @visibleForTesting |
91 | Future<MDnsVmServiceDiscoveryResult?> queryForAttach({ |
92 | String? applicationId, |
93 | int? deviceVmservicePort, |
94 | bool ipv6 = false, |
95 | bool useDeviceIPAsHost = false, |
96 | Duration timeout = const Duration(minutes: 10), |
97 | bool throwOnMissingLocalNetworkPermissionsError = true, |
98 | }) async { |
99 | // Poll for 5 seconds to see if there are already services running. |
100 | // Use a new instance of MDnsClient so results don't get cached in _client. |
101 | // If no results are found, poll for a longer duration to wait for connections. |
102 | // If more than 1 result is found, throw an error since it can't be determined which to pick. |
103 | // If only one is found, return it. |
104 | final List<MDnsVmServiceDiscoveryResult> results = await _pollingVmService( |
105 | _preliminaryClient ?? MDnsClient(), |
106 | applicationId: applicationId, |
107 | deviceVmServicePort: deviceVmservicePort, |
108 | ipv6: ipv6, |
109 | useDeviceIPAsHost: useDeviceIPAsHost, |
110 | timeout: const Duration(seconds: 5), |
111 | throwOnMissingLocalNetworkPermissionsError: throwOnMissingLocalNetworkPermissionsError, |
112 | ); |
113 | if (results.isEmpty) { |
114 | return firstMatchingVmService( |
115 | _client, |
116 | applicationId: applicationId, |
117 | deviceVmservicePort: deviceVmservicePort, |
118 | ipv6: ipv6, |
119 | useDeviceIPAsHost: useDeviceIPAsHost, |
120 | timeout: timeout, |
121 | ); |
122 | } else if (results.length > 1) { |
123 | final buffer = StringBuffer(); |
124 | buffer.writeln('There are multiple Dart VM Services available.' ); |
125 | buffer.writeln( |
126 | 'Rerun this command with one of the following passed in as the app-id and device-vmservice-port:' , |
127 | ); |
128 | buffer.writeln(); |
129 | for (final result in results) { |
130 | buffer.writeln( |
131 | ' flutter attach --app-id " ${result.domainName.replaceAll('. $dartVmServiceName' , '' )}" --device-vmservice-port ${result.port}' , |
132 | ); |
133 | } |
134 | throwToolExit(buffer.toString()); |
135 | } |
136 | return results.first; |
137 | } |
138 | |
139 | /// Executes an mDNS query for Dart VM Services. |
140 | /// Listens for new services to become active and returns the first it finds that |
141 | /// match the parameters. |
142 | /// |
143 | /// The [applicationId] parameter must be set to specify which application |
144 | /// to find. For Android, it refers to the package name; on iOS, it refers to |
145 | /// the bundle ID. |
146 | /// |
147 | /// The [deviceVmservicePort] parameter must be set to specify which port |
148 | /// to find. |
149 | /// |
150 | /// [applicationId] and either [deviceVmservicePort] or [deviceName] are |
151 | /// required for launch so that if multiple flutter apps are running on |
152 | /// different devices, it will only match with the device running the desired app. |
153 | /// |
154 | /// The [useDeviceIPAsHost] parameter flags whether to get the device IP |
155 | /// and the [ipv6] parameter flags whether to get an iPv6 address |
156 | /// (otherwise it will get iPv4). |
157 | /// |
158 | /// The [timeout] parameter determines how long to continue to wait for |
159 | /// services to become active. |
160 | /// |
161 | /// If a Dart VM Service matching the [applicationId] and |
162 | /// [deviceVmservicePort]/[deviceName] cannot be found before the [timeout] |
163 | /// is reached, it will call [throwToolExit]. |
164 | @visibleForTesting |
165 | Future<MDnsVmServiceDiscoveryResult?> queryForLaunch({ |
166 | required String applicationId, |
167 | int? deviceVmservicePort, |
168 | String? deviceName, |
169 | bool ipv6 = false, |
170 | bool useDeviceIPAsHost = false, |
171 | Duration timeout = const Duration(minutes: 10), |
172 | bool throwOnMissingLocalNetworkPermissionsError = true, |
173 | }) async { |
174 | // Either the device port or the device name must be provided. |
175 | assert(deviceVmservicePort != null || deviceName != null); |
176 | |
177 | // Query for a specific application matching on either device port or device name. |
178 | return firstMatchingVmService( |
179 | _client, |
180 | applicationId: applicationId, |
181 | deviceVmservicePort: deviceVmservicePort, |
182 | deviceName: deviceName, |
183 | ipv6: ipv6, |
184 | useDeviceIPAsHost: useDeviceIPAsHost, |
185 | timeout: timeout, |
186 | throwOnMissingLocalNetworkPermissionsError: throwOnMissingLocalNetworkPermissionsError, |
187 | ); |
188 | } |
189 | |
190 | /// Polls for Dart VM Services and returns the first it finds that match |
191 | /// the [applicationId]/[deviceVmservicePort] (if applicable). |
192 | /// Returns null if no results are found. |
193 | @visibleForTesting |
194 | Future<MDnsVmServiceDiscoveryResult?> firstMatchingVmService( |
195 | MDnsClient client, { |
196 | String? applicationId, |
197 | int? deviceVmservicePort, |
198 | String? deviceName, |
199 | bool ipv6 = false, |
200 | bool useDeviceIPAsHost = false, |
201 | Duration timeout = const Duration(minutes: 10), |
202 | bool throwOnMissingLocalNetworkPermissionsError = true, |
203 | }) async { |
204 | final List<MDnsVmServiceDiscoveryResult> results = await _pollingVmService( |
205 | client, |
206 | applicationId: applicationId, |
207 | deviceVmServicePort: deviceVmservicePort, |
208 | deviceName: deviceName, |
209 | ipv6: ipv6, |
210 | useDeviceIPAsHost: useDeviceIPAsHost, |
211 | timeout: timeout, |
212 | quitOnFind: true, |
213 | throwOnMissingLocalNetworkPermissionsError: throwOnMissingLocalNetworkPermissionsError, |
214 | ); |
215 | if (results.isEmpty) { |
216 | return null; |
217 | } |
218 | return results.first; |
219 | } |
220 | |
221 | Future<List<MDnsVmServiceDiscoveryResult>> _pollingVmService( |
222 | MDnsClient client, { |
223 | String? applicationId, |
224 | int? deviceVmServicePort, |
225 | String? deviceName, |
226 | bool ipv6 = false, |
227 | bool useDeviceIPAsHost = false, |
228 | required Duration timeout, |
229 | bool quitOnFind = false, |
230 | bool throwOnMissingLocalNetworkPermissionsError = true, |
231 | }) async { |
232 | // macOS blocks mDNS unless the app has Local Network permissions. |
233 | // Since the mDNS client does not handle errors from the socket's stream, |
234 | // socket exceptions are routed to the current zone. Create an error zone to |
235 | // catch the socket exception. |
236 | // See: https://github.com/flutter/flutter/issues/150131 |
237 | final completer = Completer<List<MDnsVmServiceDiscoveryResult>>(); |
238 | unawaited( |
239 | runZonedGuarded( |
240 | () async { |
241 | final List<MDnsVmServiceDiscoveryResult> results = await _doPollingVmService( |
242 | client, |
243 | applicationId: applicationId, |
244 | deviceVmServicePort: deviceVmServicePort, |
245 | deviceName: deviceName, |
246 | ipv6: ipv6, |
247 | useDeviceIPAsHost: useDeviceIPAsHost, |
248 | timeout: timeout, |
249 | quitOnFind: quitOnFind, |
250 | ); |
251 | |
252 | if (!completer.isCompleted) { |
253 | completer.complete(results); |
254 | } |
255 | }, |
256 | (Object error, StackTrace stackTrace) { |
257 | if (!completer.isCompleted) { |
258 | completer.completeError(error, stackTrace); |
259 | } |
260 | }, |
261 | ), |
262 | ); |
263 | |
264 | try { |
265 | return await completer.future; |
266 | } on SocketException catch (e, stackTrace) { |
267 | if (!globals.platform.isMacOS) { |
268 | rethrow; |
269 | } |
270 | |
271 | _logger.printTrace(stackTrace.toString()); |
272 | if (throwOnMissingLocalNetworkPermissionsError) { |
273 | throwToolExit(_missingLocalNetworkPermissionsInstructions(e.toString())); |
274 | } else { |
275 | _logger.printError(_missingLocalNetworkPermissionsInstructions(e.toString())); |
276 | return <MDnsVmServiceDiscoveryResult>[]; |
277 | } |
278 | } |
279 | } |
280 | |
281 | Future<List<MDnsVmServiceDiscoveryResult>> _doPollingVmService( |
282 | MDnsClient client, { |
283 | String? applicationId, |
284 | int? deviceVmServicePort, |
285 | String? deviceName, |
286 | bool ipv6 = false, |
287 | bool useDeviceIPAsHost = false, |
288 | required Duration timeout, |
289 | bool quitOnFind = false, |
290 | }) async { |
291 | _logger.printTrace('Checking for advertised Dart VM Services...' ); |
292 | try { |
293 | await client.start(); |
294 | |
295 | final results = <MDnsVmServiceDiscoveryResult>[]; |
296 | |
297 | // uniqueDomainNames is used to track all domain names of Dart VM services |
298 | // It is later used in this function to determine whether or not to throw an error. |
299 | // We do not want to throw the error if it was unable to find any domain |
300 | // names because that indicates it may be a problem with mDNS, which has |
301 | // a separate error message in _checkForIPv4LinkLocal. |
302 | final uniqueDomainNames = <String>{}; |
303 | // uniqueDomainNamesInResults is used to filter out duplicates with exactly |
304 | // the same domain name from the results. |
305 | final uniqueDomainNamesInResults = <String>{}; |
306 | |
307 | // Listen for mDNS connections until timeout. |
308 | final Stream<PtrResourceRecord> ptrResourceStream = client.lookup<PtrResourceRecord>( |
309 | ResourceRecordQuery.serverPointer(dartVmServiceName), |
310 | timeout: timeout, |
311 | ); |
312 | |
313 | await for (final PtrResourceRecord ptr in ptrResourceStream) { |
314 | uniqueDomainNames.add(ptr.domainName); |
315 | |
316 | String? domainName; |
317 | if (applicationId != null) { |
318 | // If applicationId is set, only use records that match it |
319 | if (ptr.domainName.toLowerCase().startsWith(applicationId.toLowerCase())) { |
320 | domainName = ptr.domainName; |
321 | } else { |
322 | continue; |
323 | } |
324 | } else { |
325 | domainName = ptr.domainName; |
326 | } |
327 | |
328 | // Result with same domain name was already found, skip it. |
329 | if (uniqueDomainNamesInResults.contains(domainName)) { |
330 | continue; |
331 | } |
332 | |
333 | _logger.printTrace('Checking for available port on $domainName' ); |
334 | final List<SrvResourceRecord> srvRecords = await client |
335 | .lookup<SrvResourceRecord>(ResourceRecordQuery.service(domainName)) |
336 | .toList(); |
337 | if (srvRecords.isEmpty) { |
338 | continue; |
339 | } |
340 | |
341 | // If more than one SrvResourceRecord found, it should just be a duplicate. |
342 | final SrvResourceRecord srvRecord = srvRecords.first; |
343 | if (srvRecords.length > 1) { |
344 | _logger.printWarning( |
345 | 'Unexpectedly found more than one Dart VM Service report for $domainName ' |
346 | '- using first one ( ${srvRecord.port}).' , |
347 | ); |
348 | } |
349 | |
350 | // If deviceVmServicePort is set, only use records that match it |
351 | if (deviceVmServicePort != null && srvRecord.port != deviceVmServicePort) { |
352 | continue; |
353 | } |
354 | |
355 | // If deviceName is set, only use records that match it |
356 | if (deviceName != null && !deviceNameMatchesTargetName(deviceName, srvRecord.target)) { |
357 | continue; |
358 | } |
359 | |
360 | // Get the IP address of the device if using the IP as the host. |
361 | InternetAddress? ipAddress; |
362 | if (useDeviceIPAsHost) { |
363 | List<IPAddressResourceRecord> ipAddresses = await client |
364 | .lookup<IPAddressResourceRecord>( |
365 | ipv6 |
366 | ? ResourceRecordQuery.addressIPv6(srvRecord.target) |
367 | : ResourceRecordQuery.addressIPv4(srvRecord.target), |
368 | ) |
369 | .toList(); |
370 | if (ipAddresses.isEmpty) { |
371 | throwToolExit('Did not find IP for service ${srvRecord.target}.' ); |
372 | } |
373 | |
374 | // Filter out link-local addresses. |
375 | if (ipAddresses.length > 1) { |
376 | ipAddresses = ipAddresses |
377 | .where((IPAddressResourceRecord element) => !element.address.isLinkLocal) |
378 | .toList(); |
379 | } |
380 | |
381 | ipAddress = ipAddresses.first.address; |
382 | if (ipAddresses.length > 1) { |
383 | _logger.printWarning( |
384 | 'Unexpectedly found more than one IP for Dart VM Service ${srvRecord.target} ' |
385 | '- using first one ( $ipAddress).' , |
386 | ); |
387 | } |
388 | } |
389 | |
390 | _logger.printTrace('Checking for authentication code for $domainName' ); |
391 | final List<TxtResourceRecord> txt = await client |
392 | .lookup<TxtResourceRecord>(ResourceRecordQuery.text(domainName)) |
393 | .toList(); |
394 | |
395 | var authCode = '' ; |
396 | if (txt.isNotEmpty) { |
397 | authCode = _getAuthCode(txt.first.text); |
398 | } |
399 | results.add( |
400 | MDnsVmServiceDiscoveryResult(domainName, srvRecord.port, authCode, ipAddress: ipAddress), |
401 | ); |
402 | uniqueDomainNamesInResults.add(domainName); |
403 | if (quitOnFind) { |
404 | return results; |
405 | } |
406 | } |
407 | |
408 | // If applicationId is set and quitOnFind is true and no results matching |
409 | // the applicationId were found but other results were found, throw an error. |
410 | if (applicationId != null && quitOnFind && results.isEmpty && uniqueDomainNames.isNotEmpty) { |
411 | var message = 'Did not find a Dart VM Service advertised for $applicationId' ; |
412 | if (deviceVmServicePort != null) { |
413 | message += ' on port $deviceVmServicePort' ; |
414 | } |
415 | throwToolExit(' $message.' ); |
416 | } |
417 | |
418 | return results; |
419 | } finally { |
420 | client.stop(); |
421 | } |
422 | } |
423 | |
424 | @visibleForTesting |
425 | bool deviceNameMatchesTargetName(String deviceName, String targetName) { |
426 | // Remove `.local` from the name along with any non-word, non-digit characters. |
427 | final cleanedNameRegex = RegExp(r'\.local|\W' ); |
428 | final String cleanedDeviceName = deviceName.trim().toLowerCase().replaceAll( |
429 | cleanedNameRegex, |
430 | '' , |
431 | ); |
432 | final String cleanedTargetName = targetName.toLowerCase().replaceAll(cleanedNameRegex, '' ); |
433 | return cleanedDeviceName == cleanedTargetName; |
434 | } |
435 | |
436 | String _getAuthCode(String txtRecord) { |
437 | const authCodePrefix = 'authCode=' ; |
438 | final Iterable<String> matchingRecords = LineSplitter.split( |
439 | txtRecord, |
440 | ).where((String record) => record.startsWith(authCodePrefix)); |
441 | if (matchingRecords.isEmpty) { |
442 | return '' ; |
443 | } |
444 | String authCode = matchingRecords.first.substring(authCodePrefix.length); |
445 | // The Dart VM Service currently expects a trailing '/' as part of the |
446 | // URI, otherwise an invalid authentication code response is given. |
447 | if (!authCode.endsWith('/' )) { |
448 | authCode += '/' ; |
449 | } |
450 | return authCode; |
451 | } |
452 | |
453 | /// Gets Dart VM Service Uri for `flutter attach`. |
454 | /// Executes an mDNS query and waits until a Dart VM Service is found. |
455 | /// |
456 | /// When [useDeviceIPAsHost] is true, it will use the device's IP as the |
457 | /// host and will not forward the port. |
458 | /// |
459 | /// Differs from [getVMServiceUriForLaunch] because it can search for any available Dart VM Service. |
460 | /// Since [applicationId] and [deviceVmservicePort] are optional, it can either look for any service |
461 | /// or a specific service matching [applicationId]/[deviceVmservicePort]. |
462 | /// It may find more than one service, which will throw an error listing the found services. |
463 | Future<Uri?> getVMServiceUriForAttach( |
464 | String? applicationId, |
465 | Device device, { |
466 | bool usesIpv6 = false, |
467 | int? hostVmservicePort, |
468 | int? deviceVmservicePort, |
469 | bool useDeviceIPAsHost = false, |
470 | Duration timeout = const Duration(minutes: 10), |
471 | }) async { |
472 | final MDnsVmServiceDiscoveryResult? result = await queryForAttach( |
473 | applicationId: applicationId, |
474 | deviceVmservicePort: deviceVmservicePort, |
475 | ipv6: usesIpv6, |
476 | useDeviceIPAsHost: useDeviceIPAsHost, |
477 | timeout: timeout, |
478 | ); |
479 | return _handleResult( |
480 | result, |
481 | device, |
482 | applicationId: applicationId, |
483 | deviceVmservicePort: deviceVmservicePort, |
484 | hostVmservicePort: hostVmservicePort, |
485 | usesIpv6: usesIpv6, |
486 | useDeviceIPAsHost: useDeviceIPAsHost, |
487 | ); |
488 | } |
489 | |
490 | /// Gets Dart VM Service Uri for `flutter run`. |
491 | /// Executes an mDNS query and waits until the Dart VM Service service is found. |
492 | /// |
493 | /// When [useDeviceIPAsHost] is true, it will use the device's IP as the |
494 | /// host and will not forward the port. |
495 | /// |
496 | /// Differs from [getVMServiceUriForAttach] because it only searches for a specific service. |
497 | /// This is enforced by [applicationId] being required and using either the |
498 | /// [deviceVmservicePort] or the [device]'s name to query. |
499 | Future<Uri?> getVMServiceUriForLaunch( |
500 | String applicationId, |
501 | Device device, { |
502 | bool usesIpv6 = false, |
503 | int? hostVmservicePort, |
504 | int? deviceVmservicePort, |
505 | bool useDeviceIPAsHost = false, |
506 | Duration timeout = const Duration(minutes: 10), |
507 | bool throwOnMissingLocalNetworkPermissionsError = true, |
508 | }) async { |
509 | final MDnsVmServiceDiscoveryResult? result = await queryForLaunch( |
510 | applicationId: applicationId, |
511 | deviceVmservicePort: deviceVmservicePort, |
512 | deviceName: deviceVmservicePort == null ? device.name : null, |
513 | ipv6: usesIpv6, |
514 | useDeviceIPAsHost: useDeviceIPAsHost, |
515 | timeout: timeout, |
516 | throwOnMissingLocalNetworkPermissionsError: throwOnMissingLocalNetworkPermissionsError, |
517 | ); |
518 | return _handleResult( |
519 | result, |
520 | device, |
521 | applicationId: applicationId, |
522 | deviceVmservicePort: deviceVmservicePort, |
523 | hostVmservicePort: hostVmservicePort, |
524 | usesIpv6: usesIpv6, |
525 | useDeviceIPAsHost: useDeviceIPAsHost, |
526 | ); |
527 | } |
528 | |
529 | Future<Uri?> _handleResult( |
530 | MDnsVmServiceDiscoveryResult? result, |
531 | Device device, { |
532 | String? applicationId, |
533 | int? deviceVmservicePort, |
534 | int? hostVmservicePort, |
535 | bool usesIpv6 = false, |
536 | bool useDeviceIPAsHost = false, |
537 | }) async { |
538 | if (result == null) { |
539 | await _checkForIPv4LinkLocal(device); |
540 | return null; |
541 | } |
542 | final String host; |
543 | |
544 | final InternetAddress? ipAddress = result.ipAddress; |
545 | if (useDeviceIPAsHost && ipAddress != null) { |
546 | host = ipAddress.address; |
547 | } else { |
548 | host = usesIpv6 ? InternetAddress.loopbackIPv6.address : InternetAddress.loopbackIPv4.address; |
549 | } |
550 | return buildVMServiceUri( |
551 | device, |
552 | host, |
553 | result.port, |
554 | hostVmservicePort, |
555 | result.authCode, |
556 | useDeviceIPAsHost, |
557 | ); |
558 | } |
559 | |
560 | // If there's not an ipv4 link local address in `NetworkInterfaces.list`, |
561 | // then request user interventions with a `printError()` if possible. |
562 | Future<void> _checkForIPv4LinkLocal(Device device) async { |
563 | _logger.printTrace( |
564 | 'mDNS query failed. Checking for an interface with a ipv4 link local address.' , |
565 | ); |
566 | final List<NetworkInterface> interfaces = await listNetworkInterfaces( |
567 | includeLinkLocal: true, |
568 | type: InternetAddressType.IPv4, |
569 | ); |
570 | if (_logger.isVerbose) { |
571 | _logInterfaces(interfaces); |
572 | } |
573 | final bool hasIPv4LinkLocal = interfaces.any( |
574 | (NetworkInterface interface) => |
575 | interface.addresses.any((InternetAddress address) => address.isLinkLocal), |
576 | ); |
577 | if (hasIPv4LinkLocal) { |
578 | _logger.printTrace('An interface with an ipv4 link local address was found.' ); |
579 | return; |
580 | } |
581 | final TargetPlatform targetPlatform = await device.targetPlatform; |
582 | switch (targetPlatform) { |
583 | case TargetPlatform.ios: |
584 | _analytics.send( |
585 | Event.appleUsageEvent(workflow: 'ios-mdns' , parameter: 'no-ipv4-link-local' ), |
586 | ); |
587 | _logger.printError( |
588 | 'The mDNS query for an attached iOS device failed. It may ' |
589 | 'be necessary to disable the "Personal Hotspot" on the device, and ' |
590 | 'to ensure that the "Disable unless needed" setting is unchecked ' |
591 | 'under System Preferences > Network > iPhone USB. ' |
592 | 'See https://github.com/flutter/flutter/issues/46698 for details.', |
593 | ); |
594 | case TargetPlatform.android: |
595 | case TargetPlatform.android_arm: |
596 | case TargetPlatform.android_arm64: |
597 | case TargetPlatform.android_x64: |
598 | case TargetPlatform.darwin: |
599 | case TargetPlatform.fuchsia_arm64: |
600 | case TargetPlatform.fuchsia_x64: |
601 | case TargetPlatform.linux_arm64: |
602 | case TargetPlatform.linux_x64: |
603 | case TargetPlatform.tester: |
604 | case TargetPlatform.web_javascript: |
605 | case TargetPlatform.windows_x64: |
606 | case TargetPlatform.windows_arm64: |
607 | _logger.printTrace('No interface with an ipv4 link local address was found.' ); |
608 | case TargetPlatform.unsupported: |
609 | TargetPlatform.throwUnsupportedTarget(); |
610 | } |
611 | } |
612 | |
613 | void _logInterfaces(List<NetworkInterface> interfaces) { |
614 | for (final interface in interfaces) { |
615 | if (_logger.isVerbose) { |
616 | _logger.printTrace('Found interface " ${interface.name}":' ); |
617 | for (final InternetAddress address in interface.addresses) { |
618 | final linkLocal = address.isLinkLocal ? 'link local' : '' ; |
619 | _logger.printTrace('\tBound address: " ${address.address}" $linkLocal' ); |
620 | } |
621 | } |
622 | } |
623 | } |
624 | } |
625 | |
626 | class MDnsVmServiceDiscoveryResult { |
627 | MDnsVmServiceDiscoveryResult(this.domainName, this.port, this.authCode, {this.ipAddress}); |
628 | final String domainName; |
629 | final int port; |
630 | final String authCode; |
631 | final InternetAddress? ipAddress; |
632 | } |
633 | |
634 | Future<Uri> buildVMServiceUri( |
635 | Device device, |
636 | String host, |
637 | int devicePort, [ |
638 | int? hostVmservicePort, |
639 | String? authCode, |
640 | bool useDeviceIPAsHost = false, |
641 | ]) async { |
642 | var path = '/' ; |
643 | if (authCode != null) { |
644 | path = authCode; |
645 | } |
646 | // Not having a trailing slash can cause problems in some situations. |
647 | // Ensure that there's one present. |
648 | if (!path.endsWith('/' )) { |
649 | path += '/' ; |
650 | } |
651 | hostVmservicePort ??= 0; |
652 | |
653 | final int? actualHostPort; |
654 | if (useDeviceIPAsHost) { |
655 | // When using the device's IP as the host, port forwarding is not required |
656 | // so just use the device's port. |
657 | actualHostPort = devicePort; |
658 | } else { |
659 | actualHostPort = hostVmservicePort == 0 |
660 | ? await device.portForwarder?.forward(devicePort) |
661 | : hostVmservicePort; |
662 | } |
663 | return Uri(scheme: 'http' , host: host, port: actualHostPort, path: path); |
664 | } |
665 | |