use std::any::Any;
use std::backtrace::Backtrace;
use std::cell::RefCell;
use std::sync::Once;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct PanicReport {
pub message: String,
pub location: Option<String>,
pub backtrace: String,
}
impl PanicReport {
pub(super) fn message_with_location(&self) -> String {
match &self.location {
Some(location) => format!("{} at {}", self.message, location),
None => self.message.clone(),
}
}
}
thread_local! {
static LAST_PANIC: RefCell<Option<PanicReport>> = const { RefCell::new(None) };
}
static INSTALL: Once = Once::new();
pub(super) fn install() {
INSTALL.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let report = PanicReport {
message: payload_message(info.payload()),
location: info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())),
backtrace: Backtrace::force_capture().to_string(),
};
record(report);
previous(info);
}));
});
}
fn record(report: PanicReport) {
let _ = LAST_PANIC.try_with(|slot| {
if let Ok(mut slot) = slot.try_borrow_mut() {
if slot.is_none() {
*slot = Some(report);
}
}
});
}
pub(super) fn clear() {
let _ = LAST_PANIC.try_with(|slot| {
if let Ok(mut slot) = slot.try_borrow_mut() {
*slot = None;
}
});
}
pub(super) fn take() -> Option<PanicReport> {
LAST_PANIC
.try_with(|slot| slot.try_borrow_mut().ok().and_then(|mut s| s.take()))
.ok()
.flatten()
}
pub(super) fn payload_message(payload: &(dyn Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn payload_message_reads_str_and_string() {
assert_eq!(payload_message(&"boom"), "boom");
assert_eq!(payload_message(&"bang".to_string()), "bang");
assert_eq!(payload_message(&42_u32), "Unknown panic");
}
#[test]
fn hook_records_message_location_and_backtrace() {
install();
clear();
let caught: std::thread::Result<()> = std::panic::catch_unwind(|| panic!("recorded panic"));
assert!(caught.is_err());
let report = take().expect("hook records the panic");
assert_eq!(report.message, "recorded panic");
let location = report.location.clone().expect("location known");
assert!(location.contains("panic_report.rs"));
assert!(!report.backtrace.is_empty());
assert!(
report
.message_with_location()
.starts_with("recorded panic at ")
);
assert!(take().is_none());
}
#[test]
fn nested_record_keeps_first() {
clear();
let first = PanicReport {
message: "first".to_string(),
location: None,
backtrace: String::new(),
};
let second = PanicReport {
message: "second".to_string(),
..first.clone()
};
record(first);
record(second);
assert_eq!(take().map(|r| r.message), Some("first".to_string()));
}
}