Skip to content

Commit

Permalink
Support iOS wireless debugging (#118104)
Browse files Browse the repository at this point in the history
* setup wireless debugging to use device IP

* fix tests

* fix unused var and missing annotation

* remove unneeded try catch

* remove commented out line, change null to package id

* better way to get package id

* update mDNS lookup to continously check for server, add messaging if takes too long to find observatory url, update flutter drive to enable publish-port if using network device

* Refactor mDNS Discovery to poll for observatories and better handle multiple instances of the same app. Update drive command to make publish-port more stable. Update attach for iOS to only use Protocol Discovery if applicable, run mDNS and Protocol Discovery simultaneously, handle --debug-port/--debug-url/--device-vmservice-port, continously poll for obseravtories with mDNS, include port in error message when mutliple available

* add and update comments, use logger spinner intead of timer in flutter attach, other small improvements

* add newline to message so next log won't be on same line

* fix install/waiting for permission status progress so it doens't double print the time it took.

* only print backtrace if observatory times out on a physical usb connected device

* fix test

* Update related references from Observatory to Dart VM Service

* fix test
  • Loading branch information
vashworth committed Jan 19, 2023
1 parent 67ffaef commit 5cd2d4c
Show file tree
Hide file tree
Showing 15 changed files with 1,745 additions and 330 deletions.
83 changes: 71 additions & 12 deletions packages/flutter_tools/lib/src/commands/attach.dart
Expand Up @@ -16,14 +16,15 @@ import '../base/logger.dart';
import '../base/platform.dart';
import '../base/signals.dart';
import '../base/terminal.dart';
import '../build_info.dart';
import '../build_info.dart';
import '../commands/daemon.dart';
import '../compile.dart';
import '../daemon.dart';
import '../device.dart';
import '../device_port_forwarder.dart';
import '../fuchsia/fuchsia_device.dart';
import '../ios/devices.dart';
import '../ios/iproxy.dart';
import '../ios/simulators.dart';
import '../macos/macos_ipad_device.dart';
import '../mdns_discovery.dart';
Expand Down Expand Up @@ -229,7 +230,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
}
if (debugPort != null && debugUri != null) {
throwToolExit(
'Either --debugPort or --debugUri can be provided, not both.');
'Either --debug-port or --debug-url can be provided, not both.');
}

