Unverified Commit 69ecca55 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] iOS fallback discovery protocol (#49735)

parent d94ff4bd
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
......@@ -23,9 +24,9 @@ import '../macos/xcode.dart';
import '../mdns_discovery.dart';
import '../project.dart';
import '../protocol_discovery.dart';
import '../reporting/reporting.dart';
import '../vmservice.dart';
import 'code_signing.dart';
import 'fallback_discovery.dart';
import 'ios_workflow.dart';
import 'mac.dart';
......@@ -270,7 +271,6 @@ class IOSDevice extends Device {
bool prebuiltApplication = false,
bool ipv6 = false,
}) async {
String packageId;
if (!prebuiltApplication) {
......@@ -307,11 +307,21 @@ class IOSDevice extends Device {
return LaunchResult.failed();
}
// Step 2.5: Generate a potential open port using the provided argument,
// or randomly with the package name as a seed. Intentionally choose
// ports within the ephemeral port range.
final int assumedObservatoryPort = debuggingOptions?.deviceVmServicePort
?? math.Random(packageId.hashCode).nextInt(16383) + 49152;
// Step 3: Attempt to install the application on the device.
final List<String> launchArguments = <String>[
'--enable-dart-profiling',
// These arguments are required to support the fallback connection strategy
// described in fallback_discovery.dart.
'--enable-service-port-fallback',
'--disable-service-auth-codes',
'--observatory-port=$assumedObservatoryPort',
if (debuggingOptions.startPaused) '--start-paused',
if (debuggingOptions.disableServiceAuthCodes) '--disable-service-auth-codes',
if (debuggingOptions.dartFlags.isNotEmpty) '--dart-flags="${debuggingOptions.dartFlags}"',
if (debuggingOptions.useTestFonts) '--use-test-fonts',
// "--enable-checked-mode" and "--verify-entry-points" should always be
......@@ -331,8 +341,6 @@ class IOSDevice extends Device {
if (debuggingOptions.dumpSkpOnShaderCompilation) '--dump-skp-on-shader-compilation',
if (debuggingOptions.verboseSystemLogs) '--verbose-logging',
if (debuggingOptions.cacheSkSL) '--cache-sksl',
if (debuggingOptions.deviceVmServicePort != null)
'--observatory-port=${debuggingOptions.deviceVmServicePort}',
if (platformArgs['trace-startup'] as bool ?? false) '--trace-startup',
];
......@@ -342,11 +350,7 @@ class IOSDevice extends Device {
try {
ProtocolDiscovery observatoryDiscovery;
if (debuggingOptions.debuggingEnabled) {
// Debugging is enabled, look for the observatory server port post launch.
globals.printTrace('Debugging is enabled, connecting to observatory');
// TODO(danrubel): The Android device class does something similar to this code below.
// The various Device subclasses should be refactored and common code moved into the superclass.
observatoryDiscovery = ProtocolDiscovery.observatory(
getLogReader(app: package),
portForwarder: portForwarder,
......@@ -372,40 +376,25 @@ class IOSDevice extends Device {
return LaunchResult.succeeded();
}
Uri localUri;
try {
globals.printTrace('Application launched on the device. Waiting for observatory port.');
localUri = await MDnsObservatoryDiscovery.instance.getObservatoryUri(
packageId,
this,
final FallbackDiscovery fallbackDiscovery = FallbackDiscovery(
logger: globals.logger,
mDnsObservatoryDiscovery: MDnsObservatoryDiscovery.instance,
portForwarder: portForwarder,
protocolDiscovery: observatoryDiscovery,
);
final Uri localUri = await fallbackDiscovery.discover(
assumedDevicePort: assumedObservatoryPort,
deivce: this,
usesIpv6: ipv6,
hostVmservicePort: debuggingOptions.hostVmServicePort,
packageId: packageId,
packageName: FlutterProject.current().manifest.appName,
);
if (localUri != null) {
UsageEvent('ios-mdns', 'success').send();
return LaunchResult.succeeded(observatoryUri: localUri);
}
} catch (error) {
globals.printError('Failed to establish a debug connection with $id using mdns: $error');
if (localUri == null) {
return LaunchResult.failed();
}
// Fallback to manual protocol discovery.
UsageEvent('ios-mdns', 'failure').send();
globals.printTrace('mDNS lookup failed, attempting fallback to reading device log.');
try {
globals.printTrace('Waiting for observatory port.');
localUri = await observatoryDiscovery.uri;
if (localUri != null) {
UsageEvent('ios-mdns', 'fallback-success').send();
return LaunchResult.succeeded(observatoryUri: localUri);
}
} catch (error) {
globals.printError('Failed to establish a debug connection with $id using logs: $error');
} finally {
await observatoryDiscovery?.cancel();
}
UsageEvent('ios-mdns', 'fallback-failure').send();
return LaunchResult.failed();
} finally {
installStatus.stop();
}
......
// 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').send();
return null;
}
// Attempt to connect to the VM service 5 times.
int attempts = 0;
const int kDelaySeconds = 2;
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 Isolate isolate = await vmService.getIsolate(isolateRefs.id) as Isolate;
final LibraryRef library = 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.
_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').send();
return null;
}
}
......@@ -63,15 +63,14 @@ class MDnsObservatoryDiscovery {
)
.toList();
if (pointerRecords.isEmpty) {
globals. printTrace('No pointer records found.');
globals.printTrace('No pointer records found.');
return null;
}
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final List<String> uniqueDomainNames = pointerRecords
final Set<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet()
.toList();
.toSet();
String domainName;
if (applicationId != null) {
......
......@@ -227,6 +227,10 @@ void main() {
when(mockPortForwarder.unforward(any))
.thenAnswer((_) async => null);
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
when(mockFileSystem.currentDirectory)
.thenReturn(memoryFileSystem.currentDirectory);
const String bundlePath = '/path/to/bundle';
final List<String> installArgs = <String>[installerPath, '-i', bundlePath];
when(mockApp.deviceBundlePath).thenReturn(bundlePath);
......@@ -277,7 +281,7 @@ void main() {
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
platformArgs: <String, dynamic>{},
);
verify(mockUsage.sendEvent('ios-mdns', 'success')).called(1);
verify(mockUsage.sendEvent('ios-handshake', 'mdns-success')).called(1);
expect(launchResult.started, isTrue);
expect(launchResult.hasObservatory, isTrue);
expect(await device.stopApp(mockApp), isFalse);
......@@ -347,8 +351,8 @@ void main() {
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
platformArgs: <String, dynamic>{},
);
verify(mockUsage.sendEvent('ios-mdns', 'failure')).called(1);
verify(mockUsage.sendEvent('ios-mdns', 'fallback-success')).called(1);
verify(mockUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
verify(mockUsage.sendEvent('ios-handshake', 'fallback-success')).called(1);
expect(launchResult.started, isTrue);
expect(launchResult.hasObservatory, isTrue);
expect(await device.stopApp(mockApp), isFalse);
......@@ -380,8 +384,9 @@ void main() {
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
platformArgs: <String, dynamic>{},
);
verify(mockUsage.sendEvent('ios-mdns', 'failure')).called(1);
verify(mockUsage.sendEvent('ios-mdns', 'fallback-failure')).called(1);
verify(mockUsage.sendEvent('ios-handshake', 'failure')).called(1);
verify(mockUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
verify(mockUsage.sendEvent('ios-handshake', 'fallback-failure')).called(1);
expect(launchResult.started, isFalse);
expect(launchResult.hasObservatory, isFalse);
}, overrides: <Type, Generator>{
......@@ -669,6 +674,9 @@ void main() {
mockCache = MockCache();
when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
mockFileSystem = MockFileSystem();
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
when(mockFileSystem.currentDirectory)
.thenReturn(memoryFileSystem.currentDirectory);
mockProcessManager = MockProcessManager();
when(
mockArtifacts.getArtifactPath(
......
// 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:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/fallback_discovery.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:flutter_tools/src/protocol_discovery.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:vm_service/vm_service.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
// This test still uses `testUsingContext` due to analytics usage.
void main() {
BufferLogger logger;
FallbackDiscovery fallbackDiscovery;
MockMDnsObservatoryDiscovery mockMDnsObservatoryDiscovery;
MockPrototcolDiscovery mockPrototcolDiscovery;
MockPortForwarder mockPortForwarder;
MockVmService mockVmService;
setUp(() {
logger = BufferLogger(
terminal: AnsiTerminal(stdio: MockStdio(), platform: const LocalPlatform()),
outputPreferences: OutputPreferences.test(),
);
mockVmService = MockVmService();
mockMDnsObservatoryDiscovery = MockMDnsObservatoryDiscovery();
mockPrototcolDiscovery = MockPrototcolDiscovery();
mockPortForwarder = MockPortForwarder();
fallbackDiscovery = FallbackDiscovery(
logger: logger,
mDnsObservatoryDiscovery: mockMDnsObservatoryDiscovery,
portForwarder: mockPortForwarder,
protocolDiscovery: mockPrototcolDiscovery,
vmServiceConnectUri: (String uri, {Log log}) async {
return mockVmService;
},
);
when(mockPortForwarder.forward(23, hostPort: anyNamed('hostPort')))
.thenAnswer((Invocation invocation) async => 1);
});
testUsingContext('Selects assumed port if VM service connection is successful', () async {
when(mockVmService.getVM()).thenAnswer((Invocation invocation) async {
return VM()..isolates = <IsolateRef>[
IsolateRef(),
];
});
when(mockVmService.getIsolate(any)).thenAnswer((Invocation invocation) async {
return Isolate()
..rootLib = (LibraryRef()..uri = 'package:hello/main.dart');
});
expect(await fallbackDiscovery.discover(
assumedDevicePort: 23,
deivce: null,
hostVmservicePort: 1,
packageId: null,
usesIpv6: false,
packageName: 'hello',
), Uri.parse('http://localhost:1'));
});
testUsingContext('Selects mdns discovery if VM service connecton fails', () async {
when(mockVmService.getVM()).thenThrow(Exception());
when(mockMDnsObservatoryDiscovery.getObservatoryUri(
'hello',
null, // Device
usesIpv6: false,
hostVmservicePort: 1,
)).thenAnswer((Invocation invocation) async {
return Uri.parse('http://localhost:1234');
});
expect(await fallbackDiscovery.discover(
assumedDevicePort: 23,
deivce: null,
hostVmservicePort: 1,
packageId: 'hello',
usesIpv6: false,
packageName: 'hello',
), Uri.parse('http://localhost:1234'));
});
testUsingContext('Selects log scanning if both VM Service and mDNS fails', () async {
when(mockVmService.getVM()).thenThrow(Exception());
when(mockMDnsObservatoryDiscovery.getObservatoryUri(
'hello',
null, // Device
usesIpv6: false,
hostVmservicePort: 1,
)).thenThrow(Exception());
when(mockPrototcolDiscovery.uri).thenAnswer((Invocation invocation) async {
return Uri.parse('http://localhost:5678');
});
expect(await fallbackDiscovery.discover(
assumedDevicePort: 23,
deivce: null,
hostVmservicePort: 1,
packageId: 'hello',
usesIpv6: false,
packageName: 'hello',
), Uri.parse('http://localhost:5678'));
});
}
class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {}
class MockPrototcolDiscovery extends Mock implements ProtocolDiscovery {}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
class MockVmService extends Mock implements VmService {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment