Skip to main content

miniextendr_api/
panic_telemetry.rs

1//! Structured panic telemetry for debugging Rust panics that become R errors.
2//!
3//! Three separate panic→R-error paths exist in miniextendr (worker thread, ALTREP
4//! trampolines, and unwind_protect). This module provides a unified hook point
5//! that fires before each panic is converted to an R error.
6//!
7//! # Usage
8//!
9//! ```ignore
10//! use miniextendr_api::panic_telemetry::{set_panic_telemetry_hook, PanicReport, PanicSource};
11//!
12//! set_panic_telemetry_hook(|report| {
13//!     eprintln!("[{:?}] panic: {}", report.source, report.message);
14//! });
15//! ```
16//!
17//! # Performance
18//!
19//! `fire()` takes a read lock (uncontended in normal use). The hook only fires
20//! on panic paths, never on hot paths.
21
22use std::panic::{AssertUnwindSafe, catch_unwind};
23use std::sync::{Arc, RwLock};
24
25/// Describes where a panic originated before being converted to an R error.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum PanicSource {
28    /// Panic on the worker thread (caught by `run_on_worker`).
29    Worker,
30    /// Panic inside an ALTREP trampoline (caught by `catch_altrep_panic`).
31    Altrep,
32    /// Panic inside `with_r_unwind_protect` (caught by `panic_payload_to_r_error`).
33    UnwindProtect,
34    /// Panic inside a connection callback trampoline.
35    Connection,
36}
37
38/// A structured panic report passed to the telemetry hook.
39pub struct PanicReport<'a> {
40    /// The panic message (extracted from the panic payload).
41    pub message: &'a str,
42    /// Which panic→R-error boundary caught this panic.
43    pub source: PanicSource,
44}
45
46type Hook = Arc<dyn Fn(&PanicReport) + Send + Sync>;
47
48static HOOK: RwLock<Option<Hook>> = RwLock::new(None);
49
50/// Register a panic telemetry hook.
51///
52/// The hook is called with a [`PanicReport`] each time a Rust panic is about
53/// to be converted into an R error. Only one hook can be active at a time;
54/// calling this again replaces (and drops) the previous hook.
55///
56/// # Thread Safety
57///
58/// The hook may be called from any thread (worker thread, main R thread, etc.).
59/// Ensure your closure is safe to call concurrently.
60///
61/// It is safe to call `set_panic_telemetry_hook` or `clear_panic_telemetry_hook`
62/// from within a hook — the lock is released before the hook is invoked.
63pub fn set_panic_telemetry_hook(f: impl Fn(&PanicReport) + Send + Sync + 'static) {
64    let mut guard = HOOK.write().unwrap_or_else(|e| e.into_inner());
65    *guard = Some(Arc::new(f));
66}
67
68/// Remove the current panic telemetry hook, if any.
69pub fn clear_panic_telemetry_hook() {
70    let mut guard = HOOK.write().unwrap_or_else(|e| e.into_inner());
71    *guard = None;
72}
73
74/// Fire the telemetry hook if one is set.
75///
76/// Called internally at each panic→R-error conversion site.
77///
78/// The hook is cloned (as `Arc`) and the lock is dropped before invocation,
79/// so the hook can safely call `set_panic_telemetry_hook` or
80/// `clear_panic_telemetry_hook` without deadlocking. Secondary panics from
81/// the hook are caught and silently suppressed.
82pub(crate) fn fire(message: &str, source: PanicSource) {
83    // Clone the Arc while holding the read lock, then drop the lock
84    // before invoking. This prevents deadlock if the hook calls
85    // set/clear_panic_telemetry_hook (which take a write lock).
86    let hook = {
87        let guard = HOOK.read().unwrap_or_else(|e| e.into_inner());
88        guard.as_ref().cloned()
89    };
90
91    if let Some(hook) = hook {
92        let report = PanicReport { message, source };
93        // Suppress secondary panics from the hook — we're already on a
94        // panic→R-error path and a double-panic would abort.
95        let _ = catch_unwind(AssertUnwindSafe(|| hook(&report)));
96    }
97}