Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 4 additions & 37 deletions crates/libs/reactor/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::cell::RefCell;
use std::marker::PhantomData;
use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Mutex};

use super::*;
Expand Down Expand Up @@ -133,7 +132,7 @@ fn set_exit_callback(callback: Option<Box<dyn FnOnce() + Send>>) {

fn run_exit_callback() {
if let Some(callback) = ON_EXIT.with(|slot| slot.borrow_mut().take()) {
fault::catch("app exit", callback);
fault::abort_on_panic("app exit", callback);
}
}

Expand All @@ -145,7 +144,6 @@ pub struct App {
presenter: PresenterKind,
backdrop: Option<Backdrop>,
icon: Option<String>,
on_fault: Option<Box<dyn Fn(&Fault) + Send>>,
on_exit: Option<Box<dyn FnOnce() + Send>>,
Comment on lines 144 to 147
}

Expand All @@ -164,7 +162,6 @@ impl App {
presenter: PresenterKind::Default,
backdrop: None,
icon: None,
on_fault: None,
on_exit: None,
}
}
Expand Down Expand Up @@ -214,19 +211,6 @@ impl App {
self
}

/// Set a handler invoked when a panic is caught at a reactor callback
/// boundary - an event handler, a timer tick, or the render pass. Without a
/// handler such panics are logged and execution continues (a panic that
/// reaches WinUI's `extern "system"` delegate boundary would otherwise abort
/// the process). The handler runs on the UI thread.
pub fn on_fault<F>(mut self, f: F) -> Self
where
F: Fn(&Fault) + Send + 'static,
{
self.on_fault = Some(Box::new(f));
self
}

