use backtrace;
use console::style;
use std::borrow::Cow;
use std::fs::File;
use std::io::{self, BufRead, BufReader, ErrorKind, Write};
use std::panic::PanicInfo;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Verbosity {
Minimal,
Medium,
Full,
}
impl Verbosity {
pub fn from_env() -> Self {
match std::env::var("RUST_BACKTRACE") {
Ok(ref x) if x == "full" => Verbosity::Full,
Ok(_) => Verbosity::Medium,
Err(_) => Verbosity::Minimal,
}
}
fn apply_to_process(self) {
let val = match self {
Verbosity::Full => "full",
Verbosity::Medium => "1",
Verbosity::Minimal => "",
};
if val.is_empty() {
std::env::remove_var("RUST_BACKTRACE");
} else {
std::env::set_var("RUST_BACKTRACE", val);
}
}
}
pub fn install() {
Settings::auto().install()
}
pub fn debug_install() {
Settings::debug().install()
}
struct Frame {
name: Option<String>,
lineno: Option<u32>,
filename: Option<PathBuf>,
}
impl Frame {
fn is_dependency_code(&self) -> bool {
const SYM_PREFIXES: &[&str] = &[
"std::",
"core::",
"backtrace::backtrace::",
"_rust_begin_unwind",
"pretty_backtrace::",
"__rust_",
"___rust_",
"__pthread",
"_main",
"main",
"__scrt_common_main_seh",
"BaseThreadInitThunk",
"_start",
"__libc_start_main",
"start_thread",
];
if let Some(ref name) = self.name {
if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
return true;
}
}
const FILE_PREFIXES: &[&str] = &[
"/rustc/",
"src/libstd/",
"src/libpanic_unwind/",
"src/libtest/",
];
if let Some(ref filename) = self.filename.as_ref().and_then(|x| x.to_str()) {
if filename.contains('<') {
return true;
}
if FILE_PREFIXES.iter().any(|x| filename.starts_with(x))
|| filename.contains("/.cargo/registry/src/")
{
return true;
}
}
false
}
fn is_post_panic_code(&self) -> bool {
const SYM_PREFIXES: &[&str] = &[
"_rust_begin_unwind",
"panic_bounds_check",
"core::result::unwrap_failed",
"core::panicking::panic_fmt",
"color_backtrace::create_panic_handler",
"std::panicking::begin_panic",
"begin_panic_fmt",
"rust_begin_panic",
"panic_bounds_check",
"panic_fmt",
];
if let Some(filename) = self.filename.as_ref().and_then(|x| x.to_str()) {
if filename.contains("libcore/panicking.rs") {
return true;
}
}
match self.name.as_ref() {
Some(name) => SYM_PREFIXES.iter().any(|x| name.starts_with(x)),
None => false,
}
}
fn is_runtime_init_code(&self) -> bool {
const SYM_PREFIXES: &[&str] =
&["std::rt::lang_start::", "test::run_test::run_test_inner::"];
let (name, file) = match (self.name.as_ref(), self.filename.as_ref()) {
(Some(name), Some(filename)) => (name, filename.to_string_lossy()),
_ => return false,
};
if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
return true;
}
if name == "{{closure}}" && file == "src/libtest/lib.rs" {
return true;
}
false
}
fn print_source(&self, s: &Settings) -> Result<(), io::Error> {
let (lineno, filename) = match (self.lineno, self.filename.as_ref()) {
(Some(a), Some(b)) => (a, b),
_ => return Ok(()),
};
print_source(filename, lineno, s)
}
fn print(&self, s: &Settings) -> Result<(), io::Error> {
let is_dependency_code = self.is_dependency_code();
let name = self
.name
.as_ref()
.map(String::as_str)
.unwrap_or("<unknown>");
let has_hash_suffix = name.len() > 19
&& &name[name.len() - 19..name.len() - 16] == "::h"
&& name[name.len() - 16..].chars().all(|x| x.is_digit(16));
let mut name_style = console::Style::new();
if is_dependency_code {
name_style = name_style.cyan();
} else {
name_style = name_style.green();
}
let file = match &self.filename {
Some(filename) => trim_filename(&filename),
None => Cow::Borrowed("<unknown>"),
};
writeln!(
&s.out,
" File \"{}\", line {}, in {}",
style(file).underlined(),
style(self.lineno.unwrap_or(0)).yellow(),
if has_hash_suffix {
name_style.apply_to(&name[..name.len() - 19])
} else {
name_style.apply_to(name)
},
)?;
if s.verbosity >= Verbosity::Full {
self.print_source(s)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Settings {
message: String,
out: console::Term,
verbosity: Verbosity,
backtrace_first: bool,
most_recent_first: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
verbosity: Verbosity::from_env(),
message: "The application panicked (crashed).".to_owned(),
out: console::Term::stderr(),
backtrace_first: true,
most_recent_first: true,
}
}
}
impl Settings {
pub fn new() -> Self {
Self::default()
}
pub fn debug() -> Self {
Self::new().verbosity(Verbosity::Full)
}
pub fn auto() -> Self {
#[cfg(debug_assertions)] {
Self::debug()
}
#[cfg(not(debug_assertions))] {
Self::new()
}
}
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn verbosity(mut self, v: Verbosity) -> Self {
self.verbosity = v;
self
}
pub fn backtrace_first(mut self, value: bool) -> Self {
self.backtrace_first = value;
self
}
pub fn most_recent_first(mut self, value: bool) -> Self {
self.most_recent_first = value;
self
}
pub fn create_panic_handler(self) -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> {
Box::new(move |pi| {
print_panic_and_backtrace(pi, &self).unwrap();
})
}
pub fn install(self) {
self.verbosity.apply_to_process();
std::panic::set_hook(self.create_panic_handler())
}
}
fn print_source(filename: &Path, lineno: u32, s: &Settings) -> Result<(), io::Error> {
let file = match File::open(filename) {
Ok(file) => file,
Err(ref e) if e.kind() == ErrorKind::NotFound => return Ok(()),
e @ Err(_) => e?,
};
let reader = BufReader::new(file);
let source_line = reader.lines().nth((lineno - 1) as usize);
if let Some(Ok(source_line)) = source_line {
writeln!(&s.out, " {}", style(source_line.trim()).dim())?;
}
Ok(())
}
fn print_backtrace(bt: Option<&backtrace::Backtrace>, s: &Settings) -> Result<(), io::Error> {
if s.most_recent_first {
writeln!(
&s.out,
"{}",
style("Backtrace (most recent call first):").bold()
)?;
} else {
writeln!(
&s.out,
"{}",
style("Backtrace (most recent call last):").bold()
)?;
}
let mut frames = Vec::new();
if let Some(bt) = bt {
for frame in bt.frames() {
for sym in frame.symbols() {
frames.push(Frame {
name: sym.name().map(|x| x.to_string()),
lineno: sym.lineno(),
filename: sym.filename().map(|x| x.into()),
});
}
}
} else {
backtrace::trace(|x| {
backtrace::resolve(x.ip(), |sym| {
frames.push(Frame {
name: sym.name().map(|x| x.to_string()),
lineno: sym.lineno(),
filename: sym.filename().map(|x| x.into()),
});
});
true
});
}
let top_cutoff = frames
.iter()
.rposition(Frame::is_post_panic_code)
.map(|x| x + 1)
.unwrap_or(0);
let bottom_cutoff = frames
.iter()
.position(Frame::is_runtime_init_code)
.unwrap_or_else(|| frames.len());
let frames = &frames[top_cutoff..bottom_cutoff];
if s.most_recent_first {
for frame in frames {
frame.print(s)?;
}
} else {
for frame in frames.iter().rev() {
frame.print(s)?;
}
}
Ok(())
}
fn print_panic_and_backtrace(pi: &PanicInfo, s: &Settings) -> Result<(), io::Error> {
if s.backtrace_first {
print_backtrace_info(s)?;
writeln!(&s.out)?;
}
print_panic_info(pi, s)?;
if !s.backtrace_first {
writeln!(&s.out)?;
print_backtrace_info(s)?;
}
Ok(())
}
fn trim_filename(file: &Path) -> Cow<'_, str> {
let filename = file.to_str().unwrap_or("<bad utf8>");
if filename.starts_with("/rustc/") {
if let Some(filename) = filename.get(48..) {
Cow::Owned(format!("rust:{}", filename))
} else {
Cow::Borrowed(filename)
}
} else if let Some(basename) = file.file_name().and_then(|x| x.to_str()) {
if basename.starts_with('<') && basename.ends_with('>') {
Cow::Borrowed(basename)
} else {
Cow::Borrowed(filename)
}
} else {
Cow::Borrowed(filename)
}
}
fn print_panic_info(pi: &PanicInfo, s: &Settings) -> Result<(), io::Error> {
writeln!(&s.out, "{}", style(&s.message).bold())?;
let thread = std::thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
let payload = pi
.payload()
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| pi.payload().downcast_ref::<&str>().cloned())
.unwrap_or("Box<Any>");
for line in payload.lines() {
writeln!(&s.out, " {}", style(line).yellow())?;
}
write!(&s.out, "in ")?;
if let Some(loc) = pi.location() {
writeln!(
&s.out,
"{}, line {}",
style(trim_filename(Path::new(loc.file()))).underlined(),
style(loc.line()).yellow()
)?;
} else {
writeln!(&s.out, "<unknown>")?;
}
writeln!(&s.out, "thread: {}", style(thread_name).yellow())?;
Ok(())
}
fn print_backtrace_info(s: &Settings) -> Result<(), io::Error> {
if s.verbosity == Verbosity::Minimal {
writeln!(
&s.out,
"\nBacktrace omitted. Run with RUST_BACKTRACE=1 to display it."
)?;
}
if s.verbosity <= Verbosity::Medium {
if s.verbosity == Verbosity::Medium {
writeln!(&s.out)?;
}
writeln!(
&s.out,
"Run with RUST_BACKTRACE=full to include source snippets."
)?;
}
if s.verbosity >= Verbosity::Medium {
print_backtrace(None, s)?;
}
Ok(())
}