Add publish-port flag to disable mDNS port discovery (#67452)
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index f3c520f..7fa8e81 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -55,6 +55,11 @@ }) { requiresPubspecYaml(); addEnableExperimentation(hide: !verboseHelp); + + // By default, the drive app should not publish the VM service port over mDNS + // to prevent a local network permission dialog on iOS 14+, + // which cannot be accepted or dismissed in a CI environment. + addPublishPort(enabledByDefault: false, verboseHelp: verboseHelp); argParser ..addFlag('keep-app-running', defaultsTo: null, @@ -215,7 +220,8 @@ ) : DebuggingOptions.enabled( getBuildInfo(), - port: stringArg('web-port') + port: stringArg('web-port'), + disablePortPublication: disablePortPublication, ), stayResident: false, urlTunneller: null, @@ -501,6 +507,7 @@ command.getBuildInfo(), startPaused: true, hostVmServicePort: webUri != null ? command.hostVmservicePort : 0, + disablePortPublication: command.disablePortPublication, ddsPort: command.ddsPort, verboseSystemLogs: command.verboseSystemLogs, cacheSkSL: command.cacheSkSL,
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 63c00d2..092abe1 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -97,6 +97,11 @@ usesFilesystemOptions(hide: !verboseHelp); usesExtraFrontendOptions(); addEnableExperimentation(hide: !verboseHelp); + + // By default, the app should to publish the VM service port over mDNS. + // This will allow subsequent "flutter attach" commands to connect to the VM + // without needing to know the port. + addPublishPort(enabledByDefault: true, verboseHelp: verboseHelp); argParser ..addFlag('start-paused', negatable: false, @@ -407,6 +412,7 @@ purgePersistentCache: purgePersistentCache, deviceVmServicePort: deviceVmservicePort, hostVmServicePort: hostVmservicePort, + disablePortPublication: disablePortPublication, ddsPort: ddsPort, verboseSystemLogs: boolArg('verbose-system-logs'), initializePlatform: boolArg('web-initialize-platform'),
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 769b0ee..92d4ebf 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart
@@ -813,6 +813,7 @@ this.useTestFonts = false, this.verboseSystemLogs = false, this.hostVmServicePort, + this.disablePortPublication = false, this.deviceVmServicePort, this.ddsPort, this.initializePlatform = true, @@ -855,6 +856,7 @@ purgePersistentCache = false, verboseSystemLogs = false, hostVmServicePort = null, + disablePortPublication = false, deviceVmServicePort = null, ddsPort = null, vmserviceOutFile = null, @@ -884,6 +886,7 @@ final bool initializePlatform; final int hostVmServicePort; final int deviceVmServicePort; + final bool disablePortPublication; final int ddsPort; final String port; final String hostname;
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index a7b0abd..e8e053b 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -22,7 +22,6 @@ import '../device.dart'; import '../globals.dart' as globals; import '../macos/xcode.dart'; -import '../mdns_discovery.dart'; import '../project.dart'; import '../protocol_discovery.dart'; import '../vmservice.dart'; @@ -369,6 +368,7 @@ '--enable-service-port-fallback', '--disable-service-auth-codes', '--observatory-port=$assumedObservatoryPort', + if (debuggingOptions.disablePortPublication) '--disable-observatory-publication', if (debuggingOptions.startPaused) '--start-paused', if (dartVmFlags.isNotEmpty) '--dart-flags="$dartVmFlags"', if (debuggingOptions.useTestFonts) '--use-test-fonts', @@ -453,7 +453,6 @@ _logger.printTrace('Application launched on the device. Waiting for observatory port.'); final FallbackDiscovery fallbackDiscovery = FallbackDiscovery( logger: _logger, - mDnsObservatoryDiscovery: MDnsObservatoryDiscovery.instance, portForwarder: portForwarder, protocolDiscovery: observatoryDiscovery, flutterUsage: globals.flutterUsage,
diff --git a/packages/flutter_tools/lib/src/ios/fallback_discovery.dart b/packages/flutter_tools/lib/src/ios/fallback_discovery.dart index b321114..127c0fc 100644 --- a/packages/flutter_tools/lib/src/ios/fallback_discovery.dart +++ b/packages/flutter_tools/lib/src/ios/fallback_discovery.dart
@@ -8,7 +8,6 @@ import '../base/io.dart'; import '../base/logger.dart'; import '../device.dart'; -import '../mdns_discovery.dart'; import '../protocol_discovery.dart'; import '../reporting/reporting.dart'; @@ -17,36 +16,23 @@ /// 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 +/// First, it tries to discover a vmservice by assigning 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. +/// If that does not work, attempt to scan logs from the attached debugger +/// and parse the connected port logged by the engine. class FallbackDiscovery { FallbackDiscovery({ @required DevicePortForwarder portForwarder, - @required MDnsObservatoryDiscovery mDnsObservatoryDiscovery, @required Logger logger, @required ProtocolDiscovery protocolDiscovery, @required Usage flutterUsage, @required VmServiceConnector vmServiceConnectUri, Duration pollingDelay, }) : _logger = logger, - _mDnsObservatoryDiscovery = mDnsObservatoryDiscovery, _portForwarder = portForwarder, _protocolDiscovery = protocolDiscovery, _flutterUsage = flutterUsage, @@ -56,7 +42,6 @@ static const String _kEventName = 'ios-handshake'; final DevicePortForwarder _portForwarder; - final MDnsObservatoryDiscovery _mDnsObservatoryDiscovery; final Logger _logger; final ProtocolDiscovery _protocolDiscovery; final Usage _flutterUsage; @@ -97,37 +82,13 @@ } on Exception catch (err) { _logger.printTrace(err.toString()); } - _logger.printTrace('Failed to connect with log scanning, falling back to mDNS'); + _logger.printTrace('Failed to connect with log scanning'); UsageEvent( _kEventName, 'log-failure', flutterUsage: _flutterUsage, ).send(); - try { - final Uri result = await _mDnsObservatoryDiscovery.getObservatoryUri( - packageId, - device, - usesIpv6: usesIpv6, - hostVmservicePort: hostVmservicePort, - ); - if (result != null) { - UsageEvent( - _kEventName, - 'mdns-success', - flutterUsage: _flutterUsage, - ).send(); - return result; - } - } on Exception catch (err) { - _logger.printTrace(err.toString()); - } - _logger.printTrace('Failed to connect with mDNS'); - UsageEvent( - _kEventName, - 'mdns-failure', - flutterUsage: _flutterUsage, - ).send(); return null; } @@ -200,7 +161,7 @@ await Future<void>.delayed(_pollingDelay); attempts += 1; } - _logger.printTrace('Failed to connect directly, falling back to mDNS'); + _logger.printTrace('Failed to connect directly, falling back to log scanning'); _sendFailureEvent(firstException, assumedDevicePort); return null; }
diff --git a/packages/flutter_tools/lib/src/mdns_discovery.dart b/packages/flutter_tools/lib/src/mdns_discovery.dart index 3375e8d..ac76abd 100644 --- a/packages/flutter_tools/lib/src/mdns_discovery.dart +++ b/packages/flutter_tools/lib/src/mdns_discovery.dart
@@ -51,6 +51,7 @@ /// it will return that instance's information regardless of what application /// the Observatory instance is for. // TODO(jonahwilliams): use `deviceVmservicePort` to filter mdns results. + @visibleForTesting Future<MDnsObservatoryDiscoveryResult> query({String applicationId, int deviceVmservicePort}) async { globals.printTrace('Checking for advertised Dart observatories...'); try {
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 1e3ab0f..7003262 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -369,6 +369,17 @@ return null; } + void addPublishPort({ bool enabledByDefault = true, bool verboseHelp = false }) { + argParser.addFlag('publish-port', + negatable: true, + hide: !verboseHelp, + help: 'Publish the VM service port over mDNS. Disable to prevent the' + 'local network permission app dialog in debug and profile build modes (iOS devices only.)', + defaultsTo: enabledByDefault); + } + + bool get disablePortPublication => !boolArg('publish-port'); + void usesIpv6Flag() { argParser.addFlag(ipv6Flag, hide: true,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart index fa739fa..48930cc 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
@@ -708,6 +708,11 @@ '--purge-persistent-cache', () => debuggingOptions.purgePersistentCache, ); + + testOptionThatDefaultsToFalse( + '--publish-port', + () => !debuggingOptions.disablePortPublication, + ); }); });
diff --git a/packages/flutter_tools/test/general.shard/ios/fallback_discovery_test.dart b/packages/flutter_tools/test/general.shard/ios/fallback_discovery_test.dart index 3b33719..ff7db18 100644 --- a/packages/flutter_tools/test/general.shard/ios/fallback_discovery_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/fallback_discovery_test.dart
@@ -7,7 +7,6 @@ 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:flutter_tools/src/reporting/reporting.dart'; import 'package:mockito/mockito.dart'; @@ -19,7 +18,6 @@ void main() { BufferLogger logger; FallbackDiscovery fallbackDiscovery; - MockMDnsObservatoryDiscovery mockMDnsObservatoryDiscovery; MockPrototcolDiscovery mockPrototcolDiscovery; MockPortForwarder mockPortForwarder; MockVmService mockVmService; @@ -30,12 +28,10 @@ outputPreferences: OutputPreferences.test(), ); mockVmService = MockVmService(); - mockMDnsObservatoryDiscovery = MockMDnsObservatoryDiscovery(); mockPrototcolDiscovery = MockPrototcolDiscovery(); mockPortForwarder = MockPortForwarder(); fallbackDiscovery = FallbackDiscovery( logger: logger, - mDnsObservatoryDiscovery: mockMDnsObservatoryDiscovery, portForwarder: mockPortForwarder, protocolDiscovery: mockPrototcolDiscovery, flutterUsage: Usage.test(), @@ -94,7 +90,7 @@ ), Uri.parse('http://localhost:1')); }); - testWithoutContext('Selects mdns discovery if VM service connecton fails due to Sentinel', () async { + testWithoutContext('Selects log scanning if VM service connecton fails due to Sentinel', () async { when(mockVmService.getVM()).thenAnswer((Invocation invocation) async { return VM.parse(<String, Object>{})..isolates = <IsolateRef>[ IsolateRef( @@ -106,57 +102,8 @@ }); when(mockVmService.getIsolate(any)) .thenThrow(SentinelException.parse('Something', <String, dynamic>{})); - 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, - device: null, - hostVmservicePort: 1, - packageId: 'hello', - usesIpv6: false, - packageName: 'hello', - ), Uri.parse('http://localhost:1234')); - }); - - testWithoutContext('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, - device: null, - hostVmservicePort: 1, - packageId: 'hello', - usesIpv6: false, - packageName: 'hello', - ), Uri.parse('http://localhost:1234')); - }); - - testWithoutContext('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'); + return Uri.parse('http://localhost:1234'); }); expect(await fallbackDiscovery.discover( @@ -165,12 +112,42 @@ hostVmservicePort: 1, packageId: 'hello', usesIpv6: false, + packageName: 'hello', + ), Uri.parse('http://localhost:1234')); + }); + + testWithoutContext('Selects log scanning if VM service connecton fails', () async { + when(mockVmService.getVM()).thenThrow(Exception()); + + when(mockPrototcolDiscovery.uri).thenAnswer((Invocation invocation) async { + return Uri.parse('http://localhost:1234'); + }); + + expect(await fallbackDiscovery.discover( + assumedDevicePort: 23, + device: null, + hostVmservicePort: 1, + packageId: 'hello', + usesIpv6: false, + packageName: 'hello', + ), Uri.parse('http://localhost:1234')); + }); + + testWithoutContext('Fails if both VM Service and log scanning fails', () async { + when(mockVmService.getVM()).thenThrow(Exception()); + when(mockPrototcolDiscovery.uri).thenAnswer((Invocation invocation) async => null); + + expect(await fallbackDiscovery.discover( + assumedDevicePort: 23, + device: null, + hostVmservicePort: 1, + packageId: 'hello', + usesIpv6: false, packageName: 'hello', - ), Uri.parse('http://localhost:5678')); + ), isNull); }); } -class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {} class MockPrototcolDiscovery extends Mock implements ProtocolDiscovery {} class MockPortForwarder extends Mock implements DevicePortForwarder {} class MockVmService extends Mock implements VmService {}
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 85cfd78..8035163 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -21,7 +21,6 @@ import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; -import 'package:flutter_tools/src/mdns_discovery.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:mockito/mockito.dart'; import 'package:vm_service/vm_service.dart'; @@ -123,63 +122,7 @@ verify(devicePortForwarder.dispose()).called(1); }); - // Still uses context for analytics and mDNS. - testUsingContext('IOSDevice.startApp succeeds in debug mode via mDNS discovery when log reading fails', () async { - final FileSystem fileSystem = MemoryFileSystem.test(); - final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ - kDeployCommand, - kAttachDebuggerCommand, - ]); - final IOSDevice device = setUpIOSDevice( - processManager: processManager, - fileSystem: fileSystem, - vmServiceConnector: (String string, {Log log}) async { - throw const io.SocketException( - 'OS Error: Connection refused, errno = 61, address = localhost, port ' - '= 58943', - ); - }, - ); - final IOSApp iosApp = PrebuiltIOSApp( - projectBundleId: 'app', - bundleName: 'Runner', - bundleDir: fileSystem.currentDirectory, - ); - final Uri uri = Uri( - scheme: 'http', - host: '127.0.0.1', - port: 1234, - path: 'observatory', - ); - - device.portForwarder = const NoOpDevicePortForwarder(); - device.setLogReader(iosApp, FakeDeviceLogReader()); - - when(MDnsObservatoryDiscovery.instance.getObservatoryUri( - any, - any, - usesIpv6: anyNamed('usesIpv6'), - hostVmservicePort: anyNamed('hostVmservicePort') - )).thenAnswer((Invocation invocation) async => uri); - - final LaunchResult launchResult = await device.startApp(iosApp, - prebuiltApplication: true, - debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), - platformArgs: <String, dynamic>{}, - fallbackPollingDelay: Duration.zero, - fallbackThrottleTimeout: const Duration(milliseconds: 10), - ); - - verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-success')).called(1); - expect(launchResult.started, true); - expect(launchResult.hasObservatory, true); - expect(await device.stopApp(iosApp), false); - }, overrides: <Type, Generator>{ - MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), - Usage: () => MockUsage(), - }); - - // Still uses context for analytics and mDNS. + // Still uses context for analytics. testUsingContext('IOSDevice.startApp attaches in debug mode via log reading on iOS 13+', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ @@ -223,14 +166,12 @@ expect(launchResult.started, true); expect(launchResult.hasObservatory, true); verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1); - verifyNever(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')); expect(await device.stopApp(iosApp), false); }, overrides: <Type, Generator>{ - MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), Usage: () => MockUsage(), }); - // Still uses context for analytics and mDNS. + // Still uses context for analytics. testUsingContext('IOSDevice.startApp launches in debug mode via log reading on <iOS 13', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ @@ -275,14 +216,12 @@ expect(launchResult.started, true); expect(launchResult.hasObservatory, true); verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1); - verifyNever(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')); expect(await device.stopApp(iosApp), false); }, overrides: <Type, Generator>{ - MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), Usage: () => MockUsage(), }); - // Still uses context for analytics and mDNS. + // Still uses context for analytics. testUsingContext('IOSDevice.startApp fails in debug mode when Observatory URI is malformed', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ @@ -332,9 +271,7 @@ value: anyNamed('value'), )).called(1); verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-failure')).called(1); - verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1); }, overrides: <Type, Generator>{ - MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), Usage: () => MockUsage(), }); @@ -371,7 +308,7 @@ Usage: () => MockUsage(), }); - // Still uses context for analytics and mDNS. + // Still uses context for analytics. testUsingContext('IOSDevice.startApp forwards all supported debugging options', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ @@ -397,6 +334,7 @@ '--enable-service-port-fallback', '--disable-service-auth-codes', '--observatory-port=60700', + '--disable-observatory-publication', '--start-paused', '--dart-flags="--foo,--null_assertions"', '--enable-checked-mode', @@ -449,6 +387,7 @@ BuildInfo.debug, startPaused: true, disableServiceAuthCodes: true, + disablePortPublication: true, dartFlags: '--foo', enableSoftwareRendering: true, skiaDeterministicRendering: true, @@ -470,7 +409,6 @@ expect(await device.stopApp(iosApp), false); expect(processManager.hasRemainingExpectations, false); }, overrides: <Type, Generator>{ - MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), Usage: () => MockUsage(), }); } @@ -525,6 +463,5 @@ class MockDevicePortForwarder extends Mock implements DevicePortForwarder {} class MockDeviceLogReader extends Mock implements DeviceLogReader {} class MockUsage extends Mock implements Usage {} -class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {} class MockVmService extends Mock implements VmService {} class MockDartDevelopmentService extends Mock implements DartDevelopmentService {}