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/yoshuawuyts/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", deny(missing_docs))]
44 #![cfg_attr(feature = "nightly", feature(external_doc))]
45 #![cfg_attr(feature = "nightly", feature(panic_info_message))]
46 
47 pub mod report;
48 use report::{Method, Report};
49 
50 use std::borrow::Cow;
51 use std::io::{Result as IoResult, Write};
52 use std::panic::PanicInfo;
53 use std::path::{Path, PathBuf};
54 use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
55 
56 /// A convenient metadata struct that describes a crate
57 pub struct Metadata {
58   /// The crate version
59   pub version: Cow<'static, str>,
60   /// The crate name
61   pub name: Cow<'static, str>,
62   /// The list of authors of the crate
63   pub authors: Cow<'static, str>,
64   /// The URL of the crate's website
65   pub homepage: Cow<'static, str>,
66 }
67 
68 /// `human-panic` initialisation macro
69 ///
70 /// You can either call this macro with no arguments `setup_panic!()` or
71 /// with a Metadata struct, if you don't want the error message to display
72 /// the values used in your `Cargo.toml` file.
73 ///
74 /// The Metadata struct can't implement `Default` because of orphan rules, which
75 /// means you need to provide all fields for initialisation.
76 ///
77 /// ```
78 /// use human_panic::setup_panic;
79 ///
80 /// setup_panic!(Metadata {
81 ///     name: env!("CARGO_PKG_NAME").into(),
82 ///     version: env!("CARGO_PKG_VERSION").into(),
83 ///     authors: "My Company Support <support@mycompany.com>".into(),
84 ///     homepage: "support.mycompany.com".into(),
85 /// });
86 /// ```
87 #[macro_export]
88 macro_rules! setup_panic {
89   ($meta:expr) => {
90     #[allow(unused_imports)]
91     use std::panic::{self, PanicInfo};
92     #[allow(unused_imports)]
93     use $crate::{handle_dump, print_msg, Metadata};
94 
95     #[cfg(not(debug_assertions))]
96     match ::std::env::var("RUST_BACKTRACE") {
97       Err(_) => {
98         panic::set_hook(Box::new(move |info: &PanicInfo| {
99           let file_path = handle_dump(&$meta, info);
100           print_msg(file_path, &$meta)
101             .expect("human-panic: printing error message to console failed");
102         }));
103       }
104       Ok(_) => {}
105     }
106   };
107 
108   () => {
109     #[allow(unused_imports)]
110     use std::panic::{self, PanicInfo};
111     #[allow(unused_imports)]
112     use $crate::{handle_dump, print_msg, Metadata};
113 
114     #[cfg(not(debug_assertions))]
115     match ::std::env::var("RUST_BACKTRACE") {
116       Err(_) => {
117         let meta = Metadata {
118           version: env!("CARGO_PKG_VERSION").into(),
119           name: env!("CARGO_PKG_NAME").into(),
120           authors: env!("CARGO_PKG_AUTHORS").replace(":", ", ").into(),
121           homepage: env!("CARGO_PKG_HOMEPAGE").into(),
122         };
123 
124         panic::set_hook(Box::new(move |info: &PanicInfo| {
125           let file_path = handle_dump(&meta, info);
126           print_msg(file_path, &meta)
127             .expect("human-panic: printing error message to console failed");
128         }));
129       }
130       Ok(_) => {}
131     }
132   };
133 }
134 
135 /// Utility function that prints a message to our human users
print_msg<P: AsRef<Path>>( file_path: Option<P>, meta: &Metadata, ) -> IoResult<()>136 pub fn print_msg<P: AsRef<Path>>(
137   file_path: Option<P>,
138   meta: &Metadata,
139 ) -> IoResult<()> {
140   let (_version, name, authors, homepage) =
141     (&meta.version, &meta.name, &meta.authors, &meta.homepage);
142 
143   let stderr = BufferWriter::stderr(ColorChoice::Auto);
144   let mut buffer = stderr.buffer();
145   buffer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
146 
147   writeln!(&mut buffer, "Well, this is embarrassing.\n")?;
148   writeln!(
149     &mut buffer,
150     "{} had a problem and crashed. To help us diagnose the \
151      problem you can send us a crash report.\n",
152     name
153   )?;
154   writeln!(
155     &mut buffer,
156     "We have generated a report file at \"{}\". Submit an \
157      issue or email with the subject of \"{} Crash Report\" and include the \
158      report as an attachment.\n",
159     match file_path {
160       Some(fp) => format!("{}", fp.as_ref().display()),
161       None => "<Failed to store file to disk>".to_string(),
162     },
163     name
164   )?;
165 
166   if !homepage.is_empty() {
167     writeln!(&mut buffer, "- Homepage: {}", homepage)?;
168   }
169   if !authors.is_empty() {
170     writeln!(&mut buffer, "- Authors: {}", authors)?;
171   }
172   writeln!(
173     &mut buffer,
174     "\nWe take privacy seriously, and do not perform any \
175      automated error collection. In order to improve the software, we rely on \
176      people to submit reports.\n"
177   )?;
178   writeln!(&mut buffer, "Thank you kindly!")?;
179 
180   buffer.reset()?;
181 
182   stderr.print(&buffer).unwrap();
183   Ok(())
184 }
185 
186 /// Utility function which will handle dumping information to disk
handle_dump(meta: &Metadata, panic_info: &PanicInfo) -> Option<PathBuf>187 pub fn handle_dump(meta: &Metadata, panic_info: &PanicInfo) -> Option<PathBuf> {
188   let mut expl = String::new();
189 
190   #[cfg(feature = "nightly")]
191   let message = panic_info.message().map(|m| format!("{}", m));
192 
193   #[cfg(not(feature = "nightly"))]
194   let message = match (
195     panic_info.payload().downcast_ref::<&str>(),
196     panic_info.payload().downcast_ref::<String>(),
197   ) {
198     (Some(s), _) => Some(s.to_string()),
199     (_, Some(s)) => Some(s.to_string()),
200     (None, None) => None,
201   };
202 
203   let cause = match message {
204     Some(m) => m,
205     None => "Unknown".into(),
206   };
207 
208   match panic_info.location() {
209     Some(location) => expl.push_str(&format!(
210       "Panic occurred in file '{}' at line {}\n",
211       location.file(),
212       location.line()
213     )),
214     None => expl.push_str("Panic location unknown.\n"),
215   }
216 
217   let report =
218     Report::new(&meta.name, &meta.version, Method::Panic, expl, cause);
219 
220   match report.persist() {
221     Ok(f) => Some(f),
222     Err(_) => {
223       eprintln!("{}", report.serialize().unwrap());
224       None
225     }
226   }
227 }
228