1#![cfg_attr(feature = "nightly", feature(panic_info_message))]
44#![cfg_attr(docsrs, feature(doc_auto_cfg))]
45#![warn(clippy::print_stderr)]
46#![warn(clippy::print_stdout)]
47
48pub mod report;
49use report::{Method, Report};
50
51use std::borrow::Cow;
52use std::io::Result as IoResult;
53use std::panic::PanicInfo;
54use std::path::{Path, PathBuf};
55
56pub struct Metadata {
60 name: Cow<'static, str>,
61 version: Cow<'static, str>,
62 authors: Option<Cow<'static, str>>,
63 homepage: Option<Cow<'static, str>>,
64 support: Option<Cow<'static, str>>,
65}
66
67impl Metadata {
68 pub fn new(name: impl Into<Cow<'static, str>>, version: impl Into<Cow<'static, str>>) -> Self {
70 Self {
71 name: name.into(),
72 version: version.into(),
73 authors: None,
74 homepage: None,
75 support: None,
76 }
77 }
78
79 pub fn authors(mut self, value: impl Into<Cow<'static, str>>) -> Self {
81 let value = value.into();
82 if !value.is_empty() {
83 self.authors = value.into();
84 }
85 self
86 }
87
88 pub fn homepage(mut self, value: impl Into<Cow<'static, str>>) -> Self {
90 let value = value.into();
91 if !value.is_empty() {
92 self.homepage = value.into();
93 }
94 self
95 }
96
97 pub fn support(mut self, value: impl Into<Cow<'static, str>>) -> Self {
99 let value = value.into();
100 if !value.is_empty() {
101 self.support = value.into();
102 }
103 self
104 }
105}
106
107#[macro_export]
109macro_rules! metadata {
110 () => {{
111 $crate::Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
112 .authors(env!("CARGO_PKG_AUTHORS").replace(":", ", "))
113 .homepage(env!("CARGO_PKG_HOMEPAGE"))
114 }};
115}
116
117#[macro_export]
140macro_rules! setup_panic {
141 ($meta:expr) => {{
142 $crate::setup_panic(|| $meta);
143 }};
144
145 () => {
146 $crate::setup_panic!($crate::metadata!());
147 };
148}
149
150#[doc(hidden)]
151pub fn setup_panic(meta: impl Fn() -> Metadata) {
152 #[allow(unused_imports)]
153 use std::panic::{self, PanicInfo};
154
155 match PanicStyle::default() {
156 PanicStyle::Debug => {}
157 PanicStyle::Human => {
158 let meta = meta();
159
160 panic::set_hook(Box::new(move |info: &PanicInfo<'_>| {
161 let file_path = handle_dump(&meta, info);
162 print_msg(file_path, &meta)
163 .expect("human-panic: printing error message to console failed");
164 }));
165 }
166 }
167}
168
169#[non_exhaustive]
171#[derive(Copy, Clone, PartialEq, Eq)]
172pub enum PanicStyle {
173 Debug,
175 Human,
177}
178
179impl Default for PanicStyle {
180 fn default() -> Self {
181 if cfg!(debug_assertions) {
182 PanicStyle::Debug
183 } else {
184 match ::std::env::var("RUST_BACKTRACE") {
185 Ok(_) => PanicStyle::Debug,
186 Err(_) => PanicStyle::Human,
187 }
188 }
189 }
190}
191
192#[cfg(feature = "color")]
194pub fn print_msg<P: AsRef<Path>>(file_path: Option<P>, meta: &Metadata) -> IoResult<()> {
195 use std::io::Write as _;
196
197 let stderr = anstream::stderr();
198 let mut stderr = stderr.lock();
199
200 write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?;
201 write_msg(&mut stderr, file_path, meta)?;
202 write!(stderr, "{}", anstyle::Reset.render())?;
203
204 Ok(())
205}
206
207#[cfg(not(feature = "color"))]
208pub fn print_msg<P: AsRef<Path>>(file_path: Option<P>, meta: &Metadata) -> IoResult<()> {
209 let stderr = std::io::stderr();
210 let mut stderr = stderr.lock();
211
212 write_msg(&mut stderr, file_path, meta)?;
213
214 Ok(())
215}
216
217fn write_msg<P: AsRef<Path>>(
218 buffer: &mut impl std::io::Write,
219 file_path: Option<P>,
220 meta: &Metadata,
221) -> IoResult<()> {
222 let Metadata {
223 name,
224 authors,
225 homepage,
226 support,
227 ..
228 } = meta;
229
230 writeln!(buffer, "Well, this is embarrassing.\n")?;
231 writeln!(
232 buffer,
233 "{name} had a problem and crashed. To help us diagnose the \
234 problem you can send us a crash report.\n"
235 )?;
236 writeln!(
237 buffer,
238 "We have generated a report file at \"{}\". Submit an \
239 issue or email with the subject of \"{} Crash Report\" and include the \
240 report as an attachment.\n",
241 match file_path {
242 Some(fp) => format!("{}", fp.as_ref().display()),
243 None => "<Failed to store file to disk>".to_owned(),
244 },
245 name
246 )?;
247
248 if let Some(homepage) = homepage {
249 writeln!(buffer, "- Homepage: {homepage}")?;
250 }
251 if let Some(authors) = authors {
252 writeln!(buffer, "- Authors: {authors}")?;
253 }
254 if let Some(support) = support {
255 writeln!(buffer, "\nTo submit the crash report:\n\n{support}")?;
256 }
257 writeln!(
258 buffer,
259 "\nWe take privacy seriously, and do not perform any \
260 automated error collection. In order to improve the software, we rely on \
261 people to submit reports.\n"
262 )?;
263 writeln!(buffer, "Thank you kindly!")?;
264
265 Ok(())
266}
267
268pub fn handle_dump(meta: &Metadata, panic_info: &PanicInfo<'_>) -> Option<PathBuf> {
270 let mut expl = String::new();
271
272 #[cfg(feature = "nightly")]
273 let message = panic_info.message().map(|m| format!("{}", m));
274
275 #[cfg(not(feature = "nightly"))]
276 let message = match (
277 panic_info.payload().downcast_ref::<&str>(),
278 panic_info.payload().downcast_ref::<String>(),
279 ) {
280 (Some(s), _) => Some((*s).to_owned()),
281 (_, Some(s)) => Some(s.to_owned()),
282 (None, None) => None,
283 };
284
285 let cause = match message {
286 Some(m) => m,
287 None => "Unknown".into(),
288 };
289
290 match panic_info.location() {
291 Some(location) => expl.push_str(&format!(
292 "Panic occurred in file '{}' at line {}\n",
293 location.file(),
294 location.line()
295 )),
296 None => expl.push_str("Panic location unknown.\n"),
297 }
298
299 let report = Report::new(&meta.name, &meta.version, Method::Panic, expl, cause);
300
301 if let Ok(f) = report.persist() {
302 Some(f)
303 } else {
304 use std::io::Write as _;
305 let stderr = std::io::stderr();
306 let mut stderr = stderr.lock();
307
308 let _ = writeln!(
309 stderr,
310 "{}",
311 report
312 .serialize()
313 .expect("only doing toml compatible types")
314 );
315 None
316 }
317}