use std::any::Any;
use std::cell::RefCell;
use std::fmt::{Debug, Display};
use std::panic::{PanicHookInfo, UnwindSafe};
use std::sync::{Once, OnceLock};
#[cfg(feature = "spantrace")]
use tracing_error::SpanTrace;
use crate::backtrace::Backtrace;
use crate::error::Error;
use crate::location::SourceLocation;
#[cfg(feature = "color")]
use crate::render::ColorMode;
use crate::report::Report;
use crate::value::Value;
struct PanicCapture {
location: Option<SourceLocation>,
backtrace: Backtrace,
#[cfg(feature = "spantrace")]
spantrace: SpanTrace,
message: String,
}
thread_local! {
static LAST_PANIC: RefCell<Option<PanicCapture>> = const { RefCell::new(None) };
}
struct RenderedPanic(String);
#[track_caller]
pub(crate) fn panic_rendered(text: String) -> ! {
ensure_hook_installed();
std::panic::panic_any(RenderedPanic(text))
}
struct UncaughtPanic;
impl Debug for UncaughtPanic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UncaughtPanic").finish()
}
}
impl Error for UncaughtPanic {
fn message(&self) -> Option<&dyn Display> {
None
}
fn type_name(&self) -> &'static str {
"panic"
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct PrettyPanicOptions {
report_to: Option<String>,
fields: Vec<(String, Value)>,
}
impl PrettyPanicOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_report_to(mut self, report_to: impl Into<String>) -> Self {
self.report_to = Some(report_to.into());
self
}
#[must_use]
pub fn with_field(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.fields.push((key.into(), value.into()));
self
}
}
fn render_uncaught_panic(
info: &PanicHookInfo<'_>,
message: &str,
options: &PrettyPanicOptions,
) -> String {
let location = info
.location()
.map_or_else(SourceLocation::caller, SourceLocation::from_std);
let report = Report::from_capture(
UncaughtPanic,
location,
Backtrace::force_capture(),
#[cfg(feature = "spantrace")]
SpanTrace::capture(),
)
.message(message.to_owned());
append_pretty_panic_footer(format!("{report:?}"), options)
}
pub(crate) fn append_pretty_panic_footer(
mut rendered: String,
options: &PrettyPanicOptions,
) -> String {
use std::fmt::Write as _;
let _ = write!(rendered, "\n\n━━━━\n\n");
for (key, value) in &options.fields {
let _ = writeln!(rendered, "{key}: {value}");
}
let suggestion = match &options.report_to {
Some(report_to) => {
format!("this indicates a bug in the program; please report it at {report_to}")
}
None => "this indicates a bug in the program".to_owned(),
};
rendered.push_str(&style_footer_suggestion(&suggestion));
rendered
}
#[cfg(feature = "color")]
fn style_footer_suggestion(text: &str) -> String {
crate::render::styled(console::style(text).cyan(), ColorMode::AutoStderr).to_string()
}
#[cfg(not(feature = "color"))]
fn style_footer_suggestion(text: &str) -> String {
text.to_owned()
}
pub(crate) fn decorate_with_pretty_panic_options(rendered: String) -> String {
match PRETTY_PRINT.get() {
Some(options) => append_pretty_panic_footer(rendered, options),
None => rendered,
}
}
static PRETTY_PRINT: OnceLock<PrettyPanicOptions> = OnceLock::new();
fn ensure_hook_installed() {
static INSTALLED: Once = Once::new();
INSTALLED.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| {
let rendered = info.payload().downcast_ref::<RenderedPanic>();
let message = rendered.map_or_else(
|| {
info.payload_as_str()
.map_or_else(|| "thread panicked".to_owned(), str::to_owned)
},
|rendered| rendered.0.clone(),
);
#[allow(clippy::manual_map)]
let pretty = if rendered.is_none() {
match PRETTY_PRINT.get() {
Some(options) => Some(render_uncaught_panic(info, &message, options)),
None => None,
}
} else {
None
};
let capture = PanicCapture {
location: info.location().map(SourceLocation::from_std),
backtrace: Backtrace::force_capture(),
#[cfg(feature = "spantrace")]
spantrace: SpanTrace::capture(),
message,
};
LAST_PANIC.with(|cell| *cell.borrow_mut() = Some(capture));
match (rendered, pretty) {
(Some(rendered), _) => eprintln!("{}", rendered.0),
(None, Some(text)) => eprintln!("{text}"),
(None, None) => previous(info),
}
}));
});
}
pub struct Panicked {
payload: Box<dyn Any + Send>,
}
impl Panicked {
#[must_use]
pub fn payload(&self) -> &(dyn Any + Send) {
&*self.payload
}
pub fn resume_unwind(self) -> ! {
std::panic::resume_unwind(self.payload)
}
}
impl Debug for Panicked {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Panicked").finish_non_exhaustive()
}
}
impl Error for Panicked {
fn message(&self) -> Option<&dyn Display> {
None
}
}
pub fn install_panic_hook() {
ensure_hook_installed();
}
pub fn install_pretty_panic_hook() {
install_pretty_panic_hook_with(PrettyPanicOptions::default());
}
pub fn install_pretty_panic_hook_with(options: PrettyPanicOptions) {
ensure_hook_installed();
let _ = PRETTY_PRINT.set(options);
}
pub fn catch_unwind<F, T>(f: F) -> Result<T, Report<Panicked>>
where
F: FnOnce() -> T + UnwindSafe,
{
ensure_hook_installed();
match std::panic::catch_unwind(f) {
Ok(value) => Ok(value),
Err(payload) => {
let capture = LAST_PANIC.with(|cell| cell.borrow_mut().take());
let message = capture.as_ref().map_or_else(
|| "thread panicked".to_owned(),
|capture| capture.message.clone(),
);
let report = match capture {
Some(capture) => Report::from_capture(
Panicked { payload },
capture.location.unwrap_or_else(SourceLocation::caller),
capture.backtrace,
#[cfg(feature = "spantrace")]
capture.spantrace,
),
None => Report::new(Panicked { payload }),
};
Err(report.message(message))
}
}
}