better_panic/
lib.rs

1//! `better-panic` gives you pretty backtraces for panics.
2//!
3//! It is inspired by Python tracebacks and tries to replicate them as well
4//! as possible.  This is what it looks like:
5//!
6//! <img src="https://github.com/mitsuhiko/better-panic/raw/master/screenshot.png">
7//!
8//! Some of the code is based on the
9//! [color-backtrace](https://crates.io/crates/color-backtrace) library.
10//!
11//! ## Usage
12//!
13//! The most common way to use it is to invoke the `install` function
14//! which installs a panic handler.  In debug builds the backtrace is shown
15//! automatically, in release builds it's hidden by default.
16//!
17//! ```
18//! better_panic::install();
19//! ```
20//!
21//! For more configuration see the `Settings` object.
22//!
23//! ## Features
24//!
25//! - Colorize backtraces to be easier on the eyes
26//! - Show source snippets if source files are found on disk
27//! - Hide all the frames after the panic was already initiated
28use console::style;
29use std::borrow::Cow;
30use std::fs::File;
31use std::io::{self, BufRead, BufReader, ErrorKind, Write};
32use std::panic::PanicInfo;
33use std::path::{Path, PathBuf};
34
35/// Defines how verbose the backtrace is supposed to be.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
37pub enum Verbosity {
38    /// Print a small message including the panic payload and the panic location.
39    Minimal,
40    /// Everything in `Minimal` and additionally print a backtrace.
41    Medium,
42    /// Everything in `Medium` plus source snippets for all backtrace locations.
43    Full,
44}
45
46impl Verbosity {
47    /// Get the verbosity level from the `RUST_BACKTRACE` env variable.
48    pub fn from_env() -> Self {
49        match std::env::var("RUST_BACKTRACE") {
50            Ok(ref x) if x == "full" => Verbosity::Full,
51            Ok(_) => Verbosity::Medium,
52            Err(_) => Verbosity::Minimal,
53        }
54    }
55
56    fn apply_to_process(self) {
57        let val = match self {
58            Verbosity::Full => "full",
59            Verbosity::Medium => "1",
60            Verbosity::Minimal => "",
61        };
62        if val.is_empty() {
63            std::env::remove_var("RUST_BACKTRACE");
64        } else {
65            std::env::set_var("RUST_BACKTRACE", val);
66        }
67    }
68}
69
70/// Installs the panic handler with `Settings::auto`.
71pub fn install() {
72    Settings::auto().install()
73}
74
75/// Installs the panic handler with debug settings.
76pub fn debug_install() {
77    Settings::debug().install()
78}
79
80struct Frame {
81    name: Option<String>,
82    lineno: Option<u32>,
83    filename: Option<PathBuf>,
84}
85
86impl Frame {
87    fn name_without_hash(&self) -> Option<&str> {
88        let name = self.name.as_ref()?;
89        let has_hash_suffix = name.len() > 19
90            && &name[name.len() - 19..name.len() - 16] == "::h"
91            && name[name.len() - 16..].chars().all(|x| x.is_digit(16));
92        if has_hash_suffix {
93            Some(&name[..name.len() - 19])
94        } else {
95            Some(name)
96        }
97    }
98
99    fn is_dependency_code(&self) -> bool {
100        const SYM_PREFIXES: &[&str] = &[
101            "std::",
102            "core::",
103            "backtrace::backtrace::",
104            "_rust_begin_unwind",
105            "better_panic::",
106            "__rust_",
107            "___rust_",
108            "__pthread",
109            "_main",
110            "main",
111            "__scrt_common_main_seh",
112            "BaseThreadInitThunk",
113            "_start",
114            "__libc_start_main",
115            "start_thread",
116        ];
117
118        // Inspect name.
119        if let Some(ref name) = self.name {
120            if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
121                return true;
122            }
123        }
124
125        const FILE_PREFIXES: &[&str] = &[
126            "rust:",
127            "/rustc/",
128            "src/libstd/",
129            "src/libpanic_unwind/",
130            "src/libtest/",
131        ];
132
133        // Inspect filename.
134        if let Some(filename) = self.filename.as_ref().and_then(|x| x.to_str()) {
135            // some filenames are really weird from macro expansion.  consider
136            // them not to be part of the user code.
137            if filename.contains('<') {
138                return true;
139            }
140            if FILE_PREFIXES.iter().any(|x| filename.starts_with(x))
141                || filename.contains("/.cargo/registry/src/")
142            {
143                return true;
144            }
145        }
146
147        false
148    }
149
150    // Heuristically determine whether a frame is likely to be a post panic
151    // frame.
152    //
153    // Post panic frames are frames of a functions called after the actual panic
154    // is already in progress and don't contain any useful information for a
155    // reader of the backtrace.
156    fn is_post_panic_code(&self) -> bool {
157        const SYM_PREFIXES: &[&str] = &[
158            "_rust_begin_unwind",
159            "panic_bounds_check",
160            "core::result::unwrap_failed",
161            "core::panicking::panic_fmt",
162            "core::panicking::panic_bounds_check",
163            "color_backtrace::create_panic_handler",
164            "std::panicking::begin_panic",
165            "begin_panic_fmt",
166            "rust_begin_panic",
167            "panic_bounds_check",
168            "panic_fmt",
169        ];
170
171        if let Some(filename) = self.filename.as_ref().and_then(|x| x.to_str()) {
172            if filename.contains("libcore/panicking.rs") {
173                return true;
174            }
175        }
176
177        match self.name_without_hash() {
178            Some(name) => SYM_PREFIXES
179                .iter()
180                .any(|x| name.starts_with(x) || name.ends_with("__rust_end_short_backtrace")),
181            None => false,
182        }
183    }
184
185    // Heuristically determine whether a frame is likely to be part of language
186    // runtime.
187    fn is_runtime_init_code(&self) -> bool {
188        const SYM_PREFIXES: &[&str] =
189            &["std::rt::lang_start::", "test::run_test::run_test_inner::"];
190
191        let (name, file) = match (self.name_without_hash(), self.filename.as_ref()) {
192            (Some(name), Some(filename)) => (name, filename.to_string_lossy()),
193            _ => return false,
194        };
195
196        if SYM_PREFIXES
197            .iter()
198            .any(|x| name.starts_with(x) || name.ends_with("__rust_start_short_backtrace"))
199        {
200            return true;
201        }
202
203        // For Linux, this is the best rule for skipping test init I found.
204        if name == "{{closure}}" && file == "src/libtest/lib.rs" {
205            return true;
206        }
207
208        false
209    }
210
211    /// Is this a call once frame?
212    fn is_call_once(&self) -> bool {
213        if let Some(name) = self.name_without_hash() {
214            name.ends_with("FnOnce::call_once")
215        } else {
216            false
217        }
218    }
219
220    fn print_source(&self, s: &Settings) -> Result<(), io::Error> {
221        let (lineno, filename) = match (self.lineno, self.filename.as_ref()) {
222            (Some(a), Some(b)) => (a, b),
223            // Without a line number and file name, we can't sensibly proceed.
224            _ => return Ok(()),
225        };
226
227        print_source(filename, lineno, s)
228    }
229
230    fn print(&self, s: &Settings) -> Result<(), io::Error> {
231        let is_dependency_code = self.is_dependency_code();
232
233        let name = self.name_without_hash().unwrap_or("<unknown>");
234
235        // Print function name.
236        let mut name_style = console::Style::new();
237        if is_dependency_code {
238            name_style = name_style.cyan();
239        } else {
240            name_style = name_style.green();
241        }
242
243        // Print source location, if known.
244        let file = match &self.filename {
245            Some(filename) => trim_filename(filename),
246            None => Cow::Borrowed("<unknown>"),
247        };
248
249        if s.lineno_suffix {
250            writeln!(
251                &s.out,
252                "  File \"{}:{}\", in {}",
253                style(file).underlined(),
254                style(self.lineno.unwrap_or(0)).yellow(),
255                name_style.apply_to(name)
256            )?;
257        } else {
258            writeln!(
259                &s.out,
260                "  File \"{}\", line {}, in {}",
261                style(file).underlined(),
262                style(self.lineno.unwrap_or(0)).yellow(),
263                name_style.apply_to(name)
264            )?;
265        }
266
267        // Maybe print source.
268        if s.verbosity >= Verbosity::Full {
269            self.print_source(s)?;
270        }
271
272        Ok(())
273    }
274}
275
276/// Configuration for panic printing.
277#[derive(Debug, Clone)]
278pub struct Settings {
279    message: String,
280    out: console::Term,
281    verbosity: Verbosity,
282    backtrace_first: bool,
283    most_recent_first: bool,
284    lineno_suffix: bool,
285}
286
287impl Default for Settings {
288    fn default() -> Self {
289        Self {
290            verbosity: Verbosity::from_env(),
291            message: "The application panicked (crashed).".to_owned(),
292            out: console::Term::stderr(),
293            backtrace_first: true,
294            most_recent_first: true,
295            lineno_suffix: false,
296        }
297    }
298}
299
300impl Settings {
301    /// Alias for `Settings::default`.
302    pub fn new() -> Self {
303        Self::default()
304    }
305
306    /// Common settings for debugging.
307    pub fn debug() -> Self {
308        Self::new().verbosity(Verbosity::Full)
309    }
310
311    /// In release builds this is `new`, in debug builds this is `debug`.
312    pub fn auto() -> Self {
313        #[cfg(debug_assertions)]
314        {
315            Self::debug()
316        }
317        #[cfg(not(debug_assertions))]
318        {
319            Self::new()
320        }
321    }
322
323    /// Controls the "greeting" message of the panic.
324    ///
325    /// Defaults to `"The application panicked (crashed)"`.
326    pub fn message(mut self, message: impl Into<String>) -> Self {
327        self.message = message.into();
328        self
329    }
330
331    /// Controls the verbosity level.
332    ///
333    /// Defaults to `Verbosity::get_env()`.
334    pub fn verbosity(mut self, v: Verbosity) -> Self {
335        self.verbosity = v;
336        self
337    }
338
339    /// Controls the backtrace position.
340    ///
341    /// Defaults to `true` which causes the backtrace to be printed above
342    /// the panic.
343    pub fn backtrace_first(mut self, value: bool) -> Self {
344        self.backtrace_first = value;
345        self
346    }
347
348    /// Controls the most-recent-first behavior.
349    ///
350    /// Defaults to `true` which causes the backtrace to be printed above
351    /// the panic.
352    pub fn most_recent_first(mut self, value: bool) -> Self {
353        self.most_recent_first = value;
354        self
355    }
356
357    /// Append the line number as suffix to the filename.
358    ///
359    /// Defaults to `false` which causes line numbers to be rendered separately.
360    /// Specifically this renders `File "foo.rs:42"` instead of
361    /// `File "foo.rs", line 42` which lets some terminals open the editor
362    /// at the right location on click.
363    pub fn lineno_suffix(mut self, value: bool) -> Self {
364        self.lineno_suffix = value;
365        self
366    }
367
368    /// Consumes the settings and creates a panic handler.
369    pub fn create_panic_handler(self) -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> {
370        Box::new(move |pi| {
371            print_panic_and_backtrace(pi, &self).unwrap();
372        })
373    }
374
375    /// Installs the panic handler.
376    pub fn install(self) {
377        self.verbosity.apply_to_process();
378        std::panic::set_hook(self.create_panic_handler())
379    }
380}
381
382fn print_source(filename: &Path, lineno: u32, s: &Settings) -> Result<(), io::Error> {
383    let file = match File::open(filename) {
384        Ok(file) => file,
385        Err(ref e) if e.kind() == ErrorKind::NotFound => return Ok(()),
386        e @ Err(_) => e?,
387    };
388
389    let reader = BufReader::new(file);
390    let source_line = reader.lines().nth((lineno - 1) as usize);
391    if let Some(Ok(source_line)) = source_line {
392        writeln!(&s.out, "    {}", style(source_line.trim()).dim())?;
393    }
394
395    Ok(())
396}
397
398fn print_backtrace(bt: Option<&backtrace::Backtrace>, s: &Settings) -> Result<(), io::Error> {
399    if s.most_recent_first {
400        writeln!(
401            &s.out,
402            "{}",
403            style("Backtrace (most recent call first):").bold()
404        )?;
405    } else {
406        writeln!(
407            &s.out,
408            "{}",
409            style("Backtrace (most recent call last):").bold()
410        )?;
411    }
412
413    // Collect frame info.
414    let mut frames = Vec::new();
415    if let Some(bt) = bt {
416        for frame in bt.frames() {
417            for sym in frame.symbols() {
418                frames.push(Frame {
419                    name: sym.name().map(|x| x.to_string()),
420                    lineno: sym.lineno(),
421                    filename: sym.filename().map(|x| x.into()),
422                });
423            }
424        }
425    } else {
426        backtrace::trace(|x| {
427            // TODO: Don't just drop unresolvable frames.
428            backtrace::resolve(x.ip(), |sym| {
429                frames.push(Frame {
430                    name: sym.name().map(|x| x.to_string()),
431                    lineno: sym.lineno(),
432                    filename: sym.filename().map(|x| x.into()),
433                });
434            });
435
436            true
437        });
438    }
439
440    // Try to find where the interesting part starts...
441    let top_cutoff = frames
442        .iter()
443        .rposition(Frame::is_post_panic_code)
444        .map(|x| x + 1)
445        .unwrap_or(0);
446
447    // Try to find where language init frames start ...
448    let bottom_cutoff = frames
449        .iter()
450        .position(Frame::is_runtime_init_code)
451        .map(|x| x - 1)
452        .unwrap_or_else(|| frames.len());
453
454    // Turn them into `Frame` objects and print them.
455    let mut frames = &frames[top_cutoff..bottom_cutoff];
456
457    if !frames.is_empty() && frames[frames.len() - 1].is_call_once() {
458        frames = &frames[..frames.len() - 1];
459    }
460
461    if s.most_recent_first {
462        for frame in frames {
463            frame.print(s)?;
464        }
465    } else {
466        for frame in frames.iter().rev() {
467            frame.print(s)?;
468        }
469    }
470
471    Ok(())
472}
473
474fn print_panic_and_backtrace(pi: &PanicInfo, s: &Settings) -> Result<(), io::Error> {
475    if s.backtrace_first {
476        print_backtrace_info(s)?;
477        writeln!(&s.out)?;
478    }
479    print_panic_info(pi, s)?;
480    if !s.backtrace_first {
481        writeln!(&s.out)?;
482        print_backtrace_info(s)?;
483    }
484    Ok(())
485}
486
487fn trim_filename(file: &Path) -> Cow<'_, str> {
488    let filename = file.to_str().unwrap_or("<bad utf8>");
489    if filename.starts_with("/rustc/") {
490        if let Some(filename) = filename.get(48..) {
491            Cow::Owned(format!("rust:{}", filename))
492        } else {
493            Cow::Borrowed(filename)
494        }
495    } else if let Some(basename) = file.file_name().and_then(|x| x.to_str()) {
496        if basename.starts_with('<') && basename.ends_with('>') {
497            Cow::Borrowed(basename)
498        } else {
499            Cow::Borrowed(filename)
500        }
501    } else {
502        Cow::Borrowed(filename)
503    }
504}
505
506fn print_panic_info(pi: &PanicInfo, s: &Settings) -> Result<(), io::Error> {
507    writeln!(&s.out, "{}", style(&s.message).bold())?;
508
509    let thread = std::thread::current();
510    let thread_name = thread.name().unwrap_or("<unnamed>");
511
512    // Print panic message.
513    let payload = pi
514        .payload()
515        .downcast_ref::<String>()
516        .map(String::as_str)
517        .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
518        .unwrap_or("Box<Any>");
519
520    for line in payload.lines() {
521        writeln!(&s.out, "  {}", style(line).yellow())?;
522    }
523
524    // If known, print panic location.
525    write!(&s.out, "in ")?;
526    if let Some(loc) = pi.location() {
527        if s.lineno_suffix {
528            writeln!(
529                &s.out,
530                "{}:{}",
531                style(trim_filename(Path::new(loc.file()))).underlined(),
532                style(loc.line()).yellow()
533            )?;
534        } else {
535            writeln!(
536                &s.out,
537                "{}, line {}",
538                style(trim_filename(Path::new(loc.file()))).underlined(),
539                style(loc.line()).yellow()
540            )?;
541        }
542    } else {
543        writeln!(&s.out, "<unknown>")?;
544    }
545    writeln!(&s.out, "thread: {}", style(thread_name).yellow())?;
546    Ok(())
547}
548
549fn print_backtrace_info(s: &Settings) -> Result<(), io::Error> {
550    // Print some info on how to increase verbosity.
551    if s.verbosity == Verbosity::Minimal {
552        writeln!(
553            &s.out,
554            "\nBacktrace omitted. Run with RUST_BACKTRACE=1 to display it."
555        )?;
556    }
557    if s.verbosity <= Verbosity::Medium {
558        if s.verbosity == Verbosity::Medium {
559            // If exactly medium, no newline was printed before.
560            writeln!(&s.out)?;
561        }
562
563        writeln!(
564            &s.out,
565            "Run with RUST_BACKTRACE=full to include source snippets."
566        )?;
567    }
568
569    if s.verbosity >= Verbosity::Medium {
570        print_backtrace(None, s)?;
571    }
572
573    Ok(())
574}