wolfram_library_link/
catch_panic.rs

1//! Utilities for catching panics, capturing a backtrace, and extracting the panic
2//! message.
3
4use std::collections::HashMap;
5use std::panic::{self, UnwindSafe};
6use std::process;
7use std::sync::{self, Mutex};
8use std::thread::{self, ThreadId};
9use std::time::Instant;
10
11#[cfg(feature = "panic-failure-backtraces")]
12use backtrace::Backtrace;
13
14use once_cell::sync::Lazy;
15
16use crate::expr::{Expr, Symbol};
17
18static CAUGHT_PANICS: Lazy<Mutex<HashMap<ThreadId, (Instant, CaughtPanic)>>> =
19    Lazy::new(|| Default::default());
20
21/// Information from a caught panic.
22///
23/// Returned by [`call_and_catch_panic()`].
24#[derive(Clone)]
25pub struct CaughtPanic {
26    /// Note: In certain circumstances, this message will NOT match the message used
27    /// in panic!(). This can happen when user code changes the panic hook, or when
28    /// the panic occurs in a different thread from the one `call_and_catch_panic()`
29    /// was called in.
30    ///
31    /// An inaccurate instance of `CaughtPanic` can also be reported when panic's
32    /// occur in multiple threads at once.
33    message: Option<String>,
34    location: Option<String>,
35
36    #[cfg(feature = "panic-failure-backtraces")]
37    backtrace: Option<Backtrace>,
38}
39
40impl CaughtPanic {
41    pub(crate) fn to_pretty_expr(&self) -> Expr {
42        let CaughtPanic {
43            message,
44            location,
45
46            #[cfg(feature = "panic-failure-backtraces")]
47            backtrace,
48        } = self.clone();
49
50        let message = Expr::string(message.unwrap_or("Rust panic (no message)".into()));
51        let location = Expr::string(location.unwrap_or("Unknown".into()));
52
53        let backtrace = {
54            // Avoid showing the backtrace if it hasn't been explicitly requested by the user.
55            // This avoids calling `.resolve()` below, which can sometimes be very slow (100s of
56            // millisends).
57            if !cfg!(feature = "panic-failure-backtraces") || !should_show_backtrace() {
58                Expr::normal(Symbol::new("System`Missing"), vec![Expr::string(
59                    "NotEnabled",
60                )])
61            } else {
62                #[cfg(feature = "panic-failure-backtraces")]
63                {
64                    display_backtrace(backtrace)
65                }
66
67                #[cfg(not(feature = "panic-failure-backtraces"))]
68                unreachable!()
69            }
70        };
71
72        // Failure["RustPanic", <|
73        //     "MessageTemplate" -> "Rust LibraryLink function panic: `message`",
74        //     "MessageParameters" -> <| "message" -> "..." |>,
75        //     "SourceLocation" -> "...",
76        //     "Backtrace" -> "..."
77        // |>]
78        Expr::normal(Symbol::new("System`Failure"), vec![
79            Expr::string("RustPanic"),
80            Expr::normal(Symbol::new("System`Association"), vec![
81                Expr::normal(Symbol::new("System`Rule"), vec![
82                    Expr::string("MessageTemplate"),
83                    Expr::string("Rust LibraryLink function panic: `message`"),
84                ]),
85                Expr::normal(Symbol::new("System`Rule"), vec![
86                    Expr::string("MessageParameters"),
87                    Expr::normal(Symbol::new("System`Association"), vec![Expr::normal(
88                        Symbol::new("System`Rule"),
89                        vec![Expr::string("message"), message],
90                    )]),
91                ]),
92                Expr::normal(Symbol::new("System`Rule"), vec![
93                    Expr::string("SourceLocation"),
94                    location,
95                ]),
96                Expr::normal(Symbol::new("System`Rule"), vec![
97                    Expr::string("Backtrace"),
98                    backtrace,
99                ]),
100            ]),
101        ])
102    }
103}
104
105fn should_show_backtrace() -> bool {
106    std::env::var(crate::BACKTRACE_ENV_VAR).is_ok()
107}
108
109#[cfg(feature = "panic-failure-backtraces")]
110fn display_backtrace(bt: Option<Backtrace>) -> Expr {
111    let bt: Expr = if let Some(mut bt) = bt {
112        // Resolve the symbols in the frames of the backtrace.
113        bt.resolve();
114
115        // Expr::string(format!("{:?}", bt))
116
117        let mut frames = Vec::new();
118        for (index, frame) in bt.frames().into_iter().enumerate() {
119            use backtrace::{BacktraceSymbol, SymbolName};
120
121            // TODO: Show all the symbols, not just the last one. A frame will be
122            //       associated with more than one symbol if any inlining occured, so this
123            //       would help show better backtraces in optimized builds.
124            let bt_symbol: Option<&BacktraceSymbol> = frame.symbols().last();
125
126            let name: String = bt_symbol
127                .and_then(BacktraceSymbol::name)
128                .as_ref()
129                .map(|sym: &SymbolName| format!("{}", sym))
130                .unwrap_or("<unknown>".into());
131
132            // Skip frames from within the `backtrace` crate itself.
133            if name.starts_with("backtrace::") {
134                continue;
135            }
136
137            let filename = bt_symbol.and_then(BacktraceSymbol::filename);
138            let lineno = bt_symbol.and_then(BacktraceSymbol::lineno);
139            let file_and_line: String = match (filename, lineno) {
140                (Some(path), Some(lineno)) => format!("{}:{}", path.display(), lineno),
141                (Some(path), None) => format!("{}", path.display()),
142                _ => "".into(),
143            };
144
145            // Row[{
146            //     %[index.to_string()],
147            //     ": ",
148            //     'file_and_line,
149            //     'name
150            // }]
151            frames.push(Expr::normal(Symbol::new("System`Row"), vec![Expr::normal(
152                Symbol::new("System`List"),
153                vec![
154                    Expr::string(index.to_string()),
155                    Expr::string(": "),
156                    Expr::string(file_and_line),
157                    Expr::string(name),
158                ],
159            )]));
160        }
161
162        let frames = Expr::normal(Symbol::new("System`List"), frames);
163        // Set ImageSize so that the lines don't wordwrap for very long function names,
164        // which makes the backtrace hard to read.
165
166        // Column['frames, ImageSize -> {1200, Automatic}]
167        Expr::normal(Symbol::new("System`Column"), vec![
168            frames,
169            Expr::normal(Symbol::new("System`Rule"), vec![
170                Expr::symbol(Symbol::new("System`ImageSize")),
171                Expr::normal(Symbol::new("System`List"), vec![
172                    Expr::from(1200i64),
173                    Expr::symbol(Symbol::new("System`Automatic")),
174                ]),
175            ]),
176        ])
177    } else {
178        Expr::string("<unable to capture backtrace>")
179    };
180
181    // Row[{
182    //     Style["Backtrace", Bold],
183    //     ": ",
184    //     Style['bt, FontSize -> 13, FontFamily -> "Source Code Pro"]
185    // }]
186    Expr::normal(Symbol::new("System`Row"), vec![Expr::normal(
187        Symbol::new("System`List"),
188        vec![
189            Expr::normal(Symbol::new("System`Style"), vec![
190                Expr::string("Backtrace"),
191                Expr::symbol(Symbol::new("System`Bold")),
192            ]),
193            Expr::string(": "),
194            Expr::normal(Symbol::new("System`Style"), vec![
195                bt,
196                Expr::normal(Symbol::new("System`Rule"), vec![
197                    Expr::symbol(Symbol::new("System`FontSize")),
198                    Expr::from(13i16),
199                ]),
200                Expr::normal(Symbol::new("System`Rule"), vec![
201                    Expr::symbol(Symbol::new("System`FontFamily")),
202                    Expr::string("Source Code Pro"),
203                ]),
204            ]),
205        ],
206    )])
207}
208
209/// Call `func` and catch any unwinding panic which occurs during that call, returning
210/// information from the caught panic in the form of a `CaughtPanic`.
211///
212/// NOTE: `func` should not set it's own panic hook, or unset the panic hook set upon
213///       calling it. Doing so would likely interfere with the operation of this function.
214pub fn call_and_catch_panic<T, F>(func: F) -> Result<T, CaughtPanic>
215where
216    F: FnOnce() -> T + UnwindSafe,
217{
218    // Set up the panic hook. If calling `func` triggers a panic, the panic message string
219    // and location will be saved into CAUGHT_PANICS.
220    //
221    // The panic hook is reset to the default handler before we return.
222    let prev_hook = panic::take_hook();
223    let _: () = panic::set_hook(Box::new(custom_hook));
224
225    // Call `func`, catching any panic's which occur. The `Err` produced by `catch_unwind`
226    // is an opaque object we can't get any information from; this is why it's necessary
227    // to set the panic hook, which *does* get an inspectable object.
228    let result: Result<T, ()> = panic::catch_unwind(|| func()).map_err(|_| ());
229
230    // Return to the previously set hook (will be the default hook if no previous hook was
231    // set).
232    panic::set_hook(prev_hook);
233
234    // If `result` is an `Err`, meaning a panic occured, read information out of
235    // CAUGHT_PANICS.
236    let result: Result<T, CaughtPanic> = result.map_err(|()| get_caught_panic());
237
238    result
239}
240
241fn get_caught_panic() -> CaughtPanic {
242    let id = thread::current().id();
243    let mut map = acquire_lock();
244    // Remove the `CaughtPanic` which should be associated with `id` now.
245    let caught_panic = match map.remove(&id) {
246        Some((_time, caught_panic)) => caught_panic.clone(),
247        None => {
248            match map.len() {
249                0 => {
250                    // This can occur when the user code sets their own panic hook, but
251                    // fails to restore the previous panic hook (i.e., the `custom_hook`
252                    // we set above).
253                    let message = format!(
254                        "could not get panic info for current thread. \
255                         Operation of custom panic hook was interrupted"
256                    );
257                    CaughtPanic {
258                        message: Some(message),
259                        location: None,
260
261                        #[cfg(feature = "panic-failure-backtraces")]
262                        backtrace: None,
263                    }
264                },
265                // This case can occur when a panic occurs in a thread spawned by the
266                // current thread: the ThreadId stored in CAUGHT_PANICS's is not
267                // the ThreadId of the current thread, but the panic still
268                // "bubbled up" accross thread boundries to the catch_unwind() call
269                // above.
270                //
271                // We simply guess that the only panic in the HashMap is the right one --
272                // it's rare that multiple panic's will occur in multiple threads at the
273                // same time (meaning there's more than one entry in the map).
274                1 => map.values().next().unwrap().1.clone(),
275                // Pick the most recent panic, and hope it's the right one.
276                _ => map
277                    .values()
278                    .max_by(|a, b| a.0.cmp(&b.0))
279                    .map(|(_time, info)| info)
280                    .cloned()
281                    .unwrap(),
282            }
283        },
284    };
285    caught_panic
286}
287
288fn custom_hook(info: &panic::PanicInfo) {
289    let caught_panic = {
290        let message: Option<String> = get_panic_message(info);
291        let location: Option<String> = info.location().map(ToString::to_string);
292
293        // Don't resolve the backtrace inside the panic hook. This seems to hang for a
294        // long time (maybe forever?). Resolving it later, in the ToPrettyExpr impl, seems
295        // to work (though it is noticeably slower, takes maybe ~0.5s-1s).
296        #[cfg(feature = "panic-failure-backtraces")]
297        let backtrace = Some(Backtrace::new_unresolved());
298
299        CaughtPanic {
300            message,
301            location,
302
303            #[cfg(feature = "panic-failure-backtraces")]
304            backtrace,
305        }
306    };
307
308    // The `ThreadId` of the thread which is currently panic'ing.
309    let thread = thread::current();
310    let data = (Instant::now(), caught_panic);
311
312    let mut lock = acquire_lock();
313
314    if let Some(_previous) = lock.insert(thread.id(), data) {
315        // This situation is unlikely, but it can happen.
316        //
317        // This panic hook is used for every panic which occurs while it is set. This
318        // includes panic's which are caught before reaching the `panic::catch_unwind()`,
319        // above in `call_and_catch_panic()`, which happens when the user code also uses
320        // `panic::catch_unwind()`. When that occurs, this hook (assuming the user hasn't
321        // also set their own panic hook) will create an entry in CAUGHT_PANICS's. That
322        // entry is never cleared, because the panic is caught before reaching the call to
323        // `remove()` in `call_and_catch_panic()`.
324    }
325}
326
327fn get_panic_message(info: &panic::PanicInfo) -> Option<String> {
328    // Extract the message from `panic!("...")` statements.
329    // In this case, the payload is always the static formatting string.
330    if let Some(string) = info.payload().downcast_ref::<&str>() {
331        return Some(string.to_string());
332    }
333
334    // Extract the message from `panic!("... {} ...", arg...)` statements.
335    // In this case, the payload has to be a dynamically allocated String to contain
336    // the arbitrary formatted arguments.
337    if let Some(string) = info.payload().downcast_ref::<String>() {
338        return Some(string.to_owned());
339    }
340
341    #[cfg(feature = "nightly")]
342    if let Some(fmt_arguments) = info.message() {
343        return Some(format!("{}", fmt_arguments));
344    }
345
346    None
347}
348
349/// Attempt to acquire a lock on CAUGHT_PANIC. Exit the current process if we can not,
350/// without panic'ing.
351fn acquire_lock() -> sync::MutexGuard<'static, HashMap<ThreadId, (Instant, CaughtPanic)>>
352{
353    let lock = match CAUGHT_PANICS.lock() {
354        Ok(lock) => lock,
355        Err(_err) => {
356            println!(
357                "catch_panic: acquire_lock: failed to acquire lock. Exiting process."
358            );
359            process::exit(-1);
360        },
361    };
362    lock
363}