/// Set a callback invoked on the UI thread immediately before the process exits after the
/// final reactor window closes.
pub fn on_exit<F>(mut self, f: F) -> Self
Expand All @@ -244,21 +228,16 @@ impl App {
{
init_app_platform()?;
let setup = Mutex::new(Some(setup));
let on_fault = Mutex::new(self.on_fault);
let on_exit = Mutex::new(self.on_exit);
let result_slot: Arc<Mutex<Result<()>>> = Arc::new(Mutex::new(Ok(())));
let result_slot_cb = Arc::clone(&result_slot);
let start_result =
Application::Start(&ApplicationInitializationCallback::new(move |_params| {
let inner = || -> Result<()> {
let setup = setup.lock().unwrap().take().unwrap();
let on_fault = on_fault.lock().unwrap().take();
let on_exit = on_exit.lock().unwrap().take();

let on_launched: Box<dyn FnOnce() -> Result<()>> = Box::new(move || {
if let Some(on_fault) = on_fault {
fault::set_handler(on_fault);
}
set_exit_callback(on_exit);
let app = APP_SLOT.with(|slot| slot.borrow().clone()).unwrap();
install_xaml_controls_resources(&app)?;
Expand Down Expand Up @@ -294,7 +273,6 @@ impl App {
let presenter = self.presenter;
let backdrop = self.backdrop;
let icon = self.icon;
let on_fault = Mutex::new(self.on_fault);
let on_exit = Mutex::new(self.on_exit);
if let Some(icon) = &icon
&& !std::path::Path::new(icon).is_file()
Expand All @@ -311,15 +289,11 @@ impl App {
Application::Start(&ApplicationInitializationCallback::new(move |_params| {
let inner = || -> Result<()> {
let factory = factory.lock().unwrap().take().unwrap();
let on_fault = on_fault.lock().unwrap().take();
let on_exit = on_exit.lock().unwrap().take();

let title = title.clone();
let icon = icon.clone();
let on_launched: Box<dyn FnOnce() -> Result<()>> = Box::new(move || {
if let Some(on_fault) = on_fault {
fault::set_handler(on_fault);
}
set_exit_callback(on_exit);
let app = APP_SLOT.with(|slot| slot.borrow().clone()).unwrap();
install_xaml_controls_resources(&app)?;
Expand Down Expand Up @@ -558,21 +532,14 @@ fn run_callback<F>(label: &'static str, f: F) -> Result<()>
where
F: FnOnce() -> Result<()>,
{
match std::panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => {
match fault::abort_on_panic(label, f) {
Ok(()) => Ok(()),
Err(err) => {
diagnostics::emit(&format!(
"windows_reactor: {label} callback returned error: {err:?}"
));
Err(err)
}
Err(payload) => {
let msg = diagnostics::format_panic_payload(&payload);
diagnostics::emit(&format!(
"windows_reactor: {label} callback panicked: {msg}"
));
Err(Error::new(E_FAIL, format!("{label} panicked: {msg}")))
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/libs/reactor/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,7 @@ fn render_loop<B: Backend + 'static, D: Dispatcher + 'static>(inner: &Rc<RenderH
}

fn render_once<B: Backend + 'static, D: Dispatcher + 'static>(inner: &Rc<RenderHostInner<B, D>>) {
fault::render_scope(|| render_once_inner(inner));
fault::abort_on_panic("render", || render_once_inner(inner));
}

fn render_once_inner<B: Backend + 'static, D: Dispatcher + 'static>(
Expand Down
155 changes: 16 additions & 139 deletions crates/libs/reactor/src/fault.rs
Original file line number Diff line number Diff line change
@@ -1,151 +1,28 @@
//! Central fault boundary for panics that cross a WinUI callback boundary.
//!
//! Reactor closures run behind `extern "system"` WinUI delegates that cannot
//! unwind: a panic reaching one aborts the process. This module catches panics
//! at the reactor-owned entry points (event handlers, timers, the render pass)
//! and routes them to a developer-supplied handler installed via
//! [`App::on_fault`](crate::App::on_fault), defaulting to log-and-continue.
//!
//! The catch is context-aware: callbacks invoked during a render pass are left
//! to propagate to the render boundary, while callbacks invoked outside render
//! (events, timer ticks) are caught and reported directly.
//! Fatal boundary for panics that would otherwise cross a WinUI callback.

use std::cell::{Cell, RefCell};
use std::panic::AssertUnwindSafe;
use std::rc::Rc;

use super::diagnostics;

thread_local! {
static IN_RENDER: Cell<bool> = const { Cell::new(false) };
static HANDLER: RefCell<Option<Rc<dyn Fn(&Fault)>>> = const { RefCell::new(None) };
}

/// Information about a panic caught at a reactor callback boundary.
pub struct Fault {
/// Where the fault was caught, e.g. `"event handler"`, `"render"`, `"timer"`.
pub context: &'static str,
/// The panic message.
pub message: String,
}

/// Install the per-thread fault handler. Called by [`App::on_fault`](crate::App::on_fault)
/// on the UI thread before the first render.
pub(crate) fn set_handler<F: Fn(&Fault) + 'static>(handler: F) {
HANDLER.with(|slot| *slot.borrow_mut() = Some(Rc::new(handler)));
}

/// Run `f`, catching a panic and routing it to the fault handler.
///
/// During a render pass this is a no-op wrapper so [`render_scope`] handles the
/// panic. Outside render it catches the panic and reports it under `context`.
pub(crate) fn catch<F: FnOnce()>(context: &'static str, f: F) {
if IN_RENDER.with(Cell::get) {
f();
return;
}
if let Err(payload) = std::panic::catch_unwind(AssertUnwindSafe(f)) {
dispatch(context, &*payload);
/// Run `f` and abort after reporting any panic under `context`.
pub(crate) fn abort_on_panic<T>(context: &'static str, f: impl FnOnce() -> T) -> T {
match std::panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(value) => value,
Err(payload) => abort(context, &*payload),
}
}
Comment on lines +7 to 13

/// Run the render pass `f` with the render guard set, catching any panic that
/// escapes the render pass and routes it to the fault handler under the
/// `"render"` context.
pub(crate) fn render_scope<F: FnOnce()>(f: F) {
let previous = IN_RENDER.replace(true);
let result = std::panic::catch_unwind(AssertUnwindSafe(f));
IN_RENDER.set(previous);
if let Err(payload) = result {
dispatch("render", &*payload);
}
}

/// Report an explicit failure (not a panic) to the fault handler. Used for
/// deferred best-effort work that runs behind a WinUI callback and therefore
/// cannot return its `Result` to the caller (e.g. applying the window icon,
/// backdrop, or presenter during `activate`).
/// Report an explicit failure from deferred best-effort work that cannot return
/// its `Result` to the caller.
pub(crate) fn report(context: &'static str, message: String) {
deliver(&Fault { context, message });
diagnostics::emit(&format!("windows_reactor: {context} failed: {message}"));
}

fn dispatch(context: &'static str, payload: &(dyn std::any::Any + Send)) {
deliver(&Fault {
context,
message: diagnostics::format_panic_payload(payload),
});
}

fn deliver(fault: &Fault) {
let handler = HANDLER.with(|slot| slot.borrow().clone());
match handler {
Some(handler) => {
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| handler(fault)));
}
None => diagnostics::emit(&format!(
"windows_reactor: {} fault: {}",
fault.context, fault.message
)),
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;

fn install_recorder() -> Rc<RefCell<Vec<(&'static str, String)>>> {
let log: Rc<RefCell<Vec<(&'static str, String)>>> = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&log);
set_handler(move |fault: &Fault| {
sink.borrow_mut()
.push((fault.context, fault.message.clone()));
});
log
}

#[test]
fn catch_outside_render_routes_to_handler() {
let log = install_recorder();
catch("event handler", || panic!("boom"));
let entries = log.borrow();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, "event handler");
assert_eq!(entries[0].1, "boom");
}

#[test]
fn catch_returns_normally_without_panic() {
let log = install_recorder();
let mut ran = false;
catch("event handler", || ran = true);
assert!(ran);
assert!(log.borrow().is_empty());
}

#[test]
fn catch_during_render_defers_to_render_scope() {
let log = install_recorder();
render_scope(|| {
catch("event handler", || panic!("nested"));
});
let entries = log.borrow();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, "render");
assert_eq!(entries[0].1, "nested");
assert!(!IN_RENDER.with(Cell::get));
}

#[test]
fn render_scope_resets_guard_after_panic() {
let _log = install_recorder();
render_scope(|| panic!("render boom"));
assert!(!IN_RENDER.with(Cell::get));
}

#[test]
fn a_panicking_handler_does_not_escape() {
set_handler(|_| panic!("handler explodes"));
catch("event handler", || panic!("boom"));
}
#[cold]
fn abort(context: &'static str, payload: &(dyn std::any::Any + Send)) -> ! {
let message = diagnostics::format_panic_payload(payload);
diagnostics::emit(&format!(
"windows_reactor: {context} panicked: {message}; aborting"
));
std::process::abort()
}
Comment on lines +21 to 28
4 changes: 2 additions & 2 deletions crates/libs/reactor/src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl DispatcherTimer {
timer.SetIsRepeating(repeating)?;

let tick_revoker = timer.Tick(move |_, _| {
fault::catch("timer", &f);
fault::abort_on_panic("timer", &f);
})?;
timer.Start()?;
Ok(Self {
Expand Down Expand Up @@ -69,7 +69,7 @@ where
F: Fn() + 'static,
{
let revoker = CompositionTarget::Rendering(move |_, _| {
fault::catch("rendering", &f);
fault::abort_on_panic("rendering", &f);
})?;
Ok(Rendering { _revoker: revoker })
}
Expand Down
2 changes: 1 addition & 1 deletion crates/libs/reactor/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ impl ReactorHost {
let icon = self.icon.borrow().clone();
let window = self.state.window().clone();
let handler = DispatcherQueueHandler::new(move || {
fault::catch("activate", || {
fault::abort_on_panic("activate", || {
let mut hwnd: HWND = HWND::default();
if let Ok(native) = window.cast::<IWindowNative>() {
let _ = unsafe { native.WindowHandle(&mut hwnd) };
Expand Down
2 changes: 1 addition & 1 deletion crates/libs/reactor/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ impl_rc_fn_wrapper! {

impl<T> Callback<T> {
pub fn invoke(&self, arg: T) {
fault::catch("event handler", || (self.inner)(arg));
fault::abort_on_panic("event handler", || (self.inner)(arg));
}

pub fn from_rc(inner: Rc<dyn Fn(T)>) -> Self {
Expand Down
1 change: 0 additions & 1 deletion crates/libs/reactor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ pub use canvas_bridge::{
pub use drag::*;
pub use element::*;
pub use engine::*;
pub use fault::Fault;
pub use hooks::*;
pub use host::*;
pub use interaction::*;
Expand Down
42 changes: 0 additions & 42 deletions crates/samples/reactor/samples/examples/on_fault.rs

This file was deleted.

Loading