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}