1use std::panic::{self, AssertUnwindSafe, UnwindSafe};
4use std::sync::Once;
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone)]
9pub struct PanicError {
10 pub message: String,
11}
12
13impl std::fmt::Display for PanicError {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 write!(f, "Module panicked: {}", self.message)
16 }
17}
18
19impl std::error::Error for PanicError {}
20
21pub fn setup_panic_hook() {
22 static SETUP: Once = Once::new();
23 SETUP.call_once(|| {
24 let default_hook = panic::take_hook();
25 panic::set_hook(Box::new(move |panic_info| {
26 eprintln!("[RUNTIME PANIC] {}", format_panic_info(panic_info));
27 default_hook(panic_info);
28 }));
29 });
30}
31
32#[allow(clippy::incompatible_msrv)]
33fn format_panic_info(panic_info: &panic::PanicHookInfo<'_>) -> String {
34 let location = panic_info
35 .location()
36 .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
37 .unwrap_or_else(|| "unknown location".to_string());
38
39 let payload = panic_info
40 .payload()
41 .downcast_ref::<&str>()
42 .map(|s| s.to_string())
43 .or_else(|| panic_info.payload().downcast_ref::<String>().cloned())
44 .unwrap_or_else(|| format!("{:?}", panic_info.payload()));
45
46 format!("{} at {}", payload, location)
47}
48
49pub fn safe_call<F, R>(f: F) -> Result<R>
50where
51 F: FnOnce() -> R + UnwindSafe,
52{
53 match panic::catch_unwind(f) {
54 Ok(result) => Ok(result),
55 Err(panic_payload) => {
56 let message = extract_panic_message(&panic_payload);
57 Err(Error::ModulePanicked(message))
58 }
59 }
60}
61
62pub fn safe_call_unchecked<F, R>(f: F) -> Result<R>
63where
64 F: FnOnce() -> R,
65{
66 safe_call(AssertUnwindSafe(f))
67}
68
69fn extract_panic_message(payload: &(dyn std::any::Any + Send)) -> String {
70 if let Some(s) = payload.downcast_ref::<&str>() {
71 s.to_string()
72 } else if let Some(s) = payload.downcast_ref::<String>() {
73 s.clone()
74 } else {
75 format!("{:?}", payload)
76 }
77}