use std::backtrace::Backtrace;
use std::panic;
use std::sync::{Once, OnceLock};
use crate::shared::ENV_CONTAINER_TYPE;
static PANIC_INFO: OnceLock<Option<CapturedPanicInfo>> = OnceLock::new();
static INIT_PANIC_HOOK: Once = Once::new();
#[derive(Clone, Debug)]
pub(crate) struct CapturedPanicInfo {
pub(crate) message: String,
pub(crate) location: Option<String>,
pub(crate) backtrace: String,
}
pub(crate) fn init_panic_hook() {
INIT_PANIC_HOOK.call_once(|| {
panic::set_hook(Box::new(|panic_info| {
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
let location = panic_info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));
let backtrace = Backtrace::force_capture();
let info = CapturedPanicInfo {
message,
location,
backtrace: backtrace.to_string(),
};
let _ = PANIC_INFO.set(Some(info));
}));
});
}
pub(crate) fn get_panic_info() -> Option<CapturedPanicInfo> {
PANIC_INFO.get().and_then(|guard| guard.as_ref().cloned())
}
fn format_panic_message(panic_info: &CapturedPanicInfo) -> String {
match &panic_info.location {
Some(location) => format!("{} at {}", panic_info.message, location),
None => panic_info.message.clone(),
}
}
pub(crate) fn build_panic_status(panic_info: &CapturedPanicInfo) -> tonic::Status {
use std::env;
use tonic_types::{ErrorDetails, StatusExt};
let panic_message = format_panic_message(panic_info);
let status_msg = format!(
"UDF_EXECUTION_ERROR({}): {}",
env::var(ENV_CONTAINER_TYPE).unwrap_or_default(),
panic_message
);
let details = ErrorDetails::with_debug_info(vec![], panic_info.backtrace.clone());
tonic::Status::with_error_details(tonic::Code::Internal, status_msg, details)
}
#[cfg(all(test, feature = "test-panic"))]
mod tests {
use super::*;
fn ensure_panic_hook_initialized() {
init_panic_hook();
}
fn clear_panic_info_for_test() {
let _ = PANIC_INFO.set(None);
}
#[test]
fn test_panic_hook_functionality() {
ensure_panic_hook_initialized();
clear_panic_info_for_test();
assert!(
get_panic_info().is_none(),
"Panic info should be cleared initially"
);
let result = std::panic::catch_unwind(|| {
panic!("Test panic message");
});
assert!(result.is_err(), "catch_unwind should capture the panic");
std::thread::sleep(std::time::Duration::from_millis(10));
let panic_info = get_panic_info();
assert!(
panic_info.is_some(),
"Panic info should be captured by the hook"
);
let info = panic_info.unwrap();
assert_eq!(info.message, "Test panic message");
assert!(info.location.is_some(), "Panic location should be captured");
assert!(!info.backtrace.is_empty(), "Backtrace should not be empty");
let formatted = format_panic_message(&info);
assert!(formatted.contains("Test panic message"));
assert!(formatted.contains("panic.rs"));
let second_call = get_panic_info();
assert!(second_call.is_some(), "Panic info should persist");
assert_eq!(second_call.unwrap().message, "Test panic message");
clear_panic_info_for_test();
}
}