Skip to main content

rtb_error/
hook.rs

1//! Hook installation helpers.
2//!
3//! `miette` stores its error hook in a process-global [`OnceLock`], which
4//! means `miette::set_hook` succeeds only on the first call and refuses
5//! subsequent attempts with `InstallError`. To give callers mutable,
6//! idempotent semantics — swap the footer at any time, call
7//! `install_*` twice without panicking — we install a single wrapper
8//! handler that reads from our own, mutable footer slot at render time.
9//!
10//! The net effect for callers: all three `install_*` functions are safe
11//! to call in any order, any number of times.
12
13use std::fmt;
14use std::sync::{OnceLock, RwLock};
15
16use miette::{Diagnostic, GraphicalReportHandler, ReportHandler};
17
18type Footer = Box<dyn Fn() -> String + Send + Sync + 'static>;
19
20/// Global footer slot. Read on every diagnostic render.
21static FOOTER: OnceLock<RwLock<Option<Footer>>> = OnceLock::new();
22
23fn footer_slot() -> &'static RwLock<Option<Footer>> {
24    FOOTER.get_or_init(|| RwLock::new(None))
25}
26
27/// Install the default `miette` graphical report handler.
28///
29/// Idempotent. Safe to call from `main()` before `tokio::main`
30/// expansion or from inside an `Application::run()` invocation.
31///
32/// If another caller (including a previous call to this function, to
33/// [`install_with_footer`], or to `miette::set_hook` directly) has
34/// already installed a hook, this call is a no-op — the existing hook
35/// is preserved.
36pub fn install_report_handler() {
37    // Prime the footer slot so concurrent callers can't observe a
38    // half-initialised state when we install the hook below.
39    let _ = footer_slot();
40
41    let _ = miette::set_hook(Box::new(|_| Box::new(RtbReportHandler::new())));
42}
43
44/// Install the `miette` panic hook, routing panics through the same
45/// graphical report pipeline.
46///
47/// Idempotent — `std::panic::set_hook` simply overwrites any previous
48/// hook, so calling twice leaves miette's renderer in place.
49pub fn install_panic_hook() {
50    miette::set_panic_hook();
51}
52
53/// Install the report handler (if not already) and register a closure
54/// that appends a tool-specific support footer to every rendered
55/// diagnostic.
56///
57/// `footer` is called on every diagnostic render and may return an
58/// empty string to suppress the footer for that render. Replacing the
59/// footer is permitted — the most recent call wins.
60pub fn install_with_footer<F>(footer: F)
61where
62    F: Fn() -> String + Send + Sync + 'static,
63{
64    install_report_handler();
65    let mut guard =
66        footer_slot().write().expect("footer lock poisoned — another thread panicked mid-update");
67    *guard = Some(Box::new(footer));
68}
69
70/// `rtb-error`'s `ReportHandler` implementation.
71///
72/// Delegates the structural render to `miette`'s `GraphicalReportHandler`
73/// and appends the currently-registered footer, if any. The footer is
74/// resolved at render time, not install time, so post-install updates
75/// are visible immediately.
76struct RtbReportHandler {
77    inner: GraphicalReportHandler,
78}
79
80impl RtbReportHandler {
81    fn new() -> Self {
82        Self { inner: GraphicalReportHandler::new() }
83    }
84}
85
86thread_local! {
87    /// Re-entry guard for [`RtbReportHandler::debug`]. When a footer
88    /// closure panics and the installed `miette` panic hook is
89    /// active, the hook may render the panic *through this same
90    /// handler* — which would re-invoke the (panicking) footer and
91    /// produce a double-panic abort. This flag makes the nested
92    /// render skip the footer step entirely.
93    static IN_RENDER: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
94}
95
96impl ReportHandler for RtbReportHandler {
97    fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        self.inner.render_report(f, diagnostic)?;
99
100        // Skip the footer if we're already inside a render (we're
101        // being re-entered by miette's panic hook rendering a panic
102        // that occurred inside *our* footer closure).
103        if IN_RENDER.with(std::cell::Cell::get) {
104            return Ok(());
105        }
106
107        // Capture the footer closure's output with `catch_unwind` so a
108        // panicking closure cannot poison the `FOOTER` RwLock. The
109        // read guard is released before we emit to the formatter.
110        let maybe_text = {
111            let Some(slot) = FOOTER.get() else {
112                return Ok(());
113            };
114            let Ok(guard) = slot.read() else {
115                // Lock poisoned by a concurrent writer; suppress.
116                return Ok(());
117            };
118            let Some(footer) = guard.as_ref() else {
119                return Ok(());
120            };
121
122            // Mark re-entry so a panic during the footer call, if it
123            // recurses through the installed panic hook, short-
124            // circuits without invoking the footer again.
125            IN_RENDER.with(|flag| flag.set(true));
126            // clippy::redundant_closure is triggered here because
127            // `footer` is callable, but we need to wrap in a closure
128            // so `catch_unwind` has a `FnOnce() -> String` of the
129            // correct shape (footer is `&Box<dyn Fn() -> String>`).
130            #[allow(clippy::redundant_closure)]
131            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| footer()));
132            IN_RENDER.with(|flag| flag.set(false));
133
134            outcome.ok()
135        };
136
137        if let Some(text) = maybe_text {
138            if !text.is_empty() {
139                writeln!(f)?;
140                writeln!(f, "{text}")?;
141            }
142        }
143        Ok(())
144    }
145}