human_panic/
lib.rs

1//! Panic messages for humans
2//!
3//! Handles panics by calling
4//! [`std::panic::set_hook`](https://doc.rust-lang.org/std/panic/fn.set_hook.html)
5//! to make errors nice for humans.
6//!
7//! ## Why?
8//! When you're building a CLI, polish is super important. Even though Rust is
9//! pretty great at safety, it's not unheard of to access the wrong index in a
10//! vector or have an assert fail somewhere.
11//!
12//! When an error eventually occurs, you probably will want to know about it. So
13//! instead of just providing an error message on the command line, we can create a
14//! call to action for people to submit a report.
15//!
16//! This should empower people to engage in communication, lowering the chances
17//! people might get frustrated. And making it easier to figure out what might be
18//! causing bugs.
19//!
20//! ### Default Output
21//!
22//! ```txt
23//! thread 'main' panicked at 'oops', examples/main.rs:2:3
24//! note: Run with `RUST_BACKTRACE=1` for a backtrace.
25//! ```
26//!
27//! ### Human-Panic Output
28//!
29//! ```txt
30//! Well, this is embarrassing.
31//!
32//! human-panic had a problem and crashed. To help us diagnose the problem you can send us a crash report.
33//!
34//! We have generated a report file at "/var/folders/zw/bpfvmq390lv2c6gn_6byyv0w0000gn/T/report-8351cad6-d2b5-4fe8-accd-1fcbf4538792.toml". Submit an issue or email with the subject of "human-panic Crash Report" and include the report as an attachment.
35//!
36//! - Homepage: https://github.com/rust-ci/human-panic
37//! - Authors: Yoshua Wuyts <yoshuawuyts@gmail.com>
38//!
39//! We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports.
40//!
41//! Thank you kindly!
42
43#![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
60/// A convenient metadata struct that describes a crate
61///
62/// See [`metadata!`]
63pub 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    /// See [`metadata!`]
73    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    /// The list of authors of the crate
84    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    /// The URL of the crate's website
93    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    /// The support information
102    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/// Initialize [`Metadata`]
112#[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/// `human-panic` initialisation macro
122///
123/// You can either call this macro with no arguments `setup_panic!()` or
124/// with a Metadata struct, if you don't want the error message to display
125/// the values used in your `Cargo.toml` file.
126///
127/// The Metadata struct can't implement `Default` because of orphan rules, which
128/// means you need to provide all fields for initialisation.
129///
130/// The macro should be called from within a function, for example as the first line of the
131/// `main()` function of the program.
132///
133/// ```
134/// use human_panic::setup_panic;
135/// use human_panic::Metadata;
136///
137/// setup_panic!(Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
138///     .authors("My Company Support <support@mycompany.com>")
139///     .homepage("support.mycompany.com")
140///     .support("- Open a support request by email to support@mycompany.com")
141/// );
142/// ```
143#[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/// Style of panic to be used
176#[non_exhaustive]
177#[derive(Copy, Clone, PartialEq, Eq)]
178pub enum PanicStyle {
179    /// Normal panic
180    Debug,
181    /// Human-formatted panic
182    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/// Utility function that prints a message to our human users
199#[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/// Utility function which will handle dumping information to disk
275#[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}