Skip to content

Commit 2a97da2

Browse files
lklimekclaude
andcommitted
fix(macos): order windows out on exit to prevent KVO crash
On macOS, winit's event handler teardown triggers a display cycle flush that finds _NSTouchBarFinderObservation KVO observers on objects in an inconsistent state, causing a SIGILL crash via _crashOnException:. Fix: call [window orderOut:nil] on all windows as the first action in on_exit(), before returning control to eframe/winit. This lets AppKit properly tear down its display-related observations while the windows and their views are still alive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5aaa3c9 commit 2a97da2

3 files changed

Lines changed: 55 additions & 0 deletions

File tree

src/app.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,11 @@ impl App for AppState {
16461646
}
16471647

16481648
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
1649+
// On macOS, order windows out before winit tears down the event
1650+
// handler. This lets AppKit properly clean up display-related KVO
1651+
// observers (TouchBar, etc.) while views are still alive.
1652+
crate::platform::order_out_all_windows();
1653+
16491654
// If shutdown_receiver is Some, the async shutdown was already initiated
16501655
// in update(). Skip the blocking fallback to avoid double-shutdown.
16511656
// The blocking path only runs when the window was force-closed without

src/platform/macos.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@ use objc2::MainThreadMarker;
22
use objc2::msg_send;
33
use objc2::runtime::{AnyClass, AnyObject};
44

5+
/// Orders all application windows out (hides them) so that AppKit can
6+
/// properly tear down display-related observations — in particular
7+
/// `_NSTouchBarFinderObservation` KVO observers — while the windows and
8+
/// their views are still alive.
9+
///
10+
/// Without this, winit's event handler teardown triggers a display cycle
11+
/// flush that finds KVO observers on inconsistent objects, causing a
12+
/// SIGILL crash via `_crashOnException:`.
13+
///
14+
/// Must be called from `on_exit()` before returning control to the
15+
/// eframe/winit shutdown path.
16+
pub fn order_out_all_windows(_mtm: MainThreadMarker) {
17+
unsafe {
18+
let Some(cls) = AnyClass::get(c"NSApplication") else {
19+
return;
20+
};
21+
let app: *mut AnyObject = msg_send![cls, sharedApplication];
22+
if app.is_null() {
23+
return;
24+
}
25+
26+
let windows: *mut AnyObject = msg_send![app, windows];
27+
if windows.is_null() {
28+
return;
29+
}
30+
31+
let count: usize = msg_send![windows, count];
32+
for i in 0..count {
33+
let window: *mut AnyObject = msg_send![windows, objectAtIndex: i];
34+
if !window.is_null() {
35+
let _: () = msg_send![window, orderOut: std::ptr::null::<AnyObject>()];
36+
}
37+
}
38+
tracing::debug!("Ordered out {count} windows for clean shutdown");
39+
}
40+
}
41+
542
/// Queries `accessibilityChildren` on the key window's content view to force
643
/// macOS to call the AccessKit adapter's subclassed method, which transitions
744
/// the adapter from `Inactive` to `Active`. Without this, tools like Peekaboo

src/platform/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,16 @@ pub fn force_accessibility_activation() -> bool {
2929
true
3030
}
3131
}
32+
33+
/// Orders all windows out before shutdown so macOS can clean up
34+
/// display-related observations. No-op on non-macOS platforms.
35+
pub fn order_out_all_windows() {
36+
#[cfg(target_os = "macos")]
37+
{
38+
let Some(mtm) = objc2::MainThreadMarker::new() else {
39+
tracing::error!("order_out_all_windows called from non-main thread");
40+
return;
41+
};
42+
macos::order_out_all_windows(mtm);
43+
}
44+
}

0 commit comments

Comments
 (0)