use backtrace;
use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader, ErrorKind, Write};
use std::panic::PanicInfo;
use std::path::PathBuf;
use std::sync::Mutex;
use term::{self, color, Attr, StderrTerminal};
#[cfg(feature = "failure-bt")]
pub mod failure;
type IOResult<T = ()> = Result<T, std::io::Error>;
#[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,
}
}
}
pub fn create_panic_handler(
settings: Settings,
) -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> {
let settings_mutex = Mutex::new(settings);
Box::new(move |pi| {
let mut settings_lock = settings_mutex.lock().unwrap();
if let Err(e) = print_panic_info(pi, &mut *settings_lock) {
eprintln!("Error while printing panic: {:?}", e);
}
})
}
pub fn install() {
std::panic::set_hook(create_panic_handler(Settings::new()))
}
pub fn install_with_settings(settings: Settings) {
std::panic::set_hook(create_panic_handler(settings))
}
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",
"color_traceback::",
"__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 {
let filename = filename.to_string_lossy();
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",
"core::result::unwrap_failed",
"core::panicking::panic_fmt",
"color_backtrace::create_panic_handler",
"std::panicking::begin_panic",
"begin_panic_fmt",
"failure::backtrace::Backtrace::new",
"backtrace::capture",
"failure::error_message::err_msg",
"<failure::error::Error as core::convert::From<F>>::from",
];
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_if_avail(&self, s: &mut Settings) -> IOResult {
let (lineno, filename) = match (self.lineno, self.filename.as_ref()) {
(Some(a), Some(b)) => (a, b),
_ => return Ok(()),
};
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 start_line = lineno - 2.min(lineno - 1);
let surrounding_src = reader.lines().skip(start_line as usize - 1).take(5);
for (line, cur_line_no) in surrounding_src.zip(start_line..) {
if cur_line_no == lineno {
s.out.attr(Attr::Bold)?;
writeln!(s.out, "{:>8} > {}", cur_line_no, line?)?;
s.out.reset()?;
} else {
writeln!(s.out, "{:>8} │ {}", cur_line_no, line?)?;
}
}
Ok(())
}
fn print(&self, i: usize, s: &mut Settings) -> IOResult {
let is_dependency_code = self.is_dependency_code();
write!(s.out, "{:>2}: ", i)?;
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));
s.out.fg(if is_dependency_code {
color::GREEN
} else {
color::BRIGHT_RED
})?;
if has_hash_suffix && s.dim_function_hash_part {
write!(s.out, "{}", &name[..name.len() - 19])?;
s.out.fg(color::BRIGHT_BLACK)?;
writeln!(s.out, "{}", &name[name.len() - 19..])?;
} else {
writeln!(s.out, "{}", name)?;
}
s.out.reset()?;
if let Some(ref file) = self.filename {
let filestr = file.to_str().unwrap_or("<bad utf8>");
let lineno = self
.lineno
.map_or("<unknown line>".to_owned(), |x| x.to_string());
writeln!(s.out, " at {}:{}", filestr, lineno)?;
} else {
writeln!(s.out, " at <unknown source file>")?;
}
if s.verbosity >= Verbosity::Full {
self.print_source_if_avail(s)?;
}
Ok(())
}
}
pub struct Settings {
message: String,
out: Box<dyn PanicOutputStream>,
verbosity: Verbosity,
dim_function_hash_part: bool,
}
impl Default for Settings {
fn default() -> Self {
let term = term::stderr();
Self {
verbosity: Verbosity::from_env(),
message: "The application panicked (crashed).".to_owned(),
out: if term.is_some() && atty::is(atty::Stream::Stderr) {
Box::new(ColorizedStderrOutput::new(term.unwrap()))
} else {
Box::new(StreamOutput::new(std::io::stderr()))
},
dim_function_hash_part: true,
}
}
}
impl fmt::Debug for Settings {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("Settings")
.field("message", &self.message)
.field("verbosity", &self.verbosity)
.field("dim_function_hash_part", &self.dim_function_hash_part)
.finish()
}
}
impl Settings {
pub fn new() -> Self {
Self::default()
}
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn output_stream(mut self, out: Box<dyn PanicOutputStream>) -> Self {
self.out = out;
self
}
pub fn verbosity(mut self, v: Verbosity) -> Self {
self.verbosity = v;
self
}
pub fn dim_function_hash_part(mut self, dim: bool) -> Self {
self.dim_function_hash_part = dim;
self
}
}
pub trait Colorize {
fn fg(&mut self, color: color::Color) -> IOResult;
fn bg(&mut self, color: color::Color) -> IOResult;
fn attr(&mut self, attr: Attr) -> IOResult;
fn reset(&mut self) -> IOResult;
}
pub trait PanicOutputStream: Colorize + Write + Send {}
pub struct ColorizedStderrOutput {
term: Box<StderrTerminal>,
}
impl ColorizedStderrOutput {
pub fn new(term: Box<StderrTerminal>) -> Self {
Self { term }
}
}
impl Colorize for ColorizedStderrOutput {
fn fg(&mut self, color: color::Color) -> IOResult {
Ok(self.term.fg(color)?)
}
fn bg(&mut self, color: color::Color) -> IOResult {
Ok(self.term.bg(color)?)
}
fn attr(&mut self, attr: Attr) -> IOResult {
Ok(self.term.attr(attr)?)
}
fn reset(&mut self) -> IOResult {
Ok(self.term.reset()?)
}
}
impl Write for ColorizedStderrOutput {
fn write(&mut self, buf: &[u8]) -> IOResult<usize> {
self.term.get_mut().write(buf)
}
fn flush(&mut self) -> IOResult {
self.term.get_mut().flush()
}
}
impl PanicOutputStream for ColorizedStderrOutput {}
pub struct StreamOutput<T: Write + Send> {
stream: T,
}
impl<T: Write + Send> StreamOutput<T> {
pub fn new(stream: T) -> Self {
Self { stream }
}
}
impl<T: Write + Send> Write for StreamOutput<T> {
fn write(&mut self, buf: &[u8]) -> IOResult<usize> {
self.stream.write(buf)
}
fn flush(&mut self) -> IOResult {
self.stream.flush()
}
}
impl<T: Write + Send> Colorize for StreamOutput<T> {
fn fg(&mut self, _color: color::Color) -> IOResult {
Ok(())
}
fn bg(&mut self, _color: color::Color) -> IOResult {
Ok(())
}
fn attr(&mut self, _attr: Attr) -> IOResult {
Ok(())
}
fn reset(&mut self) -> IOResult {
Ok(())
}
}
impl<T: Write + Send> PanicOutputStream for StreamOutput<T> {}
pub fn print_backtrace(trace: &backtrace::Backtrace, settings: &mut Settings) -> IOResult {
let s = settings;
writeln!(s.out, "{:━^80}", " BACKTRACE ")?;
let frames: Vec<_> = trace
.frames()
.iter()
.flat_map(|frame| frame.symbols())
.map(|sym| Frame {
name: sym.name().map(|x| x.to_string()),
lineno: sym.lineno(),
filename: sym.filename().map(|x| x.into()),
})
.collect();
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());
if top_cutoff != 0 {
let text = format!("({} post panic frames hidden)", top_cutoff);
s.out.fg(color::BRIGHT_CYAN)?;
writeln!(s.out, "{:^80}", text)?;
s.out.reset()?;
}
let num_frames = frames.len();
let frames = frames
.into_iter()
.skip(top_cutoff)
.take(bottom_cutoff - top_cutoff)
.zip(top_cutoff..);
for (frame, i) in frames {
frame.print(i, s)?;
}
if bottom_cutoff != num_frames {
let text = format!(
"({} runtime init frames hidden)",
num_frames - bottom_cutoff
);
s.out.fg(color::BRIGHT_CYAN)?;
writeln!(s.out, "{:^80}", text)?;
s.out.reset()?;
}
Ok(())
}
pub fn print_panic_info(pi: &PanicInfo, s: &mut Settings) -> IOResult {
s.out.fg(color::RED)?;
writeln!(s.out, "{}", s.message)?;
s.out.reset()?;
let payload = pi
.payload()
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| pi.payload().downcast_ref::<&str>().cloned())
.unwrap_or("<non string panic payload>");
write!(s.out, "Message: ")?;
s.out.fg(color::CYAN)?;
writeln!(s.out, "{}", payload)?;
s.out.reset()?;
write!(s.out, "Location: ")?;
if let Some(loc) = pi.location() {
s.out.fg(color::MAGENTA)?;
write!(s.out, "{}", loc.file())?;
s.out.fg(color::WHITE)?;
write!(s.out, ":")?;
s.out.fg(color::MAGENTA)?;
writeln!(s.out, "{}", loc.line())?;
s.out.reset()?;
} else {
writeln!(s.out, "<unknown>")?;
}
if s.verbosity == Verbosity::Minimal {
write!(s.out, "\nBacktrace omitted. Run with ")?;
s.out.attr(Attr::Bold)?;
write!(s.out, "RUST_BACKTRACE=1")?;
s.out.reset()?;
writeln!(s.out, " environment variable to display it.")?;
}
if s.verbosity <= Verbosity::Medium {
if s.verbosity == Verbosity::Medium {
writeln!(s.out)?;
}
write!(s.out, "Run with ")?;
s.out.attr(Attr::Bold)?;
write!(s.out, "RUST_BACKTRACE=full")?;
s.out.reset()?;
writeln!(s.out, " to include source snippets.")?;
}
if s.verbosity >= Verbosity::Medium {
print_backtrace(&backtrace::Backtrace::new(), s)?;
}
Ok(())
}