Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 5cd2d4c

Browse files
authored
Support iOS wireless debugging (#118104)
* 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
1 parent 67ffaef commit 5cd2d4c

15 files changed

+1745
-330
lines changed

packages/flutter_tools/lib/src/commands/attach.dart

+71-12
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import '../base/logger.dart';
1616
import '../base/platform.dart';
1717
import '../base/signals.dart';
1818
import '../base/terminal.dart';
19-
import '../build_info.dart';
19+
import '../build_info.dart';
2020
import '../commands/daemon.dart';
2121
import '../compile.dart';
2222
import '../daemon.dart';
2323
import '../device.dart';
2424
import '../device_port_forwarder.dart';
2525
import '../fuchsia/fuchsia_device.dart';
2626
import '../ios/devices.dart';
27+
import '../ios/iproxy.dart';
2728
import '../ios/simulators.dart';
2829
import '../macos/macos_ipad_device.dart';
2930
import '../mdns_discovery.dart';
@@ -229,7 +230,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
229230
}
230231
if (debugPort != null && debugUri != null) {
231232
throwToolExit(
232-
'Either --debugPort or --debugUri can be provided, not both.');
233+
'Either --debug-port or --debug-url can be provided, not both.');
233234
}
234235

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

286-
if (debugPort == null && debugUri == null) {
288+
if ((debugPort == null && debugUri == null) || isNetworkDevice) {
287289
if (device is FuchsiaDevice) {
288290
final String module = stringArgDeprecated('module')!;
289291
if (module == null) {
@@ -303,16 +305,73 @@ known, it can be explicitly provided to attach via the command-line, e.g.
303305
rethrow;
304306
}
305307
} else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
306-
final Uri? uriFromMdns =
307-
await MDnsObservatoryDiscovery.instance!.getObservatoryUri(
308-
appId,
309-
device,
310-
usesIpv6: usesIpv6,
311-
deviceVmservicePort: deviceVmservicePort,
308+
// Protocol Discovery relies on logging. On iOS earlier than 13, logging is gathered using syslog.
309+
// syslog is not available for iOS 13+. For iOS 13+, Protocol Discovery gathers logs from the VMService.
310+
// Since we don't have access to the VMService yet, Protocol Discovery cannot be used for iOS 13+.
311+
// Also, network devices must be found using mDNS and cannot use Protocol Discovery.
312+
final bool compatibleWithProtocolDiscovery = (device is IOSDevice) &&
313+
device.majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion &&
314+
!isNetworkDevice;
315+
316+
_logger.printStatus('Waiting for a connection from Flutter on ${device.name}...');
317+
final Status discoveryStatus = _logger.startSpinner(
318+
timeout: const Duration(seconds: 30),
319+
slowWarningCallback: () {
320+
// If relying on mDNS to find Dart VM Service, remind the user to allow local network permissions.
321+
if (!compatibleWithProtocolDiscovery) {
322+
return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n\n'
323+
'Click "Allow" to the prompt asking if you would like to find and connect devices on your local network. '
324+
'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. '
325+
"If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n";
326+
}
327+
328+
return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n';
329+
},
330+
);
331+
332+
int? devicePort;
333+
if (debugPort != null) {
334+
devicePort = debugPort;
335+
} else if (debugUri != null) {
336+
devicePort = debugUri?.port;
337+
} else if (deviceVmservicePort != null) {
338+
devicePort = deviceVmservicePort;
339+
}
340+
341+
final Future<Uri?> mDNSDiscoveryFuture = MDnsVmServiceDiscovery.instance!.getVMServiceUriForAttach(
342+
appId,
343+
device,
344+
usesIpv6: usesIpv6,
345+
isNetworkDevice: isNetworkDevice,
346+
deviceVmservicePort: devicePort,
347+
);
348+
349+
Future<Uri?>? protocolDiscoveryFuture;
350+
if (compatibleWithProtocolDiscovery) {
351+
final ProtocolDiscovery vmServiceDiscovery = ProtocolDiscovery.observatory(
352+
device.getLogReader(),
353+
portForwarder: device.portForwarder,
354+
ipv6: ipv6!,
355+
devicePort: devicePort,
356+
hostPort: hostVmservicePort,
357+
logger: _logger,
312358
);
313-
observatoryUri = uriFromMdns == null
359+
protocolDiscoveryFuture = vmServiceDiscovery.uri;
360+
}
361+
362+
final Uri? foundUrl;
363+
if (protocolDiscoveryFuture == null) {
364+
foundUrl = await mDNSDiscoveryFuture;
365+
} else {
366+
foundUrl = await Future.any(
367+
<Future<Uri?>>[mDNSDiscoveryFuture, protocolDiscoveryFuture]
368+
);
369+
}
370+
discoveryStatus.stop();
371+
372+
observatoryUri = foundUrl == null
314373
? null
315-
: Stream<Uri>.value(uriFromMdns).asBroadcastStream();
374+
: Stream<Uri>.value(foundUrl).asBroadcastStream();
316375
}
317376
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
318377
if (observatoryUri == null) {
@@ -335,7 +394,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
335394
} else {
336395
observatoryUri = Stream<Uri>
337396
.fromFuture(
338-
buildObservatoryUri(
397+
buildVMServiceUri(
339398
device,
340399
debugUri?.host ?? hostname,
341400
debugPort ?? debugUri!.port,

packages/flutter_tools/lib/src/commands/drive.dart

+25-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66

7+
import 'package:args/args.dart';
78
import 'package:meta/meta.dart';
89
import 'package:package_config/package_config_types.dart';
910

@@ -21,6 +22,8 @@ import '../dart/package_map.dart';
2122
import '../device.dart';
2223
import '../drive/drive_service.dart';
2324
import '../globals.dart' as globals;
25+
import '../ios/devices.dart';
26+
import '../ios/iproxy.dart';
2427
import '../resident_runner.dart';
2528
import '../runner/flutter_command.dart' show FlutterCommandCategory, FlutterCommandResult, FlutterOptions;
2629
import '../web/web_device.dart';
@@ -203,6 +206,27 @@ class DriveCommand extends RunCommandBase {
203206
@override
204207
bool get cachePubGet => false;
205208

209+
String? get applicationBinaryPath => stringArgDeprecated(FlutterOptions.kUseApplicationBinary);
210+
211+
Future<Device?> get targetedDevice async {
212+
return findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
213+
}
214+
215+
// Network devices need `publish-port` to be enabled because it requires mDNS.
216+
// If the flag wasn't provided as an actual argument and it's a network device,
217+
// change it to be enabled.
218+
@override
219+
Future<bool> get disablePortPublication async {
220+
final ArgResults? localArgResults = argResults;
221+
final Device? device = await targetedDevice;
222+
final bool isNetworkDevice = device is IOSDevice && device.interfaceType == IOSDeviceConnectionInterface.network;
223+
if (isNetworkDevice && localArgResults != null && !localArgResults.wasParsed('publish-port')) {
224+
_logger.printTrace('Network device is being used. Changing `publish-port` to be enabled.');
225+
return false;
226+
}
227+
return !boolArgDeprecated('publish-port');
228+
}
229+
206230
@override
207231
Future<void> validateCommand() async {
208232
if (userIdentifier != null) {
@@ -223,8 +247,7 @@ class DriveCommand extends RunCommandBase {
223247
if (await _fileSystem.type(testFile) != FileSystemEntityType.file) {
224248
throwToolExit('Test file not found: $testFile');
225249
}
226-
final String? applicationBinaryPath = stringArgDeprecated(FlutterOptions.kUseApplicationBinary);
227-
final Device? device = await findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
250+
final Device? device = await targetedDevice;
228251
if (device == null) {
229252
throwToolExit(null);
230253
}

packages/flutter_tools/lib/src/commands/run.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
254254
purgePersistentCache: purgePersistentCache,
255255
deviceVmServicePort: deviceVmservicePort,
256256
hostVmServicePort: hostVmservicePort,
257-
disablePortPublication: disablePortPublication,
257+
disablePortPublication: await disablePortPublication,
258258
ddsPort: ddsPort,
259259
devToolsServerAddress: devToolsServerAddress,
260260
verboseSystemLogs: boolArgDeprecated('verbose-system-logs'),

packages/flutter_tools/lib/src/context_runner.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ Future<T> runInContext<T>(
275275
featureFlags: featureFlags,
276276
platform: globals.platform,
277277
),
278-
MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(
278+
MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
279279
logger: globals.logger,
280280
flutterUsage: globals.flutterUsage,
281281
),

packages/flutter_tools/lib/src/device.dart

+11-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'base/utils.dart';
1717
import 'build_info.dart';
1818
import 'devfs.dart';
1919
import 'device_port_forwarder.dart';
20+
import 'ios/iproxy.dart';
2021
import 'project.dart';
2122
import 'vmservice.dart';
2223

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

920-
List<String> getIOSLaunchArguments(EnvironmentType environmentType, String? route, Map<String, Object?> platformArgs) {
921+
List<String> getIOSLaunchArguments(
922+
EnvironmentType environmentType,
923+
String? route,
924+
Map<String, Object?> platformArgs, {
925+
bool ipv6 = false,
926+
IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.none
927+
}) {
921928
final String dartVmFlags = computeDartVmFlags(this);
922929
return <String>[
923930
if (enableDartProfiling) '--enable-dart-profiling',
@@ -954,6 +961,9 @@ class DebuggingOptions {
954961
// Use the suggested host port.
955962
if (environmentType == EnvironmentType.simulator && hostVmServicePort != null)
956963
'--observatory-port=$hostVmServicePort',
964+
// Tell the observatory to listen on all interfaces, don't restrict to the loopback.
965+
if (interfaceType == IOSDeviceConnectionInterface.network)
966+
'--observatory-host=${ipv6 ? '::0' : '0.0.0.0'}',
957967
];
958968
}
959969

packages/flutter_tools/lib/src/ios/devices.dart

+65-19
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:process/process.dart';
99
import 'package:vm_service/vm_service.dart' as vm_service;
1010

1111
import '../application_package.dart';
12+
import '../base/common.dart';
1213
import '../base/file_system.dart';
1314
import '../base/io.dart';
1415
import '../base/logger.dart';
@@ -21,6 +22,7 @@ import '../device.dart';
2122
import '../device_port_forwarder.dart';
2223
import '../globals.dart' as globals;
2324
import '../macos/xcdevice.dart';
25+
import '../mdns_discovery.dart';
2426
import '../project.dart';
2527
import '../protocol_discovery.dart';
2628
import '../vmservice.dart';
@@ -189,15 +191,6 @@ class IOSDevice extends Device {
189191
return majorVersionString != null ? int.tryParse(majorVersionString) ?? 0 : 0;
190192
}
191193

192-
@override
193-
bool get supportsHotReload => interfaceType == IOSDeviceConnectionInterface.usb;
194-
195-
@override
196-
bool get supportsHotRestart => interfaceType == IOSDeviceConnectionInterface.usb;
197-
198-
@override
199-
bool get supportsFlutterExit => interfaceType == IOSDeviceConnectionInterface.usb;
200-
201194
@override
202195
final String name;
203196

@@ -318,7 +311,11 @@ class IOSDevice extends Device {
318311
@visibleForTesting Duration? discoveryTimeout,
319312
}) async {
320313
String? packageId;
321-
314+
if (interfaceType == IOSDeviceConnectionInterface.network &&
315+
debuggingOptions.debuggingEnabled &&
316+
debuggingOptions.disablePortPublication) {
317+
throwToolExit('Cannot start app on wirelessly tethered iOS device. Try running again with the --publish-port flag');
318+
}
322319
if (!prebuiltApplication) {
323320
_logger.printTrace('Building ${package.name} for $id');
324321

@@ -353,8 +350,10 @@ class IOSDevice extends Device {
353350
EnvironmentType.physical,
354351
route,
355352
platformArgs,
353+
ipv6: ipv6,
354+
interfaceType: interfaceType,
356355
);
357-
final Status installStatus = _logger.startProgress(
356+
Status startAppStatus = _logger.startProgress(
358357
'Installing and launching...',
359358
);
360359
try {
@@ -379,9 +378,10 @@ class IOSDevice extends Device {
379378
deviceLogReader.debuggerStream = iosDeployDebugger;
380379
}
381380
}
381+
// Don't port foward if debugging with a network device.
382382
observatoryDiscovery = ProtocolDiscovery.observatory(
383383
deviceLogReader,
384-
portForwarder: portForwarder,
384+
portForwarder: interfaceType == IOSDeviceConnectionInterface.network ? null : portForwarder,
385385
hostPort: debuggingOptions.hostVmServicePort,
386386
devicePort: debuggingOptions.deviceVmServicePort,
387387
ipv6: ipv6,
@@ -412,12 +412,59 @@ class IOSDevice extends Device {
412412
return LaunchResult.succeeded();
413413
}
414414

415-
_logger.printTrace('Application launched on the device. Waiting for observatory url.');
416-
final Timer timer = Timer(discoveryTimeout ?? const Duration(seconds: 30), () {
417-
_logger.printError('iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...');
418-
iosDeployDebugger?.pauseDumpBacktraceResume();
415+
_logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.');
416+
417+
final int defaultTimeout = interfaceType == IOSDeviceConnectionInterface.network ? 45 : 30;
418+
final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: defaultTimeout), () {
419+
_logger.printError('The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...');
420+
421+
// If debugging with a wireless device and the timeout is reached, remind the
422+
// user to allow local network permissions.
423+
if (interfaceType == IOSDeviceConnectionInterface.network) {
424+
_logger.printError(
425+
'\nClick "Allow" to the prompt asking if you would like to find and connect devices on your local network. '
426+
'This is required for wireless debugging. If you selected "Don\'t Allow", '
427+
'you can turn it on in Settings > Your App Name > Local Network. '
428+
"If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again."
429+
);
430+
} else {
431+
iosDeployDebugger?.pauseDumpBacktraceResume();
432+
}
419433
});
420-
final Uri? localUri = await observatoryDiscovery?.uri;
434+
435+
Uri? localUri;
436+
if (interfaceType == IOSDeviceConnectionInterface.network) {
437+
// Wait for Dart VM Service to start up.
438+
final Uri? serviceURL = await observatoryDiscovery?.uri;
439+
if (serviceURL == null) {
440+
await iosDeployDebugger?.stopAndDumpBacktrace();
441+
return LaunchResult.failed();
442+
}
443+
444+
// If Dart VM Service URL with the device IP is not found within 5 seconds,
445+
// change the status message to prompt users to click Allow. Wait 5 seconds because it
446+
// should only show this message if they have not already approved the permissions.
447+
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
448+
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
449+
startAppStatus.stop();
450+
startAppStatus = _logger.startProgress(
451+
'Waiting for approval of local network permissions...',
452+
);
453+
});
454+
455+
// Get Dart VM Service URL with the device IP as the host.
456+
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
457+
packageId,
458+
this,
459+
usesIpv6: ipv6,
460+
deviceVmservicePort: serviceURL.port,
461+
isNetworkDevice: true,
462+
);
463+
464+
mDNSLookupTimer.cancel();
465+
} else {
466+
localUri = await observatoryDiscovery?.uri;
467+
}
421468
timer.cancel();
422469
if (localUri == null) {
423470
await iosDeployDebugger?.stopAndDumpBacktrace();
@@ -429,7 +476,7 @@ class IOSDevice extends Device {
429476
_logger.printError(e.message);
430477
return LaunchResult.failed();
431478
} finally {
432-
installStatus.stop();
479+
startAppStatus.stop();
433480
}
434481
}
435482

@@ -569,7 +616,6 @@ String decodeSyslog(String line) {
569616
}
570617
}
571618

572-
@visibleForTesting
573619
class IOSDeviceLogReader extends DeviceLogReader {
574620
IOSDeviceLogReader._(
575621
this._iMobileDevice,

0 commit comments

Comments
 (0)