if (userIdentifier != null) {
Expand Down Expand Up @@ -282,8 +283,9 @@ known, it can be explicitly provided to attach via the command-line, e.g.
final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
final bool isNetworkDevice = (device is IOSDevice) && device.interfaceType == IOSDeviceConnectionInterface.network;

if (debugPort == null && debugUri == null) {
if ((debugPort == null && debugUri == null) || isNetworkDevice) {
if (device is FuchsiaDevice) {
final String module = stringArgDeprecated('module')!;
if (module == null) {
Expand All @@ -303,16 +305,73 @@ known, it can be explicitly provided to attach via the command-line, e.g.
rethrow;
}
} else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
final Uri? uriFromMdns =
await MDnsObservatoryDiscovery.instance!.getObservatoryUri(
appId,
device,
usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort,
// Protocol Discovery relies on logging. On iOS earlier than 13, logging is gathered using syslog.
// syslog is not available for iOS 13+. For iOS 13+, Protocol Discovery gathers logs from the VMService.
// Since we don't have access to the VMService yet, Protocol Discovery cannot be used for iOS 13+.
// Also, network devices must be found using mDNS and cannot use Protocol Discovery.
final bool compatibleWithProtocolDiscovery = (device is IOSDevice) &&
device.majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion &&
!isNetworkDevice;

_logger.printStatus('Waiting for a connection from Flutter on ${device.name}...');
final Status discoveryStatus = _logger.startSpinner(
timeout: const Duration(seconds: 30),
slowWarningCallback: () {
// If relying on mDNS to find Dart VM Service, remind the user to allow local network permissions.
if (!compatibleWithProtocolDiscovery) {
return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n\n'
'Click "Allow" to the prompt asking if you would like to find and connect devices on your local network. '
'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. '
"If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n";
}

return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n';
},
);

int? devicePort;
if (debugPort != null) {
devicePort = debugPort;
} else if (debugUri != null) {
devicePort = debugUri?.port;
} else if (deviceVmservicePort != null) {
devicePort = deviceVmservicePort;
}

final Future<Uri?> mDNSDiscoveryFuture = MDnsVmServiceDiscovery.instance!.getVMServiceUriForAttach(
appId,
device,
usesIpv6: usesIpv6,
isNetworkDevice: isNetworkDevice,
deviceVmservicePort: devicePort,
);

Future<Uri?>? protocolDiscoveryFuture;
if (compatibleWithProtocolDiscovery) {
final ProtocolDiscovery vmServiceDiscovery = ProtocolDiscovery.observatory(
device.getLogReader(),
portForwarder: device.portForwarder,
ipv6: ipv6!,
devicePort: devicePort,
hostPort: hostVmservicePort,
logger: _logger,
);
observatoryUri = uriFromMdns == null
protocolDiscoveryFuture = vmServiceDiscovery.uri;
}

final Uri? foundUrl;
if (protocolDiscoveryFuture == null) {
foundUrl = await mDNSDiscoveryFuture;
} else {
foundUrl = await Future.any(
<Future<Uri?>>[mDNSDiscoveryFuture, protocolDiscoveryFuture]
);
}
discoveryStatus.stop();

observatoryUri = foundUrl == null
? null
: Stream<Uri>.value(uriFromMdns).asBroadcastStream();
: Stream<Uri>.value(foundUrl).asBroadcastStream();
}
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
if (observatoryUri == null) {
Expand All @@ -335,7 +394,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
} else {
observatoryUri = Stream<Uri>
.fromFuture(
buildObservatoryUri(
buildVMServiceUri(
device,
debugUri?.host ?? hostname,
debugPort ?? debugUri!.port,
Expand Down
27 changes: 25 additions & 2 deletions packages/flutter_tools/lib/src/commands/drive.dart
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';

import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart';

Expand All @@ -21,6 +22,8 @@ import '../dart/package_map.dart';
import '../device.dart';
import '../drive/drive_service.dart';
import '../globals.dart' as globals;
import '../ios/devices.dart';
import '../ios/iproxy.dart';
import '../resident_runner.dart';
import '../runner/flutter_command.dart' show FlutterCommandCategory, FlutterCommandResult, FlutterOptions;
import '../web/web_device.dart';
Expand Down Expand Up @@ -203,6 +206,27 @@ class DriveCommand extends RunCommandBase {
@override
bool get cachePubGet => false;

String? get applicationBinaryPath => stringArgDeprecated(FlutterOptions.kUseApplicationBinary);

Future<Device?> get targetedDevice async {
return findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
}

// Network devices need `publish-port` to be enabled because it requires mDNS.
// If the flag wasn't provided as an actual argument and it's a network device,
// change it to be enabled.
@override
Future<bool> get disablePortPublication async {
final ArgResults? localArgResults = argResults;
final Device? device = await targetedDevice;
final bool isNetworkDevice = device is IOSDevice && device.interfaceType == IOSDeviceConnectionInterface.network;
if (isNetworkDevice && localArgResults != null && !localArgResults.wasParsed('publish-port')) {
_logger.printTrace('Network device is being used. Changing `publish-port` to be enabled.');
return false;
}
return !boolArgDeprecated('publish-port');
}

@override
Future<void> validateCommand() async {
if (userIdentifier != null) {
Expand All @@ -223,8 +247,7 @@ class DriveCommand extends RunCommandBase {
if (await _fileSystem.type(testFile) != FileSystemEntityType.file) {
throwToolExit('Test file not found: $testFile');
}
final String? applicationBinaryPath = stringArgDeprecated(FlutterOptions.kUseApplicationBinary);
final Device? device = await findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
final Device? device = await targetedDevice;
if (device == null) {
throwToolExit(null);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_tools/lib/src/commands/run.dart
Expand Up @@ -254,7 +254,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
purgePersistentCache: purgePersistentCache,
deviceVmServicePort: deviceVmservicePort,
hostVmServicePort: hostVmservicePort,
disablePortPublication: disablePortPublication,
disablePortPublication: await disablePortPublication,
ddsPort: ddsPort,
devToolsServerAddress: devToolsServerAddress,
verboseSystemLogs: boolArgDeprecated('verbose-system-logs'),
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_tools/lib/src/context_runner.dart
Expand Up @@ -275,7 +275,7 @@ Future<T> runInContext<T>(
featureFlags: featureFlags,
platform: globals.platform,
),
MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(
MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
logger: globals.logger,
flutterUsage: globals.flutterUsage,
),
Expand Down
12 changes: 11 additions & 1 deletion packages/flutter_tools/lib/src/device.dart
Expand Up @@ -17,6 +17,7 @@ import 'base/utils.dart';
import 'build_info.dart';
import 'devfs.dart';
import 'device_port_forwarder.dart';
import 'ios/iproxy.dart';
import 'project.dart';
import 'vmservice.dart';

Expand Down Expand Up @@ -917,7 +918,13 @@ class DebuggingOptions {
/// * https://github.com/dart-lang/sdk/blob/main/sdk/lib/html/doc/NATIVE_NULL_ASSERTIONS.md
final bool nativeNullAssertions;

List<String> getIOSLaunchArguments(EnvironmentType environmentType, String? route, Map<String, Object?> platformArgs) {
List<String> getIOSLaunchArguments(
EnvironmentType environmentType,
String? route,
Map<String, Object?> platformArgs, {
bool ipv6 = false,
IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.none
}) {
final String dartVmFlags = computeDartVmFlags(this);
return <String>[
if (enableDartProfiling) '--enable-dart-profiling',
Expand Down Expand Up @@ -954,6 +961,9 @@ class DebuggingOptions {
// Use the suggested host port.
if (environmentType == EnvironmentType.simulator && hostVmServicePort != null)
'--observatory-port=$hostVmServicePort',
// Tell the observatory to listen on all interfaces, don't restrict to the loopback.
if (interfaceType == IOSDeviceConnectionInterface.network)
'--observatory-host=${ipv6 ? '::0' : '0.0.0.0'}',
];
}

Expand Down
84 changes: 65 additions & 19 deletions packages/flutter_tools/lib/src/ios/devices.dart
Expand Up @@ -9,6 +9,7 @@ import 'package:process/process.dart';
import 'package:vm_service/vm_service.dart' as vm_service;

import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
Expand All @@ -21,6 +22,7 @@ import '../device.dart';
import '../device_port_forwarder.dart';
import '../globals.dart' as globals;
import '../macos/xcdevice.dart';
import '../mdns_discovery.dart';
import '../project.dart';
import '../protocol_discovery.dart';
import '../vmservice.dart';
Expand Down Expand Up @@ -189,15 +191,6 @@ class IOSDevice extends Device {
return majorVersionString != null ? int.tryParse(majorVersionString) ?? 0 : 0;
}

@override
bool get supportsHotReload => interfaceType == IOSDeviceConnectionInterface.usb;

@override
bool get supportsHotRestart => interfaceType == IOSDeviceConnectionInterface.usb;

@override
bool get supportsFlutterExit => interfaceType == IOSDeviceConnectionInterface.usb;

@override
final String name;

Expand Down Expand Up @@ -318,7 +311,11 @@ class IOSDevice extends Device {
@visibleForTesting Duration? discoveryTimeout,
}) async {
String? packageId;

if (interfaceType == IOSDeviceConnectionInterface.network &&
debuggingOptions.debuggingEnabled &&
debuggingOptions.disablePortPublication) {
throwToolExit('Cannot start app on wirelessly tethered iOS device. Try running again with the --publish-port flag');
}
if (!prebuiltApplication) {
_logger.printTrace('Building ${package.name} for $id');

Expand Down Expand Up @@ -353,8 +350,10 @@ class IOSDevice extends Device {
EnvironmentType.physical,
route,
platformArgs,
ipv6: ipv6,
interfaceType: interfaceType,
);
final Status installStatus = _logger.startProgress(
Status startAppStatus = _logger.startProgress(
'Installing and launching...',
);
try {
Expand All @@ -379,9 +378,10 @@ class IOSDevice extends Device {
deviceLogReader.debuggerStream = iosDeployDebugger;
}
}
// Don't port foward if debugging with a network device.
observatoryDiscovery = ProtocolDiscovery.observatory(
deviceLogReader,
portForwarder: portForwarder,
portForwarder: interfaceType == IOSDeviceConnectionInterface.network ? null : portForwarder,
hostPort: debuggingOptions.hostVmServicePort,
devicePort: debuggingOptions.deviceVmServicePort,
ipv6: ipv6,
Expand Down Expand Up @@ -412,12 +412,59 @@ class IOSDevice extends Device {
return LaunchResult.succeeded();
}

_logger.printTrace('Application launched on the device. Waiting for observatory url.');
final Timer timer = Timer(discoveryTimeout ?? const Duration(seconds: 30), () {
_logger.printError('iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...');
iosDeployDebugger?.pauseDumpBacktraceResume();
_logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.');

final int defaultTimeout = interfaceType == IOSDeviceConnectionInterface.network ? 45 : 30;
final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: defaultTimeout), () {
_logger.printError('The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...');

// If debugging with a wireless device and the timeout is reached, remind the
// user to allow local network permissions.
if (interfaceType == IOSDeviceConnectionInterface.network) {
_logger.printError(
'\nClick "Allow" to the prompt asking if you would like to find and connect devices on your local network. '
'This is required for wireless debugging. If you selected "Don\'t Allow", '
'you can turn it on in Settings > Your App Name > Local Network. '
"If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again."
);
} else {
iosDeployDebugger?.pauseDumpBacktraceResume();
}
});
final Uri? localUri = await observatoryDiscovery?.uri;

Uri? localUri;
if (interfaceType == IOSDeviceConnectionInterface.network) {
// Wait for Dart VM Service to start up.
final Uri? serviceURL = await observatoryDiscovery?.uri;
if (serviceURL == null) {
await iosDeployDebugger?.stopAndDumpBacktrace();
return LaunchResult.failed();
}

// If Dart VM Service URL with the device IP is not found within 5 seconds,
// change the status message to prompt users to click Allow. Wait 5 seconds because it
// should only show this message if they have not already approved the permissions.
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
startAppStatus.stop();
startAppStatus = _logger.startProgress(
'Waiting for approval of local network permissions...',
);
});

// Get Dart VM Service URL with the device IP as the host.
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
packageId,
this,
usesIpv6: ipv6,
deviceVmservicePort: serviceURL.port,
isNetworkDevice: true,
);

mDNSLookupTimer.cancel();
} else {
localUri = await observatoryDiscovery?.uri;
}
timer.cancel();
if (localUri == null) {
await iosDeployDebugger?.stopAndDumpBacktrace();
Expand All @@ -429,7 +476,7 @@ class IOSDevice extends Device {
_logger.printError(e.message);
return LaunchResult.failed();
} finally {
installStatus.stop();
startAppStatus.stop();
}
}

Expand Down Expand Up @@ -569,7 +616,6 @@ String decodeSyslog(String line) {
}
}

@visibleForTesting
class IOSDeviceLogReader extends DeviceLogReader {
IOSDeviceLogReader._(
this._iMobileDevice,
Expand Down

0 comments on commit 5cd2d4c

Please sign in to comment.