Intercept error when iOS 18.4 crashes with JIT mode and give guided error (#164072)

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].

<!-- Links -->
[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
This commit is contained in:
Victoria Ashworth 2025-02-25 23:07:58 -06:00 committed by GitHub
parent cca82ed93b
commit aa113bd69c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 125 additions and 0 deletions

View File

@ -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<Uri?> cancelCompleter = Completer<Uri?>();
@ -743,6 +763,11 @@ class IOSDevice extends Device {
});
}
final StreamSubscription<String>? errorListener = await _interceptErrorsFromLogs(
package,
debuggingOptions: debuggingOptions,
);
final Future<Uri?> 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<StreamSubscription<String>?> _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<String> logStream = deviceLogReader.logLines;
final String deviceSdkVersion = await sdkNameAndVersion;
final StreamSubscription<String> errorListener = logStream.listen((String line) {
if (line.contains(kJITCrashFailureMessage)) {
throwToolExit(jITCrashFailureInstructions(deviceSdkVersion));
}
});
return errorListener;
}
ProtocolDiscovery _setupDebuggerAndVmServiceDiscovery({
required IOSApp package,
required Directory bundle,

View File

@ -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: <String>['--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<void> completer = Completer<void>();
// device.startApp() asynchronously calls throwToolExit, so we
// catch it in a zone.
unawaited(
runZoned<Future<void>?>(
() {
unawaited(
device.startApp(
iosApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
platformArgs: <String, dynamic>{},
),
);
return null;
},
onError: (Object error, StackTrace stack) {
expect(error.toString(), contains(jITCrashFailureInstructions('iOS 18.4')));
completer.complete();
},
),
);
await completer.future;
},
);
});
});
}