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