bon_macros/error/
panic_context.rs

1// The new name is used on newer rust versions
2#[rustversion::since(1.81.0)]
3use std::panic::PanicHookInfo as StdPanicHookInfo;
4
5// The deprecated name for is used on older rust versions
6#[rustversion::before(1.81.0)]
7use std::panic::PanicInfo as StdPanicHookInfo;
8
9use std::any::Any;
10use std::cell::RefCell;
11use std::fmt;
12use std::rc::Rc;
13
14fn with_global_panic_context<T>(f: impl FnOnce(&mut GlobalPanicContext) -> T) -> T {
15    thread_local! {
16        /// A lazily initialized global panic context. It aggregates the panics from the
17        /// current thread. This is used to capture info about the panic after the
18        /// `catch_unwind` call and observe the context of the panic that happened.
19        ///
20        /// Unfortunately, we can't use a global static variable that would be
21        /// accessible by all threads because `std::sync::Mutex::new` became
22        /// `const` only in Rust 1.63.0, which is above our MSRV 1.59.0. However,
23        /// a thread-local works perfectly fine for our use case because we don't
24        /// spawn threads in proc macros.
25        static GLOBAL: RefCell<GlobalPanicContext> = const {
26            RefCell::new(GlobalPanicContext {
27                last_panic: None,
28                initialized: false,
29            })
30        };
31    }
32
33    GLOBAL.with(|global| f(&mut global.borrow_mut()))
34}
35
36struct GlobalPanicContext {
37    last_panic: Option<PanicContext>,
38    initialized: bool,
39}
40
41/// This struct without any fields exists to make sure that [`PanicListener::register()`]
42/// is called first before the code even attempts to get the last panic information.
43#[derive(Default)]
44pub(super) struct PanicListener {
45    /// Required to make sure struct is not constructable via a struct literal
46    /// in the code outside of this module.
47    _private: (),
48}
49
50impl PanicListener {
51    pub(super) fn register() -> Self {
52        with_global_panic_context(Self::register_with_global)
53    }
54
55    fn register_with_global(global: &mut GlobalPanicContext) -> Self {
56        if global.initialized {
57            return Self { _private: () };
58        }
59
60        let prev_panic_hook = std::panic::take_hook();
61
62        std::panic::set_hook(Box::new(move |panic_info| {
63            with_global_panic_context(|global| {
64                let panics_count = global.last_panic.as_ref().map(|p| p.0.panics_count);
65                let panics_count = panics_count.unwrap_or(0) + 1;
66
67                global.last_panic = Some(PanicContext::from_std(panic_info, panics_count));
68            });
69
70            prev_panic_hook(panic_info);
71        }));
72
73        global.initialized = true;
74
75        Self { _private: () }
76    }
77
78    /// Returns the last panic that happened since the [`PanicListener::register()`] call.
79    // `self` is required to make sure this code runs only after we initialized
80    // the global panic listener in the `register` method.
81    #[allow(clippy::unused_self)]
82    pub(super) fn get_last_panic(&self) -> Option<PanicContext> {
83        with_global_panic_context(|global| global.last_panic.clone())
84    }
85}
86
87/// Contains all the necessary bits of information about the occurred panic.
88#[derive(Clone)]
89pub(super) struct PanicContext(Rc<PanicContextShared>);
90
91struct PanicContextShared {
92    #[allow(clippy::incompatible_msrv)]
93    backtrace: backtrace::Backtrace,
94
95    location: Option<PanicLocation>,
96    thread: String,
97
98    /// Defines the number of panics that happened before this one. Each panic
99    /// increments this counter. This is useful to know how many panics happened
100    /// before the current one.
101    panics_count: usize,
102}
103
104impl PanicContext {
105    #[allow(clippy::incompatible_msrv)]
106    fn from_std(std_panic_info: &StdPanicHookInfo<'_>, panics_count: usize) -> Self {
107        let location = std_panic_info.location();
108        let current_thread = std::thread::current();
109        let thread_ = current_thread
110            .name()
111            .map(String::from)
112            .unwrap_or_else(|| format!("{:?}", current_thread.id()));
113
114        Self(Rc::new(PanicContextShared {
115            // This is expected, and we handle the compatibility with
116            // conditional compilation via the `rustversion` crate.
117            #[allow(clippy::incompatible_msrv)]
118            backtrace: backtrace::Backtrace::capture(),
119            location: location.map(PanicLocation::from_std),
120            thread: thread_,
121            panics_count,
122        }))
123    }
124}
125
126impl fmt::Debug for PanicContext {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        fmt::Display::fmt(self, f)
129    }
130}
131
132impl fmt::Display for PanicContext {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        let PanicContextShared {
135            location,
136            backtrace,
137            thread,
138            panics_count,
139        } = &*self.0;
140
141        write!(f, "panic occurred")?;
142
143        if let Some(location) = location {
144            write!(f, " at {location}")?;
145        }
146
147        write!(f, " in thread '{thread}'")?;
148
149        if *panics_count > 1 {
150            write!(f, " (total panics observed: {panics_count})")?;
151        }
152
153        #[allow(clippy::incompatible_msrv)]
154        if backtrace.status() == backtrace::BacktraceStatus::Captured {
155            write!(f, "\nbacktrace:\n{backtrace}")?;
156        }
157
158        Ok(())
159    }
160}
161
162/// Extract the message of a panic.
163pub(super) fn message_from_panic_payload(payload: &dyn Any) -> Option<String> {
164    if let Some(str_slice) = payload.downcast_ref::<&str>() {
165        return Some((*str_slice).to_owned());
166    }
167    if let Some(owned_string) = payload.downcast_ref::<String>() {
168        return Some(owned_string.clone());
169    }
170
171    None
172}
173
174/// Location of the panic call site.
175#[derive(Clone)]
176struct PanicLocation {
177    file: String,
178    line: u32,
179    col: u32,
180}
181
182impl PanicLocation {
183    fn from_std(loc: &std::panic::Location<'_>) -> Self {
184        Self {
185            file: loc.file().to_owned(),
186            line: loc.line(),
187            col: loc.column(),
188        }
189    }
190}
191
192impl fmt::Display for PanicLocation {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        write!(f, "{}:{}:{}", self.file, self.line, self.col)
195    }
196}
197
198#[rustversion::since(1.65.0)]
199mod backtrace {
200    pub(super) use std::backtrace::{Backtrace, BacktraceStatus};
201}
202
203#[rustversion::before(1.65.0)]
204mod backtrace {
205    #[derive(PartialEq)]
206    pub(super) enum BacktraceStatus {
207        Captured,
208    }
209
210    pub(super) struct Backtrace;
211
212    impl Backtrace {
213        pub(super) fn capture() -> Self {
214            Self
215        }
216        pub(super) fn status(&self) -> BacktraceStatus {
217            BacktraceStatus::Captured
218        }
219    }
220
221    impl std::fmt::Display for Backtrace {
222        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223            f.write_str("{update your Rust compiler to >=1.65.0 to see the backtrace}")
224        }
225    }
226}