use std::fmt;
use std::panic::PanicHookInfo;
use std::sync::{Mutex, PoisonError};
use oopsie_core::Backtrace;
use oopsie_core::Capturable as _;
#[cfg(feature = "tracing")]
use oopsie_core::SpanTrace;
use crate::ColorMode;
use crate::color::style;
use crate::theme::get_theme;
use crate::trace_printer::{TracePrinter, marker_strip_filter, panic_frame_filter};
#[expect(
clippy::print_stderr,
reason = "a panic hook renders the crash report to stderr, like std's default hook"
)]
pub fn install_panic_hook() {
std::panic::set_hook(Box::new(|info| {
eprint!("{}", PanicReport::new(info));
}));
}
type PriorHook = Box<dyn Fn(&PanicHookInfo<'_>) + Sync + Send + 'static>;
struct HookGuard {
depth: usize,
prior: Option<PriorHook>,
}
static HOOK_GUARD: Mutex<HookGuard> = Mutex::new(HookGuard {
depth: 0,
prior: None,
});
pub fn acquire_hook() {
let mut guard = HOOK_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
if guard.depth == 0 {
guard.prior = Some(std::panic::take_hook());
install_panic_hook();
}
guard.depth += 1;
}
pub fn release_hook() {
let mut guard = HOOK_GUARD.lock().unwrap_or_else(PoisonError::into_inner);
guard.depth -= 1;
if guard.depth == 0
&& let Some(prior) = guard.prior.take()
{
std::panic::set_hook(prior);
}
}
struct PanicReport<'a> {
info: &'a PanicHookInfo<'a>,
backtrace: Backtrace,
backtrace_setting: oopsie_core::RustBacktrace,
#[cfg(feature = "tracing")]
span_trace: Option<SpanTrace>,
color_config: ColorMode,
}
impl<'a> PanicReport<'a> {
fn new(info: &'a PanicHookInfo<'a>) -> Self {
let backtrace_setting = oopsie_core::rust_panic_backtrace();
let backtrace =
oopsie_core::with_rust_backtrace_override(backtrace_setting, Backtrace::capture);
#[cfg(feature = "tracing")]
let span_trace = {
let captured = SpanTrace::capture();
captured.is_captured().then_some(captured)
};
Self {
info,
backtrace,
backtrace_setting,
#[cfg(feature = "tracing")]
span_trace,
color_config: ColorMode::Auto,
}
}
fn write_header(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let c = self.color_config.should_colorize();
let theme = get_theme();
let payload = self.info.payload();
let message = if let Some(s) = payload.downcast_ref::<&str>() {
s
} else if let Some(s) = payload.downcast_ref::<String>() {
s
} else {
"<non-string panic payload>"
};
write!(
f,
"{}\n\
Message: {}\n\
Location: ",
style!(
"The application panicked (crashed).",
theme.error_title(),
c
),
style!(message, theme.panic_message(), c)
)?;
match self.info.location() {
Some(loc) => {
let location = format_args!("{}:{}:{}", loc.file(), loc.line(), loc.column());
writeln!(f, "{}", style!(location, theme.panic_location(), c))?;
}
None => writeln!(f, "<unknown>")?,
}
Ok(())
}
#[cfg(feature = "tracing")]
fn write_span_trace(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(span_trace) = &self.span_trace else {
return Ok(());
};
writeln!(f)?;
let mut printer = TracePrinter::new();
if !self.color_config.should_colorize() {
printer = printer.plain();
}
printer.write_spantrace(f, span_trace)?;
Ok(())
}
fn write_backtrace(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let c = self.color_config.should_colorize();
writeln!(f)?;
if !self.backtrace_setting.is_enabled() {
writeln!(
f,
"{}",
style!(
"note: run with `RUST_BACKTRACE=1` to display a backtrace",
get_theme().hint(),
c
)
)?;
return Ok(());
}
let mut printer = if self.backtrace_setting.is_full() {
TracePrinter::unfiltered()
} else if let Some(cut) = self.backtrace.marker_hidden_frames() {
TracePrinter::with_filter(marker_strip_filter(cut)).add_frame_filter(panic_frame_filter)
} else {
TracePrinter::with_filter(panic_frame_filter)
};
if !c {
printer = printer.plain();
}
printer.write_backtrace(f, &self.backtrace)
}
}
impl fmt::Display for PanicReport<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_header(f)?;
#[cfg(feature = "tracing")]
self.write_span_trace(f)?;
self.write_backtrace(f)?;
Ok(())
}
}