Skip to content

Commit

Permalink
Intercept error when iOS 18.4 crashes with JIT mode and give guided e…
Browse files Browse the repository at this point in the history
…rror (flutter#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 flutter#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 flutter#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
  • Loading branch information
vashworth authored Feb 26, 2025
1 parent cca82ed commit aa113bd
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 0 deletions.
56 changes: 56 additions & 0 deletions packages/flutter_tools/lib/src/ios/devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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?>();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
);
});
});
}
Expand Down

0 comments on commit aa113bd

Please sign in to comment.