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    backtrace: backtrace::Backtrace,
93
94    location: Option<PanicLocation>,
95    thread: String,
96
97    /// Defines the number of panics that happened before this one. Each panic
98    /// increments this counter. This is useful to know how many panics happened
99    /// before the current one.
100    panics_count: usize,
101}
102
103impl PanicContext {
104    fn from_std(std_panic_info: &StdPanicHookInfo<'_>, panics_count: usize) -> Self {
105        let location = std_panic_info.location();
106        let current_thread = std::thread::current();
107        let thread_ = current_thread
108            .name()
109            .map(String::from)
110            .unwrap_or_else(|| format!("{:?}", current_thread.id()));
111
112        Self(Rc::new(PanicContextShared {
113            backtrace: backtrace::Backtrace::capture(),
114            location: location.map(PanicLocation::from_std),
115            thread: thread_,
116            panics_count,
117        }))
118    }
119}
120
121impl fmt::Debug for PanicContext {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        fmt::Display::fmt(self, f)
124    }
125}
126
127impl fmt::Display for PanicContext {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        let PanicContextShared {
130            location,
131            backtrace,
132            thread,
133            panics_count,
134        } = &*self.0;
135
136        write!(f, "panic occurred")?;
137
138        if let Some(location) = location {
139            write!(f, " at {location}")?;
140        }
141
142        write!(f, " in thread '{thread}'")?;
143
144        if *panics_count > 1 {
145            write!(f, " (total panics observed: {panics_count})")?;
146        }
147
148        #[allow(clippy::incompatible_msrv)]
149        if backtrace.status() == backtrace::BacktraceStatus::Captured {
150            write!(f, "\nbacktrace:\n{backtrace}")?;
151        }
152
153        Ok(())
154    }
155}
156
157/// Extract the message of a panic.
158pub(super) fn message_from_panic_payload(payload: &dyn Any) -> Option<String> {
159    if let Some(str_slice) = payload.downcast_ref::<&str>() {
160        return Some((*str_slice).to_owned());
161    }
162    if let Some(owned_string) = payload.downcast_ref::<String>() {
163        return Some(owned_string.clone());
164    }
165
166    None
167}
168
169/// Location of the panic call site.
170#[derive(Clone)]
171struct PanicLocation {
172    file: String,
173    line: u32,
174    col: u32,
175}
176
177impl PanicLocation {
178    fn from_std(loc: &std::panic::Location<'_>) -> Self {
179        Self {
180            file: loc.file().to_owned(),
181            line: loc.line(),
182            col: loc.column(),
183        }
184    }
185}
186
187impl fmt::Display for PanicLocation {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(f, "{}:{}:{}", self.file, self.line, self.col)
190    }
191}
192
193#[rustversion::since(1.65.0)]
194mod backtrace {
195    pub(super) use std::backtrace::{Backtrace, BacktraceStatus};
196}
197
198#[rustversion::before(1.65.0)]
199mod backtrace {
200    #[derive(PartialEq)]
201    pub(super) enum BacktraceStatus {
202        Captured,
203    }
204
205    pub(super) struct Backtrace;
206
207    impl Backtrace {
208        pub(super) fn capture() -> Self {
209            Self
210        }
211        pub(super) fn status(&self) -> BacktraceStatus {
212            BacktraceStatus::Captured
213        }
214    }
215
216    impl std::fmt::Display for Backtrace {
217        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218            f.write_str("{update your Rust compiler to >=1.65.0 to see the backtrace}")
219        }
220    }
221}