Skip to main content

ferroui_base/threading/
dispatcher_exceptions.rs

1use std::rc::Rc;
2
3use super::dispatcher_operation::DispatcherException;
4use super::{Dispatcher, DispatcherUnhandledExceptionEventArgs, DispatcherUnhandledExceptionFilterEventArgs};
5use crate::reactive::{Disposable, IDisposable};
6
7fn exception_key(exception: &DispatcherException) -> usize {
8    let payload: &(dyn std::any::Any + Send) = &**exception;
9    payload as *const (dyn std::any::Any + Send) as *const () as usize
10}
11
12impl Dispatcher {
13    /// Occurs when a callback posted with [`post`](Self::post) or
14    /// [`post_local`](Self::post_local) panics and the panic is not caught.
15    ///
16    /// A handler can mark the panic as handled, which stops it from being
17    /// re-raised on the dispatcher thread. Handlers must be written with
18    /// care to avoid secondary panics.
19    ///
20    /// Callbacks run through `invoke`/`invoke_async` do not raise this
21    /// event: their panic is delivered to whoever waits for the operation.
22    ///
23    /// # Panics
24    /// Panics when called from a thread other than the dispatcher thread.
25    pub fn unhandled_exception(
26        &self,
27        handler: impl for<'a> Fn(&DispatcherUnhandledExceptionEventArgs<'a>) + 'static,
28    ) -> Rc<dyn IDisposable> {
29        self.verify_access();
30        let Some(local) = self.try_local() else {
31            return Disposable::empty();
32        };
33        let token = local.unhandled_exception.add(Rc::new(handler));
34        let local = Rc::downgrade(&local);
35        Disposable::create(move || {
36            if let Some(local) = local.upgrade() {
37                local.unhandled_exception.remove(token);
38            }
39        })
40    }
41
42    /// Occurs first when a posted callback panics, to decide whether
43    /// [`unhandled_exception`](Self::unhandled_exception) is raised at all.
44    ///
45    /// The filter runs regardless of whether there are
46    /// `unhandled_exception` handlers. Setting
47    /// [`request_catch`](DispatcherUnhandledExceptionFilterEventArgs::set_request_catch)
48    /// to `false` skips the `unhandled_exception` event.
49    ///
50    /// # Panics
51    /// Panics when called from a thread other than the dispatcher thread.
52    pub fn unhandled_exception_filter(
53        &self,
54        handler: impl for<'a> Fn(&DispatcherUnhandledExceptionFilterEventArgs<'a>) + 'static,
55    ) -> Rc<dyn IDisposable> {
56        self.verify_access();
57        let Some(local) = self.try_local() else {
58            return Disposable::empty();
59        };
60        let token = local.unhandled_exception_filter.add(Rc::new(handler));
61        let local = Rc::downgrade(&local);
62        Disposable::create(move || {
63            if let Some(local) = local.upgrade() {
64                local.unhandled_exception_filter.remove(token);
65            }
66        })
67    }
68
69    /// Exception filter returns true if exception should be caught.
70    pub(crate) fn exception_filter(&self, e: &DispatcherException) -> bool {
71        let Some(local) = self.try_local() else {
72            return false;
73        };
74
75        // see whether this dispatcher has already seen the exception.
76        // This can happen when the dispatcher is re-entered via
77        // push_frame (or similar).
78        let key = exception_key(e);
79        {
80            let mut seen = local.seen_exceptions.borrow_mut();
81            if seen.contains(&key) {
82                // we've seen this exception before - don't catch it
83                return false;
84            }
85            // first time we've seen this exception - remember it
86            seen.push(key);
87        }
88
89        // By default, Request catch if there's anyone signed up to catch it;
90        let mut request_catch = !local.unhandled_exception.is_empty();
91
92        // The app can hook up an exception filter to avoid catching it.
93        // The filter will run REGARDLESS of whether there are exception handlers.
94        if !local.unhandled_exception_filter.is_empty() {
95            // The default request_catch value that is passed in the args
96            // should be returned unchanged if filters don't set them explicitly.
97            let dispatcher = self.to_arc();
98            let args = DispatcherUnhandledExceptionFilterEventArgs::new(&dispatcher, &**e, request_catch);
99            for (_, handler) in local.unhandled_exception_filter.snapshot().iter() {
100                handler(&args);
101            }
102            request_catch = args.request_catch();
103        }
104
105        request_catch
106    }
107
108    pub(crate) fn catch_exception(&self, e: &DispatcherException) -> bool {
109        let Some(local) = self.try_local() else {
110            return false;
111        };
112        let mut handled = false;
113
114        if !local.unhandled_exception.is_empty() {
115            let dispatcher = self.to_arc();
116            let args = DispatcherUnhandledExceptionEventArgs::new(&dispatcher, &**e, false);
117            for (_, handler) in local.unhandled_exception.snapshot().iter() {
118                handler(&args);
119            }
120            handled = args.handled();
121        }
122
123        handled
124    }
125
126    /// Returns true, if exception was handled.
127    pub(crate) fn try_catch_when(&self, e: &DispatcherException) -> bool {
128        if self.exception_filter(e) {
129            if !self.catch_exception(e) {
130                return false;
131            }
132        } else {
133            return false;
134        }
135
136        // The exception stops here and its payload is about to be released:
137        // its address must not be mistaken for a later exception.
138        self.forget_exception(e);
139        true
140    }
141
142    /// Removes the "already seen" mark of an exception.
143    pub(crate) fn forget_exception(&self, e: &DispatcherException) {
144        if let Some(local) = self.try_local() {
145            let key = exception_key(e);
146            local.seen_exceptions.borrow_mut().retain(|seen| *seen != key);
147        }
148    }
149
150    /// Removes the mark of an exception that is about to leave the
151    /// dispatcher: no operation of this dispatcher is left on the stack that
152    /// could see it again.
153    pub(crate) fn forget_exception_if_outermost(&self, e: &DispatcherException) {
154        if let Some(local) = self.try_local() {
155            if local.executing_depth.get() == 0 {
156                let key = exception_key(e);
157                local.seen_exceptions.borrow_mut().retain(|seen| *seen != key);
158            }
159        }
160    }
161}