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;
56use std::panic::PanicHookInfo;
57use std::path::{Path, PathBuf};
58
59/// A convenient metadata struct that describes a crate
60///
61/// See [`metadata!`]
62pub struct Metadata {
63    name: Cow<'static, str>,
64    version: Cow<'static, str>,
65    authors: Option<Cow<'static, str>>,
66    homepage: Option<Cow<'static, str>>,
67    repository: 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            repository: None,
80            support: None,
81        }
82    }
83
84    /// The list of authors of the crate
85    pub fn authors(mut self, value: impl Into<Cow<'static, str>>) -> Self {
86        let value = value.into();
87        if !value.is_empty() {
88            self.authors = value.into();
89        }
90        self
91    }
92
93    /// The URL of the crate's website
94    pub fn homepage(mut self, value: impl Into<Cow<'static, str>>) -> Self {
95        let value = value.into();
96        if !value.is_empty() {
97            self.homepage = value.into();
98        }
99        self
100    }
101
102    /// The URL of the crate's repository
103    pub fn repository(mut self, value: impl Into<Cow<'static, str>>) -> Self {
104        let value = value.into();
105        if !value.is_empty() {
106            self.repository = value.into();
107        }
108        self
109    }
110
111    /// The support information
112    pub fn support(mut self, value: impl Into<Cow<'static, str>>) -> Self {
113        let value = value.into();
114        if !value.is_empty() {
115            self.support = value.into();
116        }
117        self
118    }
119}
120
121/// Initialize [`Metadata`]
122#[macro_export]
123macro_rules! metadata {
124    () => {{
125        $crate::Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
126            .authors(env!("CARGO_PKG_AUTHORS").replace(":", ", "))
127            .homepage(env!("CARGO_PKG_HOMEPAGE"))
128            .repository(env!("CARGO_PKG_REPOSITORY"))
129    }};
130}
131
132/// `human-panic` initialisation macro
133///
134/// You can either call this macro with no arguments `setup_panic!()` or
135/// with a Metadata struct, if you don't want the error message to display
136/// the values used in your `Cargo.toml` file.
137///
138/// The Metadata struct can't implement `Default` because of orphan rules, which
139/// means you need to provide all fields for initialisation.
140///
141/// The macro should be called from within a function, for example as the first line of the
142/// `main()` function of the program.
143///
144/// ```
145/// use human_panic::setup_panic;
146/// use human_panic::Metadata;
147///
148/// setup_panic!(Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
149///     .authors("My Company Support <support@mycompany.com>")
150///     .homepage("support.mycompany.com")
151///     .support("- Open a support request by email to support@mycompany.com")
152/// );
153/// ```
154#[macro_export]
155macro_rules! setup_panic {
156    ($meta:expr) => {{
157        $crate::setup_panic(|| $meta);
158    }};
159
160    () => {
161        $crate::setup_panic!($crate::metadata!());
162    };
163}
164
165#[doc(hidden)]
166pub fn setup_panic(meta: impl Fn() -> Metadata) {
167    #![allow(deprecated)]
168
169    #[allow(unused_imports)]
170    use std::panic;
171
172    match PanicStyle::default() {
173        PanicStyle::Debug => {}
174        PanicStyle::Human => {
175            let meta = meta();
176
177            panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| {
178                let file_path = handle_dump(&meta, info);
179                print_msg(file_path, &meta)
180                    .expect("human-panic: printing error message to console failed");
181            }));
182        }
183    }
184}
185
186/// Style of panic to be used
187#[non_exhaustive]
188#[derive(Copy, Clone, PartialEq, Eq)]
189pub enum PanicStyle {
190    /// Normal panic
191    Debug,
192    /// Human-formatted panic
193    Human,
194}
195
196impl Default for PanicStyle {
197    fn default() -> Self {
198        if cfg!(debug_assertions) {
199            PanicStyle::Debug
200        } else {
201            match ::std::env::var("RUST_BACKTRACE") {
202                Ok(_) => PanicStyle::Debug,
203                Err(_) => PanicStyle::Human,
204            }
205        }
206    }
207}
208
209/// Utility function that prints a message to our human users
210#[cfg(feature = "color")]
211pub fn print_msg<P: AsRef<Path>>(file_path: Option<P>, meta: &Metadata) -> IoResult<()> {
212    use std::io::Write as _;
213
214    let stderr = anstream::stderr();
215    let mut stderr = stderr.lock();
216
217    write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg())?;
218    write_msg(&mut stderr, file_path, meta)?;
219    write!(stderr, "{}", anstyle::Reset.render())?;
220
221    Ok(())
222}
223
224#[cfg(not(feature = "color"))]
225pub fn print_msg<P: AsRef<Path>>(file_path: Option<P>, meta: &Metadata) -> IoResult<()> {
226    let stderr = std::io::stderr();
227    let mut stderr = stderr.lock();
228
229    write_msg(&mut stderr, file_path, meta)?;
230
231    Ok(())
232}
233
234fn write_msg<P: AsRef<Path>>(
235    buffer: &mut impl std::io::Write,
236    file_path: Option<P>,
237    meta: &Metadata,
238) -> IoResult<()> {
239    let Metadata {
240        name,
241        authors,
242        homepage,
243        repository,
244        support,
245        ..
246    } = meta;
247
248    writeln!(buffer, "Well, this is embarrassing.\n")?;
249    writeln!(
250        buffer,
251        "{name} had a problem and crashed. To help us diagnose the \
252     problem you can send us a crash report.\n"
253    )?;
254    writeln!(
255        buffer,
256        "We have generated a report file at \"{}\". Submit an \
257     issue or email with the subject of \"{} Crash Report\" and include the \
258     report as an attachment.\n",
259        match file_path {
260            Some(fp) => format!("{}", fp.as_ref().display()),
261            None => "<Failed to store file to disk>".to_owned(),
262        },
263        name
264    )?;
265
266    if let Some(homepage) = homepage {
267        writeln!(buffer, "- Homepage: {homepage}")?;
268    } else if let Some(repository) = repository {
269        writeln!(buffer, "- Repository: {repository}")?;
270    }
271    if let Some(authors) = authors {
272        writeln!(buffer, "- Authors: {authors}")?;
273    }
274    if let Some(support) = support {
275        writeln!(buffer, "\nTo submit the crash report:\n\n{support}")?;
276    }
277    writeln!(
278        buffer,
279        "\nWe take privacy seriously, and do not perform any \
280     automated error collection. In order to improve the software, we rely on \
281     people to submit reports.\n"
282    )?;
283    writeln!(buffer, "Thank you kindly!")?;
284
285    Ok(())
286}
287
288/// Utility function which will handle dumping information to disk
289#[allow(deprecated)]
290pub fn handle_dump(meta: &Metadata, panic_info: &PanicHookInfo<'_>) -> Option<PathBuf> {
291    let mut expl = String::new();
292
293    let message = match (
294        panic_info.payload().downcast_ref::<&str>(),
295        panic_info.payload().downcast_ref::<String>(),
296    ) {
297        (Some(s), _) => Some((*s).to_owned()),
298        (_, Some(s)) => Some(s.to_owned()),
299        (None, None) => None,
300    };
301
302    let cause = match message {
303        Some(m) => m,
304        None => "Unknown".into(),
305    };
306
307    match panic_info.location() {
308        Some(location) => expl.push_str(&format!(
309            "Panic occurred in file '{}' at line {}\n",
310            location.file(),
311            location.line()
312        )),
313        None => expl.push_str("Panic location unknown.\n"),
314    }
315
316    let report = Report::new(&meta.name, &meta.version, Method::Panic, expl, cause);
317
318    if let Ok(f) = report.persist() {
319        Some(f)
320    } else {
321        use std::io::Write as _;
322        let stderr = std::io::stderr();
323        let mut stderr = stderr.lock();
324
325        let _ = writeln!(
326            stderr,
327            "{}",
328            report
329                .serialize()
330                .expect("only doing toml compatible types")
331        );
332        None
333    }
334}