charon_error/panic_hook.rs
1/// Set up a custom panic hook with issue submission and known-error matching.
2///
3/// Replaces the default panic handler with one that:
4/// - Checks if the panic matches a known error (via `$check_if_known_error`)
5/// - In release builds: shows a user-friendly message with a report link
6/// - In debug builds: shows the full error report with a clickable issue URL
7///
8/// Only active when `RUST_BACKTRACE` is not set (falls back to default otherwise).
9///
10/// # Arguments
11///
12/// - `$submit_error_report` — A type implementing [`SubmitErrorReport`](crate::SubmitErrorReport) (e.g. `GitLabErrorReport`)
13/// - `$check_if_known_error` — A function `fn(&str) -> Option<String>` that returns
14/// a message if the panic matches a known issue, or `None` to generate a report
15///
16/// # Example
17///
18/// ```rust,no_run
19/// use charon_error::prelude::*;
20/// use charon_error::prelude::gitlab_er::*;
21///
22/// fn check_if_common_errors(msg: &str) -> Option<String> {
23/// if msg.contains("out of memory") {
24/// Some("Try closing other applications".to_owned())
25/// } else {
26/// None
27/// }
28/// }
29///
30/// fn main() {
31/// # return; // Skip actual execution in doc tests
32/// setup_panic!(GitLabErrorReport, check_if_common_errors);
33/// }
34/// ```
35#[macro_export]
36macro_rules! setup_panic {
37 ($submit_error_report:ty, $check_if_known_error:ident) => {
38 // This code is inspired by the `human-panic` crate.
39 // Do not use custom error messages if `RUST_BACKTRACE` is set
40 if ::std::env::var("RUST_BACKTRACE").is_err() {
41 #[allow(unused_imports)]
42 use charon_error::prelude::panic_hook::*;
43 #[allow(unused_imports)]
44 use std::panic::{self, PanicHookInfo};
45
46 panic::set_hook(Box::new(move |info: &PanicHookInfo| {
47 let payload = info.payload();
48 let panic_message = if let Some(s) = payload.downcast_ref::<&str>() {
49 s.to_string()
50 } else if let Some(s) = payload.downcast_ref::<String>() {
51 s.clone()
52 } else if let Some(s) = payload.downcast_ref::<anyhow::Error>() {
53 s.to_string()
54 } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
55 s.get_last_error_title()
56 } else {
57 String::new()
58 };
59 if let Some(message) = $check_if_known_error(&panic_message) {
60 // Known Issue
61 eprintln!(
62 "{}: The application encountered an error it could not recover from.\n\
63 This is a known issue: {}\n\
64 Panic message: {}\n",
65 "PANIC".bright_red(),
66 message,
67 panic_message,
68 );
69 } else {
70 // Unknown Issue
71 let panic_location: Option<SourceLocation> = match info.location() {
72 Some(location) => Some(SourceLocation::from_panic_location(location)),
73 None => None,
74 };
75
76 eprintln!(
77 "{}: A panic occurred during execution.\n\
78 * Message: `{}`\n\
79 * Path: `{}`",
80 "PANIC".bright_red(),
81 panic_message.bright_cyan().bold(),
82 panic_location.map(|s| s.to_string()).unwrap_or(String::from("Unknown location")),
83 );
84
85 let error_report: &ErrorReport = if let Some(s) = payload.downcast_ref::<&str>() {
86 &ErrorReport::from_error(StringError::new(s.to_string()))
87 } else if let Some(s) = payload.downcast_ref::<String>() {
88 &ErrorReport::from_error(StringError::new(s.to_string()))
89 } else if let Some(s) = payload.downcast_ref::<anyhow::Error>() {
90 &ErrorReport::from_anyhow_error_ref(s)
91 } else if let Some(s) = payload.downcast_ref::<ErrorReport>() {
92 s
93 } else {
94 &ErrorReport::from_error(StringError::new(panic_message))
95 };
96
97 // --------- Release Build ---------
98 // Only use custom panic when in release mode.
99 #[cfg(not(debug_assertions))]
100 {
101 let submit_report =
102 <$submit_error_report as SubmitErrorReport>::new(error_report);
103 eprintln!(
104 "{}: The application encountered an error it could not recover from.\n\
105 If you report this we might be able to fix this in the future.\n\
106 {title}\n\
107 {message}",
108 "PANIC".bright_red(),
109 title = submit_report.get_title(), // issue.create_message()
110 message = submit_report.create_message().unwrap_or("Something went wrong while generating error report.".to_owned()),
111 );
112 }
113
114 // --------- Debug Build ---------
115 // Print different message on Debug builds.
116 #[cfg(debug_assertions)]
117 {
118 let submit_report =
119 <$submit_error_report as SubmitErrorReport>::new(error_report);
120 let message = submit_report.error_report.stringify(ErrorFmtSettings::default());
121 // message = submit_report.create_bug_report().unwrap_or("Something went wrong while generating error report.".to_owned()),
122 // 2083 max length: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#length-limits
123 let url = submit_report.create_submit_url_limited(2083).map(|s| s.to_string());
124
125 match (message, url) {
126 (Err(m_err), Err(u_err)) => {
127 eprintln!(
128 "PANIC: The application encountered an error it could not recover from.\n\
129 This error occurred while handling the Panic of an other error.\n\
130 Panic message while creating panic message: {m_err}\n\
131 Panic message while creating panic report URL: {u_err}\n"
132 );
133 }
134 (Err(m_err), Ok(_)) => {
135 eprintln!(
136 "PANIC: The application encountered an error it could not recover from.\n\
137 This error occurred while handling the Panic of an other error.\n\
138 Panic message while creating panic message: {m_err}"
139 );
140 }
141 (Ok(_), Err(u_err)) => {
142 eprintln!(
143 "PANIC: The application encountered an error it could not recover from.\n\
144 This error occurred while handling the Panic of an other error.\n\
145 Panic message while creating panic report URL: {u_err}"
146 );
147 }
148 (Ok(message), Ok(url)) => {
149 eprintln!(
150 "{}: The application encountered an error it could not recover from.\n\
151 {message}\n\
152 ┌──────────────────────────┐\n\
153 │ \x1b]8;;{url}\x1b\\\u{f296} Open/Submit Bug Report\x1b]8;;\x1b\\ │\n\
154 └──────────────────────────┘",
155 "PANIC".bright_red(),
156 );
157 }
158 }
159 }
160 }
161 }));
162 }
163 };
164}
165
166/// Set up a simple panic hook that prints a human-readable error message.
167///
168/// A lighter alternative to [`setup_panic!`] that does not generate issue
169/// submission URLs or check for known errors. Just prints the panic message
170/// and source location.
171#[macro_export]
172macro_rules! setup_panic_simple {
173 () => {{
174 #[allow(unused_imports)]
175 use charon_error::prelude::panic_hook::*;
176 #[allow(unused_imports)]
177 use std::panic::{self, PanicHookInfo};
178
179 panic::set_hook(Box::new(move |info: &PanicHookInfo| {
180 let payload = info.payload();
181 let panic_message = if let Some(s) = payload.downcast_ref::<&str>() {
182 s.to_string()
183 } else if let Some(s) = payload.downcast_ref::<String>() {
184 s.clone()
185 } else {
186 String::new()
187 };
188 let panic_location: Option<SourceLocation> = match info.location() {
189 Some(location) => Some(SourceLocation::from_panic_location(location)),
190 None => None,
191 };
192 eprintln!(
193 "PANIC: The application encountered an error it could not recover from.\n\
194 Panic message: {}\n\
195 Location: {}",
196 panic_message,
197 panic_location
198 .map(|s| s.to_string())
199 .unwrap_or(String::from("Unknown location")),
200 );
201 }));
202 }};
203}