From 3480c99b3ed7f06b88b3493134f5bc6303ff4e04 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 23:58:13 +0200 Subject: [PATCH] macOS: closing the window ends the process in on_exit, as menu Quit does (fixes #1331) The close button let eframe drop the window while AppKit's run loop was still running; AccessKit restored the content view's class and AppKit's Touch Bar KVO observer threw on the next display cycle. on_exit now ends the process on macOS once settings and library are saved and background work has stopped, as #444 does for Quit from the menu bar. Same fix as storytold/gridcraft#117. --- apps/lightcraft/src/main.rs | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index c9b9696e1..b206c58f1 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -87,6 +87,16 @@ impl eframe::App for App { if !self.0.shutdown(EXIT_WAIT) { log::warn!("quit: background work was still running after {EXIT_WAIT:?}"); } + // macOS (#1331): end the process here, before eframe tears the window down, as Quit from + // the menu bar already does (PR #444). AccessKit's macOS adapter subclasses the window's + // content view and restores its class on teardown; AppKit's Touch Bar support observes + // that view (KVO), so in the next display cycle `-[_NSTouchBarFinderObservation + // invalidate]` throws "not registered" and closing the window ends in a crash report. + // Nothing after this is needed: settings and library are saved, background work has + // stopped and the log file is unbuffered. `on_exit` only runs once the app exists, so a + // renderer that fails to start still relaunches on the next one (`window_gpu`). + #[cfg(target_os = "macos")] + std::process::exit(0); } } @@ -1207,6 +1217,35 @@ mod tests { assert!(loaded.is_some() && warning.is_none()); let _ = std::fs::remove_dir_all(&d); } + + /// #1331: on macOS the process ends inside `on_exit`, before eframe would tear the window down. + /// The check runs in a child (this test binary again), since on macOS it ends the process. + #[test] + fn on_exit_ends_the_process_on_macos() { + if std::env::var_os("LIGHTCRAFT_TEST_ON_EXIT").is_some() { + let app = LightcraftApp::new(lightcraft_engine::Session::new(), Services::default()); + let mut app = App( + app, + PrefsWriter::default(), + #[cfg(target_os = "macos")] + None, + ); + println!("calling on_exit"); + eframe::App::on_exit(&mut app, None); + println!("on_exit returned"); + return; + } + let out = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "tests::on_exit_ends_the_process_on_macos", "--nocapture"]) + .env("LIGHTCRAFT_TEST_ON_EXIT", "1") + .env("LIGHTCRAFT_NO_PREFS", "1") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(out.status.success(), "{stdout}"); + assert!(stdout.contains("calling on_exit"), "the child didn't run the test: {stdout}"); + assert_eq!(stdout.contains("on_exit returned"), !cfg!(target_os = "macos"), "{stdout}"); + } } #[cfg(test)]