1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -04:00

eframe web: Better panic handling (#2942)

* Refactor: remove extra store of events

* Remove unnecessary extra function

* Refactor: simplify event registering

* Store panic summary

* egui_demo_app: move web-part to own module

* index.html: await

* Properly unsubscribe from events on panic

* Better error handling

* Demo app html: hide the wasm canvas and show an error message on panic

* egui_demo_app: add panic button to test panic response on web

* fix typo

* Use a constructor to create WebHandle

* Refactor: less use of locks in the interfaces

* More consistent naming
This commit is contained in:
Emil Ernerfeldt
2023-04-21 08:33:01 +02:00
committed by GitHub
parent 7f2de426d2
commit ac50fa0d94
10 changed files with 703 additions and 650 deletions

View File

@@ -1,7 +1,6 @@
use egui::{
mutex::{Mutex, MutexGuard},
TexturesDelta,
};
use std::{cell::RefCell, rc::Rc};
use egui::{mutex::Mutex, TexturesDelta};
use crate::{epi, App};
@@ -170,6 +169,64 @@ fn test_parse_query() {
// ----------------------------------------------------------------------------
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn error(msg: String);
type Error;
#[wasm_bindgen(constructor)]
fn new() -> Error;
#[wasm_bindgen(structural, method, getter)]
fn stack(error: &Error) -> String;
}
#[derive(Clone, Debug)]
pub struct PanicSummary {
message: String,
callstack: String,
}
impl PanicSummary {
pub fn new(info: &std::panic::PanicInfo<'_>) -> Self {
let message = info.to_string();
let callstack = Error::new().stack();
Self { message, callstack }
}
pub fn message(&self) -> String {
self.message.clone()
}
pub fn callstack(&self) -> String {
self.callstack.clone()
}
}
/// Handle to information about any panic than has occurred
#[derive(Clone, Default)]
pub struct PanicHandler {
summary: Option<PanicSummary>,
}
impl PanicHandler {
pub fn has_panicked(&self) -> bool {
self.summary.is_some()
}
pub fn panic_summary(&self) -> Option<PanicSummary> {
self.summary.clone()
}
pub fn on_panic(&mut self, info: &std::panic::PanicInfo<'_>) {
self.summary = Some(PanicSummary::new(info));
}
}
// ----------------------------------------------------------------------------
pub struct AppRunner {
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
@@ -183,7 +240,6 @@ pub struct AppRunner {
pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool,
textures_delta: TexturesDelta,
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
}
impl Drop for AppRunner {
@@ -277,7 +333,6 @@ impl AppRunner {
text_cursor_pos: None,
mutable_text_under_cursor: false,
textures_delta: Default::default(),
events_to_unsubscribe: Default::default(),
};
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
@@ -333,21 +388,13 @@ impl AppRunner {
Ok(())
}
pub fn destroy(&mut self) -> Result<(), JsValue> {
let is_destroyed_already = self.is_destroyed.fetch();
if is_destroyed_already {
fn destroy(&mut self) {
if self.is_destroyed.fetch() {
log::warn!("App was destroyed already");
Ok(())
} else {
log::debug!("Destroying");
for x in self.events_to_unsubscribe.drain(..) {
x.unsubscribe()?;
}
self.painter.destroy();
self.is_destroyed.set_true();
Ok(())
}
}
@@ -439,7 +486,130 @@ impl AppRunner {
// ----------------------------------------------------------------------------
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
/// This is how we access the [`AppRunner`].
/// This is cheap to clone.
#[derive(Clone)]
pub struct AppRunnerRef {
/// If we ever panic during running, this mutex is poisoned.
/// So before we use it, we need to check `panic_handler`.
runner: Rc<RefCell<AppRunner>>,
/// Have we ever panicked?
panic_handler: Arc<Mutex<PanicHandler>>,
/// In case of a panic, unsubscribe these.
/// They have to be in a separate `Arc` so that we don't need to pass them to
/// the panic handler, since they aren't `Send`.
events_to_unsubscribe: Rc<RefCell<Vec<EventToUnsubscribe>>>,
}
impl AppRunnerRef {
pub fn new(runner: AppRunner) -> Self {
Self {
runner: Rc::new(RefCell::new(runner)),
panic_handler: Arc::new(Mutex::new(Default::default())),
events_to_unsubscribe: Rc::new(RefCell::new(Default::default())),
}
}
/// Returns true if there has been a panic.
fn unsubscribe_if_panicked(&self) {
if self.panic_handler.lock().has_panicked() {
// Unsubscribe from all events so that we don't get any more callbacks
// that will try to access the poisoned runner.
let events_to_unsubscribe: Vec<_> =
std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
if !events_to_unsubscribe.is_empty() {
log::debug!(
"Unsubscribing from {} events due to panic",
events_to_unsubscribe.len()
);
for x in events_to_unsubscribe {
if let Err(err) = x.unsubscribe() {
log::error!("Failed to unsubscribe from event: {err:?}");
}
}
}
}
}
/// Returns true if there has been a panic.
pub fn has_panicked(&self) -> bool {
self.unsubscribe_if_panicked();
self.panic_handler.lock().has_panicked()
}
/// Returns `Some` if there has been a panic.
pub fn panic_summary(&self) -> Option<PanicSummary> {
self.unsubscribe_if_panicked();
self.panic_handler.lock().panic_summary()
}
pub fn destroy(&self) {
if let Some(mut runner) = self.try_lock() {
runner.destroy();
}
}
/// Returns `None` if there has been a panic, or if we have been destroyed.
/// In that case, just return to JS.
pub fn try_lock(&self) -> Option<std::cell::RefMut<'_, AppRunner>> {
if self.has_panicked() {
None
} else {
let lock = self.runner.borrow_mut();
if lock.is_destroyed.fetch() {
None
} else {
Some(lock)
}
}
}
/// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, &mut AppRunner) + 'static,
) -> Result<(), JsValue> {
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics
let runner_ref = self.clone();
Box::new(move |event: web_sys::Event| {
// Only call the wrapped closure if the egui code has not panicked
if let Some(mut runner_lock) = runner_ref.try_lock() {
// Cast the event to the expected event type
let event = event.unchecked_into::<E>();
closure(event, &mut runner_lock);
}
}) as Box<dyn FnMut(web_sys::Event)>
});
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
let handle = TargetEvent {
target: target.clone(),
event_name: event_name.to_owned(),
closure,
};
// Remember it so we unsubscribe on panic.
// Otherwise we get calls into `self.runner` after it has been poisoned by a panic.
self.events_to_unsubscribe
.borrow_mut()
.push(EventToUnsubscribe::TargetEvent(handle));
Ok(())
}
}
// ----------------------------------------------------------------------------
pub struct TargetEvent {
target: EventTarget,
@@ -454,7 +624,7 @@ pub struct IntervalHandle {
pub enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(dead_code)]
IntervalHandle(IntervalHandle),
}
@@ -477,61 +647,42 @@ impl EventToUnsubscribe {
}
}
pub struct AppRunnerContainer {
pub runner: AppRunnerRef,
/// Set to `true` if there is a panic.
/// Used to ignore callbacks after a panic.
pub panicked: Arc<AtomicBool>,
pub events: Vec<EventToUnsubscribe>,
}
impl AppRunnerContainer {
/// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&mut self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> {
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics
let runner_ref = self.runner.clone();
let panicked = self.panicked.clone();
Box::new(move |event: web_sys::Event| {
// Only call the wrapped closure if the egui code has not panicked
if !panicked.load(Ordering::SeqCst) {
// Cast the event to the expected event type
let event = event.unchecked_into::<E>();
closure(event, runner_ref.lock());
}
}) as Box<dyn FnMut(web_sys::Event)>
});
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
let handle = TargetEvent {
target: target.clone(),
event_name: event_name.to_owned(),
closure,
};
self.events.push(EventToUnsubscribe::TargetEvent(handle));
Ok(())
}
}
// ----------------------------------------------------------------------------
/// Install event listeners to register different input events
/// and start running the given app.
pub async fn start(
///
/// ``` no_run
/// #[cfg(target_arch = "wasm32")]
/// use wasm_bindgen::prelude::*;
///
/// /// This is the entry-point for all the web-assembly.
/// /// This is called from the HTML.
/// /// It loads the app, installs some callbacks, then returns.
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
/// /// You can add more callbacks like this if you want to call in to your code.
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub struct WebHandle {
/// handle: AppRunnerRef,
/// }
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub async fn start(canvas_id: &str) -> Result<WebHandle, eframe::wasm_bindgen::JsValue> {
/// let web_options = eframe::WebOptions::default();
/// eframe::start_web(
/// canvas_id,
/// web_options,
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
/// )
/// .await
/// .map(|handle| WebHandle { handle })
/// }
/// ```
///
/// # Errors
/// Failing to initialize WebGL graphics.
pub async fn start_web(
canvas_id: &str,
web_options: crate::WebOptions,
app_creator: epi::AppCreator,
@@ -544,43 +695,36 @@ pub async fn start(
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
runner.warm_up()?;
start_runner(runner, follow_system_theme)
}
let runner_ref = AppRunnerRef::new(runner);
/// Install event listeners to register different input events
/// and starts running the given [`AppRunner`].
fn start_runner(app_runner: AppRunner, follow_system_theme: bool) -> Result<AppRunnerRef, JsValue> {
let mut runner_container = AppRunnerContainer {
runner: Arc::new(Mutex::new(app_runner)),
panicked: Arc::new(AtomicBool::new(false)),
events: Vec::with_capacity(20),
};
super::events::install_canvas_events(&mut runner_container)?;
super::events::install_document_events(&mut runner_container)?;
super::events::install_window_events(&mut runner_container)?;
text_agent::install_text_agent(&mut runner_container)?;
if follow_system_theme {
super::events::install_color_scheme_change_event(&mut runner_container)?;
// Install events:
{
super::events::install_canvas_events(&runner_ref)?;
super::events::install_document_events(&runner_ref)?;
super::events::install_window_events(&runner_ref)?;
text_agent::install_text_agent(&runner_ref)?;
if follow_system_theme {
super::events::install_color_scheme_change_event(&runner_ref)?;
}
super::events::paint_and_schedule(&runner_ref)?;
}
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
// Instal panic handler:
{
// Disable all event handlers on panic
let previous_hook = std::panic::take_hook();
let panic_handler = runner_ref.panic_handler.clone();
// Disable all event handlers on panic
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
log::info!("eframe detected a panic");
panic_handler.lock().on_panic(panic_info);
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
// Propagate panic info to the previously registered panic hook
previous_hook(panic_info);
}));
}
std::panic::set_hook(Box::new(move |panic_info| {
log::info!("egui disabled all event handlers due to panic");
runner_container.panicked.store(true, SeqCst);
// Propagate panic info to the previously registered panic hook
previous_hook(panic_info);
}));
Ok(runner_container.runner)
Ok(runner_ref)
}
// ----------------------------------------------------------------------------