Unverified Commit 4fee0079 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Inform user how to fix permissions when the observatory URL isn't found on iOS 14 (#65977)

parent b56a8c56
...@@ -310,6 +310,7 @@ class IOSDevice extends Device { ...@@ -310,6 +310,7 @@ class IOSDevice extends Device {
bool prebuiltApplication = false, bool prebuiltApplication = false,
bool ipv6 = false, bool ipv6 = false,
@visibleForTesting Duration fallbackPollingDelay, @visibleForTesting Duration fallbackPollingDelay,
@visibleForTesting Duration fallbackThrottleTimeout,
String userIdentifier, String userIdentifier,
}) async { }) async {
String packageId; String packageId;
...@@ -402,6 +403,7 @@ class IOSDevice extends Device { ...@@ -402,6 +403,7 @@ class IOSDevice extends Device {
hostPort: debuggingOptions.hostVmServicePort, hostPort: debuggingOptions.hostVmServicePort,
devicePort: debuggingOptions.deviceVmServicePort, devicePort: debuggingOptions.deviceVmServicePort,
ipv6: ipv6, ipv6: ipv6,
throttleTimeout: fallbackThrottleTimeout ?? const Duration(seconds: 1),
); );
} }
final int installationResult = await _iosDeploy.runApp( final int installationResult = await _iosDeploy.runApp(
...@@ -441,6 +443,12 @@ class IOSDevice extends Device { ...@@ -441,6 +443,12 @@ class IOSDevice extends Device {
packageName: FlutterProject.current().manifest.appName, packageName: FlutterProject.current().manifest.appName,
); );
if (localUri == null) { if (localUri == null) {
if (majorSdkVersion >= 14) {
_logger.printError('Failed to attach to the observatory.');
_logger.printError(
'Try accepting the local network permissions popup, or enable "Settings > Privacy > Local Network" for your app.');
_logger.printError('For more information, see https://flutter.dev/docs/development/ios-14#debugging-flutter');
}
return LaunchResult.failed(); return LaunchResult.failed();
} }
return LaunchResult.succeeded(observatoryUri: localUri); return LaunchResult.succeeded(observatoryUri: localUri);
......
...@@ -18,6 +18,7 @@ class ProtocolDiscovery { ...@@ -18,6 +18,7 @@ class ProtocolDiscovery {
this.serviceName, { this.serviceName, {
this.portForwarder, this.portForwarder,
this.throttleDuration, this.throttleDuration,
this.throttleTimeout,
this.hostPort, this.hostPort,
this.devicePort, this.devicePort,
this.ipv6, this.ipv6,
...@@ -34,6 +35,7 @@ class ProtocolDiscovery { ...@@ -34,6 +35,7 @@ class ProtocolDiscovery {
DeviceLogReader logReader, { DeviceLogReader logReader, {
DevicePortForwarder portForwarder, DevicePortForwarder portForwarder,
Duration throttleDuration = const Duration(milliseconds: 200), Duration throttleDuration = const Duration(milliseconds: 200),
Duration throttleTimeout,
@required int hostPort, @required int hostPort,
@required int devicePort, @required int devicePort,
@required bool ipv6, @required bool ipv6,
...@@ -44,6 +46,7 @@ class ProtocolDiscovery { ...@@ -44,6 +46,7 @@ class ProtocolDiscovery {
kObservatoryService, kObservatoryService,
portForwarder: portForwarder, portForwarder: portForwarder,
throttleDuration: throttleDuration, throttleDuration: throttleDuration,
throttleTimeout: throttleTimeout,
hostPort: hostPort, hostPort: hostPort,
devicePort: devicePort, devicePort: devicePort,
ipv6: ipv6, ipv6: ipv6,
...@@ -60,6 +63,11 @@ class ProtocolDiscovery { ...@@ -60,6 +63,11 @@ class ProtocolDiscovery {
/// The time to wait before forwarding a new observatory URIs from [logReader]. /// The time to wait before forwarding a new observatory URIs from [logReader].
final Duration throttleDuration; final Duration throttleDuration;
/// The time between URIs are discovered before timing out when scraping the [logReader].
///
/// If null, log scanning will continue indefinitely.
final Duration throttleTimeout;
StreamSubscription<String> _deviceLogSubscription; StreamSubscription<String> _deviceLogSubscription;
_BufferedStreamController<Uri> _uriStreamController; _BufferedStreamController<Uri> _uriStreamController;
...@@ -81,15 +89,21 @@ class ProtocolDiscovery { ...@@ -81,15 +89,21 @@ class ProtocolDiscovery {
/// ///
/// When a new observatory URL: is available in [logReader], /// When a new observatory URL: is available in [logReader],
/// the URLs are forwarded at most once every [throttleDuration]. /// the URLs are forwarded at most once every [throttleDuration].
/// Returns when no event has been observed for [throttleTimeout].
/// ///
/// Port forwarding is only attempted when this is invoked, /// Port forwarding is only attempted when this is invoked,
/// for each observatory URL in the stream. /// for each observatory URL in the stream.
Stream<Uri> get uris { Stream<Uri> get uris {
return _uriStreamController.stream Stream<Uri> uriStream = _uriStreamController.stream
.transform(_throttle<Uri>( .transform(_throttle<Uri>(
waitDuration: throttleDuration, waitDuration: throttleDuration,
)) ));
.asyncMap<Uri>(_forwardPort); if (throttleTimeout != null) {
// Don't throw a TimeoutException. The URL wasn't found in time, just close the stream.
uriStream = uriStream.timeout(throttleTimeout,
onTimeout: (EventSink<Uri> sink) => sink.close());
}
return uriStream.asyncMap<Uri>(_forwardPort);
} }
Future<void> cancel() => _stopScrapingLogs(); Future<void> cancel() => _stopScrapingLogs();
......
...@@ -266,6 +266,106 @@ void main() { ...@@ -266,6 +266,106 @@ void main() {
Usage: () => MockUsage(), Usage: () => MockUsage(),
}); });
group('IOSDevice.startApp on discovery failure local network permissions', () {
// Still uses context for analytics and mDNS.
testUsingContext('is not prompted on < iOS 14', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
kDeployCommand,
kLaunchDebugCommand,
]);
final BufferLogger logger = BufferLogger.test();
final IOSDevice device = setUpIOSDevice(
sdkVersion: '13.0',
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
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 FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
device.portForwarder = const NoOpDevicePortForwarder();
device.setLogReader(iosApp, deviceLogReader);
when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
.thenAnswer((Invocation invocation) async => null);
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),
);
expect(launchResult.started, false);
expect(launchResult.hasObservatory, false);
expect(logger.errorText, isEmpty);
}, overrides: <Type, Generator>{
MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
Usage: () => MockUsage(),
});
// Still uses context for analytics and mDNS.
testUsingContext('is prompted on >= iOS 14', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
kDeployCommand,
kLaunchDebugCommand,
]);
final BufferLogger logger = BufferLogger.test();
final IOSDevice device = setUpIOSDevice(
sdkVersion: '14.0',
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
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 FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
device.portForwarder = const NoOpDevicePortForwarder();
device.setLogReader(iosApp, deviceLogReader);
when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
.thenAnswer((Invocation invocation) async => null);
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),
);
expect(launchResult.started, false);
expect(launchResult.hasObservatory, false);
expect(logger.errorText, contains('Settings > Privacy > Local Network'));
}, overrides: <Type, Generator>{
MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
Usage: () => MockUsage(),
});
});
// Still uses context for TimeoutConfiguration and usage // Still uses context for TimeoutConfiguration and usage
testUsingContext('IOSDevice.startApp succeeds in release mode', () async { testUsingContext('IOSDevice.startApp succeeds in release mode', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
...@@ -430,7 +530,7 @@ IOSDevice setUpIOSDevice({ ...@@ -430,7 +530,7 @@ IOSDevice setUpIOSDevice({
fileSystem: fileSystem ?? MemoryFileSystem.test(), fileSystem: fileSystem ?? MemoryFileSystem.test(),
platform: macPlatform, platform: macPlatform,
iProxy: IProxy.test(logger: logger, processManager: processManager ?? FakeProcessManager.any()), iProxy: IProxy.test(logger: logger, processManager: processManager ?? FakeProcessManager.any()),
logger: BufferLogger.test(), logger: logger ?? BufferLogger.test(),
iosDeploy: IOSDeploy( iosDeploy: IOSDeploy(
logger: logger ?? BufferLogger.test(), logger: logger ?? BufferLogger.test(),
platform: macPlatform, platform: macPlatform,
......
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