Skip to main content

charon_error/
panic_hook.rs

1//! Panic hook macros for structured error capture and issue submission.
2//!
3//! [`setup_panic!`] installs a panic handler that checks for known errors,
4//! builds an [`ErrorReport`](crate::ErrorReport), and renders a submission
5//! link. [`setup_panic_simple!`] is a much lighter variant without submission or ErrorReport printing.
6
7/// Set up a custom panic hook with issue submission and known-error matching.
8///
9/// Replaces the default panic handler with one that:
10/// - Checks if the panic matches a known error (via [`ERGlobalSettings::check_known_error_types_fn`](crate::ERGlobalSettings))
11/// - In release builds: shows a user-friendly message with a report link
12/// - In debug builds: shows the full error report with a clickable issue URL
13///
14/// How the Error is displayed is handled by an [`SubmitErrorReport`](crate::SubmitErrorReport)
15/// You need to specify which reporter you want to use via [`ERGlobalSettings::submit_error_reporter_fn`](crate::ERGlobalSettings).
16///
17/// Only active when `RUST_BACKTRACE` is not set (falls back to default otherwise).
18///
19/// # Example
20///
21/// ```rust,no_run
22/// use charon_error::prelude::*;
23/// use charon_error::prelude::gitlab_er::*;
24///
25/// fn check_if_common_errors(msg: &str) -> Option<String> {
26///     if msg.contains("out of memory") {
27///         Some("Try closing other applications".to_owned())
28///     } else {
29///         None
30///     }
31/// }
32///
33/// fn main() {
34///     # return; // Skip actual execution in doc tests
35///     ERGlobalSettings::set_global_settings(ERGlobalSettings {
36///         check_known_error_types_fn: check_if_common_errors,
37///         submit_error_reporter_fn: |er| Box::new(GitLabErrorReport::new(er)),
38///         ..Default::default()
39///     }).unwrap();
40///     setup_panic!();
41/// }
42/// ```
43#[macro_export]
44macro_rules! setup_panic {
45    () => {
46        // This code is inspired by the `human-panic` crate.
47        // Do not use custom error messages if `RUST_BACKTRACE` is set
48        if ::std::env::var("RUST_BACKTRACE").is_err() {
49            #[allow(unused_imports)]
50            use charon_error::prelude::panic_hook::*;
51            #[allow(unused_imports)]
52            use std::panic::{self, PanicHookInfo};
53            #[allow(unused_imports)]
54            use std::ops::Deref;
55
56            // Check if Submit Error Reporter Settings are set and valid
57            #[cfg(debug_assertions)]
58            {
59                use charon_error::ResultExt;
60                let global_settings = ERGlobalSettings::get_or_default_settings().map(|gs| gs.deref().clone()).unwrap_or_default();
61                let test_error = StringError::new("").into();
62                let test_report = (global_settings.submit_error_reporter_fn)(&test_error);
63                if let Err(err) = test_report.validate_settings() {
64                    let error = err.attach_public_string("triggered_from", "setup_panic!()".to_owned());
65                    panic!("Failed to validate settings for Panic Error Reporter, \nmake sure they are set (correctly) for the current `ERGlobalSettings::submit_error_reporter_fn`:\n{error}");
66                }
67            }
68
69            panic::set_hook(Box::new(move |info: &PanicHookInfo| {
70                let payload = info.payload();
71                let panic_message = if let Some(s) = payload.downcast_ref::<&str>() {
72                    s.to_string()
73                } else if let Some(s) = payload.downcast_ref::<String>() {
74                    s.clone()
75                } else if let Some(s) = payload.downcast_ref::<AnyhowError>() {
76                    s.to_string()
77                } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
78                    s.get_last_error_title()
79                } else {
80                    format!("<unknown panic payload: {:?}>", payload.type_id())
81                };
82
83                // Check or set global settings
84                let global_settings = ERGlobalSettings::get_or_default_settings().map(|gs| gs.deref().clone()).unwrap_or_default();
85
86                if let Some(message) = (global_settings.check_known_error_types_fn)(&panic_message) {
87                    // Known Issue
88                    eprintln!(
89                        "{}: The application encountered an error it could not recover from.\n\
90                        This is a known issue: {}\n\
91                        Panic message: {}\n",
92                        "PANIC".bright_red(),
93                        message,
94                        panic_message,
95                    );
96                } else {
97                    // Unknown Issue
98                    let panic_location: Option<SourceLocation> = match info.location() {
99                        Some(location) => Some(SourceLocation::from_panic_location(location)),
100                        None => None,
101                    };
102
103                    eprintln!(
104                        "{}: A panic occurred during execution.\n\
105                        * Message: '{}'\n\
106                        * Path: '{}'",
107                        "PANIC".bright_red(),
108                        panic_message.bright_cyan().bold(),
109                        panic_location.as_ref()
110                            .map(|s| s.display_location(Some(global_settings.link_format)))
111                            .unwrap_or(String::from("Unknown location")),
112                    );
113
114                    let error_report: &ErrorReport = if let Some(s) = payload.downcast_ref::<&str>() {
115                        &ErrorReport::from_error(StringError::new(s.to_string()))
116                    } else if let Some(s) = payload.downcast_ref::<String>() {
117                        &ErrorReport::from_error(StringError::new(s.to_string()))
118                    } else if let Some(s) = payload.downcast_ref::<AnyhowError>() {
119                        &ErrorReport::from_anyhow_error_ref(s)
120                    } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
121                        s
122                    } else {
123                        &ErrorReport::from_error(StringError::new(panic_message))
124                    };
125
126                    // --------- Release Build ---------
127                    // Only use custom panic when in release mode.
128                    #[cfg(not(debug_assertions))]
129                    {
130                        let submit_report =
131                            (global_settings.submit_error_reporter_fn)(error_report);
132                        eprintln!(
133                            "{}: The application encountered an error it could not recover from.\n\
134                            If you report this we might be able to fix this in the future.\n\
135                            Title: {title}\n\n\
136                            {message}",
137                            "PANIC".bright_red(),
138                            title = submit_report.get_title().red().bold(),
139                            message = submit_report.create_message().unwrap_or_else(
140                                |err| format!("{}\nSomething went wrong while generating error report: \n{err}", "Error while processing Panic".red().bold())
141                            ),
142                        );
143                        // Log error
144                        let error_report_id = submit_report.get_error_report().get_unique_id();
145                        let error_title = submit_report.get_title();
146                        let panic_location_string = panic_location.as_ref()
147                            .map(|s| s.display_location(Some(global_settings.link_format)))
148                            .unwrap_or(String::from("Unknown location"));
149                        error!(
150                            error_type = "PANIC",
151                            error_report_id = error_report_id,
152                            error_title = error_title,
153                            panic_location = panic_location_string,
154                            "The application encountered an error it could not recover from: {error_title}"
155                        );
156                    }
157
158                    // --------- Debug Build ---------
159                    // Print different message on Debug builds.
160                    #[cfg(debug_assertions)]
161                    {
162                        let submit_report =
163                            (global_settings.submit_error_reporter_fn)(error_report);
164                        let message = submit_report.get_error_report().stringify(ErrorFmtSettings::from(&global_settings));
165                        // message = submit_report.create_bug_report().unwrap_or("Something went wrong while generating error report.".to_owned()),
166                        // 2083 max length: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#length-limits
167                        let url = submit_report.create_submit_url_limited(2083).map(|s| s.to_string());
168
169                        match (message, url) {
170                            (Err(m_err), Err(u_err)) => {
171                                eprintln!(
172                                    "PANIC: The application encountered an error it could not recover from.\n\
173                                    This error occurred while handling the Panic of an other error.\n\
174                                    Panic message while creating panic message: {m_err}\n\
175                                    Panic message while creating panic report URL: {u_err}\n"
176                                );
177                            }
178                            (Err(m_err), Ok(_)) => {
179                                eprintln!(
180                                    "PANIC: The application encountered an error it could not recover from.\n\
181                                    This error occurred while handling the Panic of an other error.\n\
182                                    Panic message while creating panic message: {m_err}"
183                                );
184                            }
185                            (Ok(_), Err(u_err)) => {
186                                eprintln!(
187                                    "PANIC: The application encountered an error it could not recover from.\n\
188                                    This error occurred while handling the Panic of an other error.\n\
189                                    Panic message while creating panic report URL: {u_err}"
190                                );
191                            }
192                            (Ok(message), Ok(url)) => {
193                                // Disable links too if ANSI is disabled.
194                                let mut report_link = "No report link available.".to_owned();
195                                if !url.starts_with("data:text/plain,") {
196                                    report_link = format!("\
197                                    ┌──────────────────────────┐\n\
198                                    │ \x1b]8;;{url}\x1b\\\u{f296} Open/Submit Bug Report\x1b]8;;\x1b\\ │\n\
199                                    └──────────────────────────┘");
200                                }
201                                if !url.starts_with("data:text/plain,") && std::env::var("NO_COLOR").is_ok() {
202                                    report_link = format!("Report Link: {}", url);
203                                }
204
205                                eprintln!(
206                                    "{}: The application encountered an error it could not recover from.\n\
207                                    {message}\n\
208                                    {report_link}",
209                                    "PANIC".bright_red(),
210                                );
211                            }
212                        }
213                        // Log error
214                        let error_report_id = submit_report.get_error_report().get_unique_id();
215                        let error_title = submit_report.get_title();
216                        let panic_location_string = panic_location.as_ref()
217                            .map(|s| s.display_location(Some(LinkDebugIde::NoLink)))
218                            .unwrap_or(String::from("Unknown location"));
219                        error!(
220                            error_type = "PANIC",
221                            error_report_id = error_report_id,
222                            error_title = error_title,
223                            panic_location = panic_location_string,
224                            "The application encountered an error it could not recover from: {error_title}"
225                        );
226                    }
227                }
228            }));
229        }
230    };
231}
232
233/// Set up a simple panic hook that prints a human-readable error message.
234///
235/// A lighter alternative to [`setup_panic!`] that does not generate issue
236/// submission URLs or check for known errors. Just prints the panic message
237/// and source location.
238#[macro_export]
239macro_rules! setup_panic_simple {
240    () => {{
241        // This code is inspired by the `human-panic` crate.
242        // Do not use custom error messages if `RUST_BACKTRACE` is set
243        if ::std::env::var("RUST_BACKTRACE").is_err() {
244            #[allow(unused_imports)]
245            use charon_error::prelude::panic_hook::*;
246            #[allow(unused_imports)]
247            use std::panic::{self, PanicHookInfo};
248
249            panic::set_hook(Box::new(move |info: &PanicHookInfo| {
250                let payload = info.payload();
251                let panic_message = if let Some(s) = payload.downcast_ref::<&str>() {
252                    s.to_string()
253                } else if let Some(s) = payload.downcast_ref::<String>() {
254                    s.clone()
255                } else if let Some(s) = payload.downcast_ref::<AnyhowError>() {
256                    s.to_string()
257                } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
258                    s.get_last_error_title()
259                } else {
260                    format!("<unknown panic payload: {:?}>", payload.type_id())
261                };
262                let panic_location: Option<SourceLocation> = match info.location() {
263                    Some(location) => Some(SourceLocation::from_panic_location(location)),
264                    None => None,
265                };
266                eprintln!(
267                    "PANIC: The application encountered an error it could not recover from.\n\
268                    * Message: '{panic_message}'\n\
269                    * Path: '{}'",
270                    panic_location
271                        .as_ref()
272                        .map(|s| s.display_location(None))
273                        .unwrap_or(String::from("Unknown location"))
274                );
275                // Log error
276                let panic_title = panic_message;
277                let panic_location_string = panic_location
278                    .as_ref()
279                    .map(|s| s.display_location(Some(LinkDebugIde::NoLink)))
280                    .unwrap_or(String::from("Unknown location"));
281                error!(
282                    error_type = "PANIC",
283                    panic_location = panic_location_string,
284                    panic_title = panic_title,
285                    "The application encountered an error it could not recover from: {panic_title}"
286                );
287            }));
288        }
289    }};
290}