| // 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:vm_service/vm_service.dart'; |
| import 'package:vm_service/vm_service_io.dart' as vm_service_io; |
| |
| import '../base/logger.dart'; |
| import '../device.dart'; |
| import '../mdns_discovery.dart'; |
| import '../protocol_discovery.dart'; |
| import '../reporting/reporting.dart'; |
| |
| /// A protocol for discovery of a vmservice on an attached iOS device with |
| /// multiple fallbacks. |
| /// |
| /// On versions of iOS 13 and greater, libimobiledevice can no longer listen to |
| /// logs directly. The only way to discover an active observatory is through the |
| /// mDNS protocol. However, there are a number of circumstances where this breaks |
| /// down, such as when the device is connected to certain wifi networks or with |
| /// certain hotspot connections enabled. |
| /// |
| /// Another approach to discover a vmservice is to attempt to assign a |
| /// specific port and then attempt to connect. This may fail if the port is |
| /// not available. This port value should be either random, or otherwise |
| /// generated with application specific input. This reduces the chance of |
| /// accidentally connecting to another running flutter application. |
| /// |
| /// Finally, if neither of the above approaches works, we can still attempt |
| /// to parse logs. |
| /// |
| /// To improve the overall resilience of the process, this class combines the |
| /// three discovery strategies. First it assigns a port and attempts to connect. |
| /// Then if this fails it falls back to mDNS, then finally attempting to scan |
| /// logs. |
| class FallbackDiscovery { |
| FallbackDiscovery({ |
| @required DevicePortForwarder portForwarder, |
| @required MDnsObservatoryDiscovery mDnsObservatoryDiscovery, |
| @required Logger logger, |
| @required ProtocolDiscovery protocolDiscovery, |
| Future<VmService> Function(String wsUri, {Log log}) vmServiceConnectUri = vm_service_io.vmServiceConnectUri, |
| }) : _logger = logger, |
| _mDnsObservatoryDiscovery = mDnsObservatoryDiscovery, |
| _portForwarder = portForwarder, |
| _protocolDiscovery = protocolDiscovery, |
| _vmServiceConnectUri = vmServiceConnectUri; |
| |
| static const String _kEventName = 'ios-handshake'; |
| |
| final DevicePortForwarder _portForwarder; |
| final MDnsObservatoryDiscovery _mDnsObservatoryDiscovery; |
| final Logger _logger; |
| final ProtocolDiscovery _protocolDiscovery; |
| final Future<VmService> Function(String wsUri, {Log log}) _vmServiceConnectUri; |
| |
| /// Attempt to discover the observatory port. |
| Future<Uri> discover({ |
| @required int assumedDevicePort, |
| @required String packageId, |
| @required Device deivce, |
| @required bool usesIpv6, |
| @required int hostVmservicePort, |
| @required String packageName, |
| }) async { |
| final Uri result = await _attemptServiceConnection( |
| assumedDevicePort: assumedDevicePort, |
| hostVmservicePort: hostVmservicePort, |
| packageName: packageName, |
| ); |
| if (result != null) { |
| return result; |
| } |
| |
| try { |
| final Uri result = await _mDnsObservatoryDiscovery.getObservatoryUri( |
| packageId, |
| deivce, |
| usesIpv6: usesIpv6, |
| hostVmservicePort: hostVmservicePort, |
| ); |
| if (result != null) { |
| UsageEvent(_kEventName, 'mdns-success').send(); |
| return result; |
| } |
| } on Exception catch (err) { |
| _logger.printTrace(err.toString()); |
| } |
| _logger.printTrace('Failed to connect with mDNS, falling back to log scanning'); |
| UsageEvent(_kEventName, 'mdns-failure').send(); |
| |
| try { |
| final Uri result = await _protocolDiscovery.uri; |
| UsageEvent(_kEventName, 'fallback-success').send(); |
| return result; |
| } on ArgumentError { |
| // In the event of an invalid InternetAddress, this code attempts to catch |
| // an ArgumentError from protocol_discovery.dart |
| } on Exception catch (err) { |
| _logger.printTrace(err.toString()); |
| } |
| _logger.printTrace('Failed to connect with log scanning'); |
| UsageEvent(_kEventName, 'fallback-failure').send(); |
| return null; |
| } |
| |
| // Attempt to connect to the VM service and find an isolate with a matching `packageName`. |
| // Returns `null` if no connection can be made. |
| Future<Uri> _attemptServiceConnection({ |
| @required int assumedDevicePort, |
| @required int hostVmservicePort, |
| @required String packageName, |
| }) async { |
| int hostPort; |
| Uri assumedWsUri; |
| try { |
| hostPort = await _portForwarder.forward(assumedDevicePort, hostPort: hostVmservicePort); |
| assumedWsUri = Uri.parse('ws://localhost:$hostPort/ws'); |
| } on Exception catch (err) { |
| _logger.printTrace(err.toString()); |
| _logger.printTrace('Failed to connect directly, falling back to mDNS'); |
| UsageEvent( |
| _kEventName, |
| 'failure', |
| label: err.toString(), |
| value: hostPort, |
| ).send(); |
| return null; |
| } |
| |
| // Attempt to connect to the VM service 5 times. |
| int attempts = 0; |
| const int kDelaySeconds = 2; |
| Object firstException; |
| while (attempts < 5) { |
| try { |
| final VmService vmService = await _vmServiceConnectUri(assumedWsUri.toString()); |
| final VM vm = await vmService.getVM(); |
| for (final IsolateRef isolateRefs in vm.isolates) { |
| final dynamic isolateResponse = await vmService.getIsolate(isolateRefs.id); |
| if (isolateResponse is Sentinel) { |
| // Might have been a Sentinel. Try again later. |
| throw Exception('Expected Isolate but found Sentinel: $isolateResponse'); |
| } |
| final LibraryRef library = (isolateResponse as Isolate).rootLib; |
| if (library.uri.startsWith('package:$packageName')) { |
| UsageEvent(_kEventName, 'success').send(); |
| return Uri.parse('http://localhost:$hostPort'); |
| } |
| } |
| } on Exception catch (err) { |
| // No action, we might have failed to connect. |
| firstException ??= err; |
| _logger.printTrace(err.toString()); |
| } |
| |
| // No exponential backoff is used here to keep the amount of time the |
| // tool waits for a connection to be reasonable. If the vmservice cannot |
| // be connected to in this way, the mDNS discovery must be reached |
| // sooner rather than later. |
| await Future<void>.delayed(const Duration(seconds: kDelaySeconds)); |
| attempts += 1; |
| } |
| _logger.printTrace('Failed to connect directly, falling back to mDNS'); |
| UsageEvent( |
| _kEventName, |
| 'failure', |
| label: firstException?.toString() ?? 'Connection attempts exhausted', |
| value: hostPort, |
| ).send(); |
| return null; |
| } |
| } |