From aa113bd69c80f52e143ad2ad4db92d22e454f7c3 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Tue, 25 Feb 2025 23:07:58 -0600 Subject: [PATCH] Intercept error when iOS 18.4 crashes with JIT mode and give guided error (#164072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds listener to device logs during launch (before Dart VM is found) and check if iOS 18.4+ JIT crash log and give guided error message: ``` ════════════════════════════════════════════════════════════════════════════════ A change to iOS has caused a temporary break in Flutter's debug mode on physical devices. See https://github.com/flutter/flutter/issues/163984 for details. In the meantime, we recommend these temporary workarounds: * When developing with a physical device, use one running iOS 18.3 or lower. * Use a simulator for development rather than a physical device. * If you must use a device updated to iOS 18.4+, use Flutter's release or profile mode via --release or --profile flags. ════════════════════════════════════════════════════════════════════════════════ ``` Fixes https://github.com/flutter/flutter/issues/164011. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter_tools/lib/src/ios/devices.dart | 56 +++++++++++++++ .../ios/ios_device_start_prebuilt_test.dart | 69 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 58415c06b313b..4d1e5db082e28 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -39,6 +39,24 @@ import 'xcode_build_settings.dart'; import 'xcode_debug.dart'; import 'xcodeproj.dart'; +const String kJITCrashFailureMessage = + 'Crash occurred when compiling unknown function in unoptimized JIT mode in unknown pass'; + +@visibleForTesting +String jITCrashFailureInstructions(String deviceVersion) => ''' +════════════════════════════════════════════════════════════════════════════════ +A change to iOS has caused a temporary break in Flutter's debug mode on +physical devices. +See https://github.com/flutter/flutter/issues/163984 for details. + +In the meantime, we recommend these temporary workarounds: + +* When developing with a physical device, use one running iOS 18.3 or lower. +* Use a simulator for development rather than a physical device. +* If you must use a device updated to $deviceVersion, use Flutter's release or + profile mode via --release or --profile flags. +════════════════════════════════════════════════════════════════════════════════'''; + class IOSDevices extends PollingDeviceDiscovery { IOSDevices({ required Platform platform, @@ -594,6 +612,7 @@ class IOSDevice extends Device { debuggingOptions: debuggingOptions, packageId: packageId, vmServiceDiscovery: vmServiceDiscovery, + package: package, ); } else if (isWirelesslyConnected) { // Wait for the Dart VM url to be discovered via logs (from `ios-deploy`) @@ -702,6 +721,7 @@ class IOSDevice extends Device { required String packageId, required DebuggingOptions debuggingOptions, ProtocolDiscovery? vmServiceDiscovery, + IOSApp? package, }) async { Timer? maxWaitForCI; final Completer cancelCompleter = Completer(); @@ -743,6 +763,11 @@ class IOSDevice extends Device { }); } + final StreamSubscription? errorListener = await _interceptErrorsFromLogs( + package, + debuggingOptions: debuggingOptions, + ); + final Future vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch( packageId, this, @@ -771,9 +796,40 @@ class IOSDevice extends Device { } } maxWaitForCI?.cancel(); + await errorListener?.cancel(); return localUri; } + /// Listen to device logs for crash on iOS 18.4+ due to JIT restriction. If + /// found, give guided error and throw tool exit. Returns null and does not + /// listen if device is less than iOS 18.4. + Future?> _interceptErrorsFromLogs( + IOSApp? package, { + required DebuggingOptions debuggingOptions, + }) async { + // Currently only checking for kJITCrashFailureMessage, which only should + // be checked on iOS 18.4+. + if (sdkVersion == null || sdkVersion! < Version(18, 4, null)) { + return null; + } + final DeviceLogReader deviceLogReader = getLogReader( + app: package, + usingCISystem: debuggingOptions.usingCISystem, + ); + + final Stream logStream = deviceLogReader.logLines; + + final String deviceSdkVersion = await sdkNameAndVersion; + + final StreamSubscription errorListener = logStream.listen((String line) { + if (line.contains(kJITCrashFailureMessage)) { + throwToolExit(jITCrashFailureInstructions(deviceSdkVersion)); + } + }); + + return errorListener; + } + ProtocolDiscovery _setupDebuggerAndVmServiceDiscovery({ required IOSApp package, required Directory bundle, 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 485c3a75f51af..3a6ebb571ac88 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 @@ -1101,6 +1101,75 @@ void main() { MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(returnsNull: true), }, ); + + testUsingContext( + 'IOSDevice.startApp prints guided message when iOS 18.4 crashes due to JIT', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final FakeProcessManager processManager = FakeProcessManager.empty(); + + final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory + .childDirectory('flutter_empty_xcode.rand0'); + final Directory bundleLocation = fileSystem.currentDirectory; + final IOSDevice device = setUpIOSDevice( + sdkVersion: '18.4', + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceControl: FakeIOSCoreDeviceControl(), + xcodeDebug: FakeXcodeDebug( + expectedProject: XcodeDebugProject( + scheme: 'Runner', + xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'), + xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'), + hostAppProjectName: 'Runner', + ), + expectedDeviceId: '123', + expectedLaunchArguments: ['--enable-dart-profiling'], + expectedBundlePath: bundleLocation.path, + ), + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer.run(() { + deviceLogReader.addLine(kJITCrashFailureMessage); + }); + + final Completer completer = Completer(); + // device.startApp() asynchronously calls throwToolExit, so we + // catch it in a zone. + unawaited( + runZoned?>( + () { + unawaited( + device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ), + ); + return null; + }, + onError: (Object error, StackTrace stack) { + expect(error.toString(), contains(jITCrashFailureInstructions('iOS 18.4'))); + completer.complete(); + }, + ), + ); + await completer.future; + }, + ); }); }); }