mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
186 lines
7.0 KiB
Dart
186 lines
7.0 KiB
Dart
// 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/io.dart';
|
|
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');
|
|
_sendFailureEvent(err, assumedDevicePort);
|
|
return null;
|
|
}
|
|
|
|
// Attempt to connect to the VM service 5 times.
|
|
int attempts = 0;
|
|
const int kDelaySeconds = 2;
|
|
Exception 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');
|
|
_sendFailureEvent(firstException, assumedDevicePort);
|
|
return null;
|
|
}
|
|
|
|
void _sendFailureEvent(Exception err, int assumedDevicePort) {
|
|
String eventAction;
|
|
String eventLabel;
|
|
if (err == null) {
|
|
eventAction = 'failure-attempts-exhausted';
|
|
eventLabel = assumedDevicePort.toString();
|
|
} else if (err is HttpException) {
|
|
eventAction = 'failure-http';
|
|
eventLabel = '${err.message}, device port = $assumedDevicePort';
|
|
} else {
|
|
eventAction = 'failure-other';
|
|
eventLabel = '$err, device port = $assumedDevicePort';
|
|
}
|
|
UsageEvent(
|
|
_kEventName,
|
|
eventAction,
|
|
label: eventLabel,
|
|
).send();
|
|
}
|
|
}
|