diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index aaa17a1d22..240f451a9b 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -5,7 +5,7 @@ // on the dispatcher messaging system and more complex Rust data types. // use crate::helpers::translate_key; -use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER}; +use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER, PANIC_DIALOG_MESSAGE_CALLBACK}; use editor::consts::FILE_EXTENSION; use editor::messages::clipboard::utility_types::ClipboardContentRaw; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; @@ -78,6 +78,16 @@ impl EditorHandle { pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) { self.send_frontend_message_to_js(message); } + + fn initialize_handle(frontend_message_handler_callback: js_sys::Function) -> EditorHandle { + let panic_callback = frontend_message_handler_callback.clone(); + let editor_handle = EditorHandle { frontend_message_handler_callback }; + if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { + log::error!("Attempted to initialize the editor handle more than once"); + } + PANIC_DIALOG_MESSAGE_CALLBACK.with_borrow_mut(|callback| *callback = Some(panic_callback)); + editor_handle + } } #[wasm_bindgen] @@ -97,23 +107,16 @@ impl EditorHandle { uuid_random_seed, ); - let editor_handle = EditorHandle { frontend_message_handler_callback }; if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() { log::error!("Attempted to initialize the editor more than once"); } - if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { - log::error!("Attempted to initialize the editor handle more than once"); - } - editor_handle + + Self::initialize_handle(frontend_message_handler_callback) } #[cfg(feature = "native")] pub fn create(_platform: String, _uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorHandle { - let editor_handle = EditorHandle { frontend_message_handler_callback }; - if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { - log::error!("Attempted to initialize the editor handle more than once"); - } - editor_handle + Self::initialize_handle(frontend_message_handler_callback) } // Sends a message to the dispatcher in the Editor Backend diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index d3141288b3..aa756b8074 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -24,6 +24,7 @@ thread_local! { pub static EDITOR: Mutex> = const { Mutex::new(None) }; pub static MESSAGE_BUFFER: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; pub static EDITOR_HANDLE: Mutex> = const { Mutex::new(None) }; + pub static PANIC_DIALOG_MESSAGE_CALLBACK: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; } /// Initialize the backend @@ -72,12 +73,80 @@ pub fn panic_hook(info: &panic::PanicHookInfo) { log::error!("{info}"); + // Prefer using the raw JS callback to avoid mutex lock contention inside the panic hook. + // Fall back to the editor handle path if needed. + if let Err(info) = send_panic_dialog_via_callback(info) { + if let Err(info) = send_panic_dialog(info) { + send_panic_dialog_deferred(info); + } + } +} + +fn send_panic_dialog_via_callback(panic_info: String) -> Result<(), String> { + let message = FrontendMessage::DisplayDialogPanic { panic_info }; + let message_type = message.to_discriminant().local_name(); + let Ok(message_data) = serde_wasm_bindgen::to_value(&message) else { + log::error!("Failed to serialize crash dialog panic message"); + let FrontendMessage::DisplayDialogPanic { panic_info } = message else { + unreachable!("Message variant changed unexpectedly") + }; + return Err(panic_info); + }; + + PANIC_DIALOG_MESSAGE_CALLBACK.with(|callback| { + let callback_ref = callback.borrow(); + let Some(callback) = callback_ref.as_ref() else { + let FrontendMessage::DisplayDialogPanic { panic_info } = message else { + unreachable!("Message variant changed unexpectedly") + }; + return Err(panic_info); + }; + + if let Err(error) = callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data) { + log::error!("Failed to send crash dialog panic message to JS: {:?}", error); + let FrontendMessage::DisplayDialogPanic { panic_info } = message else { + unreachable!("Message variant changed unexpectedly") + }; + return Err(panic_info); + } + + Ok(()) + }) +} + +fn send_panic_dialog(panic_info: String) -> Result<(), String> { EDITOR_HANDLE.with(|editor_handle| { - let mut guard = editor_handle.lock(); - if let Ok(Some(handle)) = guard.as_deref_mut() { - handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }); + let mut guard = editor_handle.try_lock(); + let Ok(Some(handle)) = guard.as_deref_mut() else { + return Err(panic_info); + }; + + handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info }); + Ok(()) + }) +} + +#[cfg(not(feature = "native"))] +fn send_panic_dialog_deferred(panic_info: String) { + let callback = Closure::once_into_js(move || { + if send_panic_dialog(panic_info).is_err() { + log::error!("Failed to send crash dialog after panic because the editor handle is unavailable"); } }); + + let Some(window) = web_sys::window() else { + log::error!("Failed to schedule crash dialog after panic because no window exists"); + return; + }; + + if window.set_timeout_with_callback_and_timeout_and_arguments_0(callback.unchecked_ref(), 0).is_err() { + log::error!("Failed to schedule crash dialog after panic with setTimeout"); + } +} + +#[cfg(feature = "native")] +fn send_panic_dialog_deferred(_panic_info: String) { + // Native builds do not use `setTimeout`, so just log the failure in the caller's context. } #[wasm_bindgen]