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(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
56/// A convenient metadata struct that describes a crate
57///
58/// See [`metadata!`]
59pub 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    /// See [`metadata!`]
69    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    /// The list of authors of the crate
80    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    /// The URL of the crate's website
89    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    /// The support information
98    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/// Initialize [`Metadata`]
108#[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/// `human-panic` initialisation macro
118///
119/// You can either call this macro with no arguments `setup_panic!()` or
120/// with a Metadata struct, if you don't want the error message to display
121/// the values used in your `Cargo.toml` file.
122///
123/// The Metadata struct can't implement `Default` because of orphan rules, which
124/// means you need to provide all fields for initialisation.
125///
126/// The macro should be called from within a function, for example as the first line of the
127/// `main()` function of the program.
128///
129/// ```
130/// use human_panic::setup_panic;
131/// use human_panic::Metadata;
132///
133/// setup_panic!(Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
134///     .authors("My Company Support <support@mycompany.com>")
135///     .homepage("support.mycompany.com")
136///     .support("- Open a support request by email to support@mycompany.com")
137/// );
138/// ```
139#[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/// Style of panic to be used
170#[non_exhaustive]
171#[derive(Copy, Clone, PartialEq, Eq)]
172pub enum PanicStyle {
173    /// Normal panic
174    Debug,
175    /// Human-formatted panic
176    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/// Utility function that prints a message to our human users
193#[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
268/// Utility function which will handle dumping information to disk
269pub 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}