use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PanicInfo {
pub message: String,
pub location: Option<String>,
}
#[allow(deprecated)]
type PanicHookFn = dyn Fn(&std::panic::PanicInfo<'_>) + Send + Sync + 'static;
#[must_use = "dropping the guard restores the previous panic hook"]
pub struct PanicGuard {
previous: Arc<PanicHookFn>,
}
pub struct PanicTracker;
impl PanicTracker {
#[allow(deprecated)]
pub fn install() -> PanicGuard {
let previous: Box<PanicHookFn> = std::panic::take_hook();
let previous: Arc<PanicHookFn> = Arc::from(previous);
let for_hook = Arc::clone(&previous);
std::panic::set_hook(Box::new(move |info| {
let span = tracing::error_span!("mytheclipse_panic_task");
let message = payload_to_string(info.payload());
let location = info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()));
let _guard = span.enter();
match location {
Some(loc) => tracing::error!("panic caught: {message} at {loc}"),
None => tracing::error!("panic caught: {message}"),
}
for_hook(info);
}));
PanicGuard { previous }
}
pub fn catch<T>(f: impl FnOnce() -> T) -> Result<T, PanicInfo> {
let wrapped = AssertUnwindSafe(f);
catch_unwind(wrapped).map_err(|payload| PanicInfo {
message: payload_to_string(&*payload),
location: None,
})
}
}
impl Drop for PanicGuard {
fn drop(&mut self) {
let _current = std::panic::take_hook();
let previous = Arc::clone(&self.previous);
std::panic::set_hook(Box::new(move |info| previous(info)));
}
}
fn payload_to_string(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"panic with a non-string payload".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn catch_returns_ok_on_success() {
let result = PanicTracker::catch(|| 1 + 1);
assert_eq!(result.unwrap(), 2);
}
#[test]
fn catch_captures_panic_as_err() {
let result: Result<u32, PanicInfo> = PanicTracker::catch(|| panic!("boom"));
assert!(result.is_err());
assert_eq!(result.unwrap_err().message, "boom");
}
#[test]
fn catch_captures_string_payload() {
let err = PanicTracker::catch(|| panic!("{}", String::from("stringy"))).unwrap_err();
assert_eq!(err.message, "stringy");
}
#[test]
fn hook_is_restored_after_guard_drop() {
let _guard = PanicTracker::install();
drop(_guard);
let _ = PanicTracker::catch(|| panic!("irrelevant"));
}
#[test]
fn hook_runs_without_and_panics_are_still_catchable() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_hook = Arc::clone(&calls);
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
calls_hook.fetch_add(1, Ordering::SeqCst);
let _ = info.location().is_some();
}));
let _guard = PanicTracker::install();
std::thread::spawn(|| panic!("worker boom"))
.join()
.unwrap_err();
drop(_guard);
std::panic::set_hook(prev);
assert!(calls.load(Ordering::SeqCst) >= 1);
}
}