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