mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 21:37:59 +00:00
Sandbox node graph execution on native targets and attempt recovery from panics on Wasm (#1846)
* Test out wasm unwinding * Implement panic catching for native targets * Hack in support for recovering panics in wasm * Keep debug info in release builds * Check for DynAnyNode in Backtrace because that can't be inlined as well * Improve error dialog * Use a mutex for storing the frontend state instead of a RefCell * Code review * Update crash text --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
06177597ae
commit
5b1d3a0ae4
8 changed files with 110 additions and 46 deletions
|
@ -12,4 +12,4 @@ rustflags = [
|
|||
]
|
||||
|
||||
[unstable]
|
||||
build-std = ["panic_abort", "std"]
|
||||
build-std = ["std"]
|
||||
|
|
|
@ -52,6 +52,9 @@ web-sys = { workspace = true, features = [
|
|||
# Optional workspace dependencies
|
||||
ron = { workspace = true, optional = true }
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
[package.metadata.wasm-pack.profile.dev]
|
||||
wasm-opt = false
|
||||
|
||||
|
@ -61,7 +64,7 @@ demangle-name-section = true
|
|||
dwarf-debug-info = true
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ["-Os"]
|
||||
wasm-opt = ["-Os", "-g"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.release.wasm-bindgen]
|
||||
debug-js-glue = false
|
||||
|
|
|
@ -71,10 +71,10 @@ impl EditorHandle {
|
|||
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
|
||||
let editor = Editor::new();
|
||||
let editor_handle = EditorHandle { frontend_message_handler_callback };
|
||||
if EDITOR.with(|editor_cell| editor_cell.set(RefCell::new(editor))).is_err() {
|
||||
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_cell| handle_cell.set(RefCell::new(editor_handle.clone()))).is_err() {
|
||||
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
|
||||
|
@ -143,18 +143,20 @@ impl EditorHandle {
|
|||
*g.borrow_mut() = Some(Closure::new(move |timestamp| {
|
||||
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
|
||||
|
||||
editor_and_handle(|editor, handle| {
|
||||
let micros: f64 = timestamp * 1000.;
|
||||
let timestamp = Duration::from_micros(micros.round() as u64);
|
||||
if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||
editor_and_handle(|editor, handle| {
|
||||
let micros: f64 = timestamp * 1000.;
|
||||
let timestamp = Duration::from_micros(micros.round() as u64);
|
||||
|
||||
for message in editor.handle_message(InputPreprocessorMessage::FrameTimeAdvance { timestamp }) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
for message in editor.handle_message(InputPreprocessorMessage::FrameTimeAdvance { timestamp }) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
|
||||
for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
});
|
||||
for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule ourself for another requestAnimationFrame callback
|
||||
request_animation_frame(f.borrow().as_ref().unwrap());
|
||||
|
@ -893,29 +895,27 @@ fn set_timeout(f: &Closure<dyn FnMut()>, delay: Duration) {
|
|||
}
|
||||
|
||||
/// Provides access to the `Editor` by calling the given closure with it as an argument.
|
||||
fn editor<T: Default>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T {
|
||||
fn editor<T>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T {
|
||||
EDITOR.with(|editor| {
|
||||
let Some(Ok(mut editor)) = editor.get().map(RefCell::try_borrow_mut) else {
|
||||
// TODO: Investigate if this should just panic instead, and if not doing so right now may be the cause of silent crashes that don't inform the user that the app has panicked
|
||||
log::error!("Failed to borrow the editor");
|
||||
return T::default();
|
||||
};
|
||||
let mut guard = editor.lock();
|
||||
let Ok(Some(ref mut editor)) = guard.as_deref_mut() else { panic!("Failed to borrow the editor") };
|
||||
|
||||
callback(&mut editor)
|
||||
callback(editor)
|
||||
})
|
||||
}
|
||||
|
||||
/// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments.
|
||||
fn editor_and_handle(mut callback: impl FnMut(&mut Editor, &mut EditorHandle)) {
|
||||
editor(|editor| {
|
||||
EDITOR_HANDLE.with(|editor_handle| {
|
||||
let Some(Ok(mut handle)) = editor_handle.get().map(RefCell::try_borrow_mut) else {
|
||||
pub(crate) fn editor_and_handle(mut callback: impl FnMut(&mut Editor, &mut EditorHandle)) {
|
||||
EDITOR_HANDLE.with(|editor_handle| {
|
||||
editor(|editor| {
|
||||
let mut guard = editor_handle.lock();
|
||||
let Ok(Some(ref mut editor_handle)) = guard.as_deref_mut() else {
|
||||
log::error!("Failed to borrow editor handle");
|
||||
return;
|
||||
};
|
||||
|
||||
// Call the closure with the editor and its handle
|
||||
callback(editor, &mut handle);
|
||||
callback(editor, editor_handle);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
@ -937,13 +937,18 @@ async fn poll_node_graph_evaluation() {
|
|||
}
|
||||
}
|
||||
|
||||
// Clear the error display if there are no more errors
|
||||
if !messages.is_empty() {
|
||||
crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// Send each `FrontendMessage` to the JavaScript frontend
|
||||
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
|
||||
handle.send_frontend_message_to_js(response);
|
||||
}
|
||||
|
||||
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn auto_save_all_documents() {
|
||||
|
|
|
@ -9,17 +9,19 @@ pub mod helpers;
|
|||
|
||||
use editor::messages::prelude::*;
|
||||
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
use std::panic;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
// Set up the persistent editor backend state
|
||||
pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false);
|
||||
pub static NODE_GRAPH_ERROR_DISPLAYED: AtomicBool = AtomicBool::new(false);
|
||||
pub static LOGGER: WasmLog = WasmLog;
|
||||
|
||||
thread_local! {
|
||||
pub static EDITOR: OnceCell<RefCell<editor::application::Editor>> = const { OnceCell::new() };
|
||||
pub static EDITOR_HANDLE: OnceCell<RefCell<editor_api::EditorHandle>> = const { OnceCell::new() };
|
||||
pub static EDITOR: Mutex<Option<editor::application::Editor>> = const { Mutex::new(None) };
|
||||
pub static EDITOR_HANDLE: Mutex<Option<editor_api::EditorHandle>> = const { Mutex::new(None) };
|
||||
}
|
||||
|
||||
/// Initialize the backend
|
||||
|
@ -35,16 +37,44 @@ pub fn init_graphite() {
|
|||
|
||||
/// When a panic occurs, notify the user and log the error to the JS console before the backend dies
|
||||
pub fn panic_hook(info: &panic::PanicInfo) {
|
||||
EDITOR_HAS_CRASHED.store(true, Ordering::SeqCst);
|
||||
let info = info.to_string();
|
||||
let backtrace = Error::new("stack").stack().to_string();
|
||||
if backtrace.contains("DynAnyNode") {
|
||||
log::error!("Node graph evaluation panicked {info}");
|
||||
|
||||
error!("{info}");
|
||||
// When the graph panics, the node runtime lock may not be released properly
|
||||
if editor::node_graph_executor::NODE_RUNTIME.try_lock().is_none() {
|
||||
unsafe { editor::node_graph_executor::NODE_RUNTIME.force_unlock() };
|
||||
}
|
||||
|
||||
if !NODE_GRAPH_ERROR_DISPLAYED.load(Ordering::SeqCst) {
|
||||
NODE_GRAPH_ERROR_DISPLAYED.store(true, Ordering::SeqCst);
|
||||
editor_api::editor_and_handle(|_, handle| {
|
||||
let error = r#"
|
||||
<rect x="50%" y="50%" width="600" height="100" transform="translate(-300 -50)" rx="4" fill="var(--color-error-red)" />
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)">
|
||||
<tspan x="50%" dy="-24" font-weight="bold">The document crashed while being rendered in its current state.</tspan>
|
||||
<tspan x="50%" dy="24">The editor is now unstable! Undo your last action to restore the artwork,</tspan>
|
||||
<tspan x="50%" dy="24">then save your document and restart the editor before continuing work.</tspan>
|
||||
/text>"#
|
||||
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
|
||||
.to_string();
|
||||
handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::UpdateDocumentArtwork { svg: error });
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
EDITOR_HAS_CRASHED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
log::error!("{info}");
|
||||
|
||||
EDITOR_HANDLE.with(|editor_handle| {
|
||||
editor_handle.get().map(|handle| {
|
||||
handle
|
||||
.borrow_mut()
|
||||
.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() })
|
||||
})
|
||||
let mut guard = editor_handle.lock();
|
||||
if let Ok(Some(ref mut handle)) = guard.as_deref_mut() {
|
||||
handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -56,6 +86,9 @@ extern "C" {
|
|||
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(msg: &str) -> Error;
|
||||
|
||||
#[wasm_bindgen(structural, method, getter)]
|
||||
fn stack(error: &Error) -> String;
|
||||
}
|
||||
|
||||
/// Logging to the JS console
|
||||
|
@ -69,6 +102,8 @@ extern "C" {
|
|||
fn warn(msg: &str, format: &str);
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn error(msg: &str, format: &str);
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn trace(msg: &str, format: &str);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue