Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 31 additions & 42 deletions packages/flutter_tools/lib/src/ios/devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -270,7 +271,6 @@ class IOSDevice extends Device {
bool prebuiltApplication = false,
bool ipv6 = false,
}) async {

String packageId;

if (!prebuiltApplication) {
Expand Down Expand Up @@ -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
Expand All @@ -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',
];

Expand All @@ -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,
Expand All @@ -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,
usesIpv6: ipv6,
hostVmservicePort: debuggingOptions.hostVmServicePort,
);
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');
}

// 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();
globals.printTrace('Application launched on the device. Waiting for observatory port.');
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) {
return LaunchResult.failed();
}
UsageEvent('ios-mdns', 'fallback-failure').send();
return LaunchResult.failed();
return LaunchResult.succeeded(observatoryUri: localUri);
} finally {
installStatus.stop();
}
Expand Down
158 changes: 158 additions & 0 deletions packages/flutter_tools/lib/src/ios/fallback_discovery.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// 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;
}
}
7 changes: 3 additions & 4 deletions packages/flutter_tools/lib/src/mdns_discovery.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 13 additions & 5 deletions packages/flutter_tools/test/general.shard/ios/devices_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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>{
Expand Down Expand Up @@ -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(
Expand Down
Loading