diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 33df0a4..e2162ee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,9 @@ jobs: features: ["--all-features"] include: - image_name: "ubuntu-22.04" - features: "--no-default-features" + features: "--no-default-features" # can't print any traces, but should compile at least + - image_name: "ubuntu-22.04" + features: "--no-default-features --features use-btparse-crate" steps: - name: Clone uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 861d740..7d3a20e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. +## Unreleased +- Add `Backtrace` trait to abstract over backtrace implementation +- `BacktracePrinter::print_trace` now takes `&dyn Backtrace` instead of `&backtrace::Backtrace` + - This may be API breaking when users use `default-features = false` so that `&backtrace::Backtrace` doesn't coerce to `&dyn Backtrace` +- Add experimental support for `std::backtrace::Backtrace` + - Enable via `{ default-features = false, features = ["use-btparse-crate"] }` + ## [v0.6.1] (2023-10-23) - Publicly expose some helper methods on `Frame` type diff --git a/Cargo.toml b/Cargo.toml index 90b3f40..952f69e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,22 +9,26 @@ description = "Colorful panic backtraces" readme = "README.md" rust-version = "1.70" -keywords = [ - "backtrace", - "color", - "colour", - "stacktrace", - "pretty", -] +keywords = ["backtrace", "color", "colour", "stacktrace", "pretty"] [features] -default = [] -resolve-modules = ["regex"] +default = ["use-backtrace-crate"] + +# Print module memory mappings. Only takes effect on Linux. +resolve-modules = ["dep:regex", "use-backtrace-crate"] + +# Uses backtrace-rs crate. Reliable. Preferred over btparse if both are enabled. +use-backtrace-crate = ["dep:backtrace"] + +# Uses btparse to parse the unstable debug repr of std::backtrace::Backtrace. +# Not guaranteed to work if Rust decides to change the format, but fewer dependencies. +use-btparse-crate = ["dep:btparse"] # Deprecated, no longer has any effect: backtrace crate removed corresponding option. gimli-symbolize = [] [dependencies] termcolor = "1.1.2" -backtrace = "0.3.57" +backtrace = { version = "0.3.57", optional = true } regex = { version = "1.4.6", optional = true } +btparse = { version = "*", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 8d360a7..6f441e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,6 +146,77 @@ pub fn install_with_settings(printer: BacktracePrinter) { std::panic::set_hook(printer.into_panic_handler(default_output_stream())) } +// ============================================================================================== // +// [Backtrace abstraction] // +// ============================================================================================== // + +/// Abstraction over backtrace library implementations. +pub trait Backtrace { + fn frames(&self) -> Vec; +} + +#[cfg(feature = "use-backtrace-crate")] +impl Backtrace for backtrace::Backtrace { + fn frames(&self) -> Vec { + backtrace::Backtrace::frames(self) + .iter() + .flat_map(|frame| frame.symbols().iter().map(move |sym| (frame.ip(), sym))) + .zip(1usize..) + .map(|((ip, sym), n)| Frame { + name: sym.name().map(|x| x.to_string()), + lineno: sym.lineno(), + filename: sym.filename().map(|x| x.into()), + n, + ip: Some(ip as usize), + _private_ctor: (), + }) + .collect() + } +} + +#[cfg(feature = "use-btparse-crate")] +impl Backtrace for btparse::Backtrace { + fn frames(&self) -> Vec { + self.frames + .iter() + .zip(1usize..) + .map(|(frame, n)| Frame { + n, + name: Some(frame.function.clone()), + lineno: frame.line.map(|x| x as u32), + filename: frame.file.as_ref().map(|x| x.clone().into()), + ip: None, + _private_ctor: (), + }) + .collect() + } +} + +// Capture a backtrace with the most reliable available backtrace implementation. +fn capture_backtrace() -> Result, Box> { + #[cfg(all(feature = "use-backtrace-crate", feature = "use-btparse-crate"))] + return Ok(Box::new(backtrace::Backtrace::new())); + + #[cfg(all(feature = "use-backtrace-crate", not(feature = "use-btparse-crate")))] + return Ok(Box::new(backtrace::Backtrace::new())); + + #[cfg(all(not(feature = "use-backtrace-crate"), feature = "use-btparse-crate"))] + { + let bt = std::backtrace::Backtrace::force_capture(); + return Ok(Box::new(btparse::deserialize(&bt).map_err(Box::new)?)); + } + + #[cfg(all( + not(feature = "use-backtrace-crate"), + not(feature = "use-btparse-crate") + ))] + { + return Err(Box::new( + "need to enable at least one backtrace crate selector feature", + )); + } +} + // ============================================================================================== // // [Backtrace frame] // // ============================================================================================== // @@ -158,7 +229,7 @@ pub struct Frame { pub name: Option, pub lineno: Option, pub filename: Option, - pub ip: usize, + pub ip: Option, _private_ctor: (), } @@ -364,11 +435,13 @@ impl Frame { // Print frame index. write!(out, "{:>2}: ", i)?; - if s.should_print_addresses() { - if let Some((module_name, module_base)) = self.module_info() { - write!(out, "{}:0x{:08x} - ", module_name, self.ip - module_base)?; - } else { - write!(out, "0x{:016x} - ", self.ip)?; + if let Some(ip) = self.ip { + if s.should_print_addresses() { + if let Some((module_name, module_base)) = self.module_info() { + write!(out, "{}:0x{:08x} - ", module_name, ip - module_base)?; + } else { + write!(out, "0x{:016x} - ", ip)?; + } } } @@ -659,24 +732,11 @@ impl BacktracePrinter { } /// Pretty-prints a [`backtrace::Backtrace`] to an output stream. - pub fn print_trace(&self, trace: &backtrace::Backtrace, out: &mut impl WriteColor) -> IOResult { + pub fn print_trace(&self, trace: &dyn Backtrace, out: &mut impl WriteColor) -> IOResult { writeln!(out, "{:━^80}", " BACKTRACE ")?; // Collect frame info. - let frames: Vec<_> = trace - .frames() - .iter() - .flat_map(|frame| frame.symbols().iter().map(move |sym| (frame.ip(), sym))) - .zip(1usize..) - .map(|((ip, sym), n)| Frame { - name: sym.name().map(|x| x.to_string()), - lineno: sym.lineno(), - filename: sym.filename().map(|x| x.into()), - n, - ip: ip as usize, - _private_ctor: (), - }) - .collect(); + let frames = trace.frames(); let mut filtered_frames = frames.iter().collect(); match env::var("COLORBT_SHOW_HIDDEN").ok().as_deref() { @@ -731,7 +791,7 @@ impl BacktracePrinter { } /// Pretty-print a backtrace to a `String`, using VT100 color codes. - pub fn format_trace_to_string(&self, trace: &backtrace::Backtrace) -> IOResult { + pub fn format_trace_to_string(&self, trace: &dyn Backtrace) -> IOResult { // TODO: should we implicitly enable VT100 support on Windows here? let mut ansi = Ansi::new(vec![]); self.print_trace(trace, &mut ansi)?; @@ -795,7 +855,10 @@ impl BacktracePrinter { } if self.current_verbosity() >= Verbosity::Medium { - self.print_trace(&backtrace::Backtrace::new(), out)?; + match capture_backtrace() { + Ok(trace) => self.print_trace(&*trace, out)?, + Err(e) => writeln!(out, "Failed to capture backtrace: {e}")?, + } } Ok(()) @@ -820,6 +883,7 @@ impl BacktracePrinter { #[doc(hidden)] #[deprecated(since = "0.4.0", note = "Use `BacktracePrinter::print_trace` instead`")] +#[cfg(feature = "use-backtrace-crate")] pub fn print_backtrace(trace: &backtrace::Backtrace, s: &mut BacktracePrinter) -> IOResult { s.print_trace(trace, &mut default_output_stream()) }