use std::any::Any;
use std::io::{self, Write};
use std::panic;
use std::sync::Once;
use tokio::task::JoinHandle;
use tracing::{error, info};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::Registry;
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
fmt,
prelude::*,
util::SubscriberInitExt,
};
#[cfg(debug_assertions)]
use better_panic::{Settings, Verbosity};
#[cfg(not(debug_assertions))]
use human_panic::setup_panic;
static INIT: Once = Once::new();
static mut LOG_GUARD: Option<WorkerGuard> = None;
pub fn setup_panic_handler() {
INIT.call_once(|| {
let env_filter = EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into());
let console_layer = fmt::Layer::new().with_writer(io::stderr);
let subscriber = Registry::default().with(env_filter).with(console_layer);
let log_file_path = "logs".to_string(); let file_appender = tracing_appender::rolling::daily(log_file_path, "application.log");
let (non_blocking_appender, guard) = tracing_appender::non_blocking(file_appender);
unsafe {
LOG_GUARD = Some(guard);
}
let file_layer = fmt::Layer::new().with_writer(non_blocking_appender).json();
subscriber.with(file_layer).init();
#[cfg(debug_assertions)]
{
Settings::auto()
.most_recent_first(false)
.lineno_suffix(true)
.verbosity(Verbosity::Full)
.install();
info!("Panic handler configured for DEBUG (better_panic).");
}
#[cfg(not(debug_assertions))]
{
setup_panic!();
info!("Panic handler configured for RELEASE (human_panic).");
}
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
use crossterm::execute;
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
let _ = io::stdout().flush();
let backtrace = std::backtrace::Backtrace::force_capture();
let backtrace_str = format!("{}", backtrace);
let location = panic_info.location().map_or("Unknown".to_string(), |l| {
format!("{}:{}:{}", l.file(), l.line(), l.column())
});
let payload = panic_info
.payload()
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| panic_info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "<unknown>".to_string());
error!(
target: "panic_handler",
location = %location,
payload = %payload,
backtrace = %backtrace_str,
"Application panicked"
);
original_hook(panic_info);
let _ = io::stderr().flush();
}));
});
}
pub fn spawn_catch_panic<F>(future: F) -> JoinHandle<F::Output>
where
F: std::future::Future + Send + 'static,
F::Output: Send + 'static,
{
tokio::spawn(async move {
let result = panic::catch_unwind(std::panic::AssertUnwindSafe(|| future));
match result {
Ok(output_future) => output_future.await,
Err(e) => {
panic::resume_unwind(e);
}
}
})
}
pub fn catch_panic<T, F>(f: F) -> Result<T, Box<dyn Any + Send + 'static>>
where
F: FnOnce() -> T + std::panic::UnwindSafe,
{
std::panic::catch_unwind(f)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use tokio::time::timeout;
#[test]
fn test_catch_panic_success() {
let result = catch_panic(|| 42);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_catch_panic_with_panic() {
let result = catch_panic(|| panic!("test panic"));
assert!(result.is_err());
}
#[test]
fn test_catch_panic_with_string_panic() {
let result = catch_panic(|| panic!("string panic message"));
assert!(result.is_err());
let panic_payload = result.unwrap_err();
let panic_str = panic_payload.downcast_ref::<&str>();
assert!(panic_str.is_some());
assert_eq!(*panic_str.unwrap(), "string panic message");
}
#[test]
fn test_catch_panic_with_custom_type() {
#[derive(Debug, PartialEq)]
struct CustomError(i32);
let result = catch_panic(|| {
std::panic::panic_any(CustomError(123));
});
assert!(result.is_err());
let panic_payload = result.unwrap_err();
let custom_error = panic_payload.downcast_ref::<CustomError>();
assert!(custom_error.is_some());
assert_eq!(*custom_error.unwrap(), CustomError(123));
}
#[test]
fn test_catch_panic_with_closure_capture() {
let value = 100;
let result = catch_panic(|| value * 2);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 200);
}
#[tokio::test]
async fn test_spawn_catch_panic_success() {
let handle = spawn_catch_panic(async { 42 });
let result = handle.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[tokio::test]
async fn test_spawn_catch_panic_with_async_work() {
let handle = spawn_catch_panic(async {
tokio::time::sleep(Duration::from_millis(10)).await;
"async result"
});
let result = timeout(Duration::from_secs(1), handle).await;
assert!(result.is_ok());
let join_result = result.unwrap();
assert!(join_result.is_ok());
assert_eq!(join_result.unwrap(), "async result");
}
#[tokio::test]
async fn test_spawn_catch_panic_with_panic() {
let handle = spawn_catch_panic(async {
panic!("async panic");
});
let result = handle.await;
assert!(result.is_err());
}
#[test]
fn test_setup_panic_handler_idempotent() {
setup_panic_handler();
setup_panic_handler();
setup_panic_handler();
}
#[test]
fn test_setup_panic_handler_thread_safety() {
let handles: Vec<_> = (0..10)
.map(|_| {
thread::spawn(|| {
setup_panic_handler();
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
#[test]
fn test_catch_panic_return_types() {
let string_result = catch_panic(|| "hello".to_string());
assert!(string_result.is_ok());
assert_eq!(string_result.unwrap(), "hello");
let vec_result = catch_panic(|| vec![1, 2, 3]);
assert!(vec_result.is_ok());
assert_eq!(vec_result.unwrap(), vec![1, 2, 3]);
let option_result = catch_panic(|| Some(42));
assert!(option_result.is_ok());
assert_eq!(option_result.unwrap(), Some(42));
}
#[tokio::test]
async fn test_spawn_catch_panic_concurrent() {
let handles: Vec<_> = (0..5)
.map(|i| {
spawn_catch_panic(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
i * 2
})
})
.collect();
let mut results = Vec::new();
for handle in handles {
let result = handle.await;
assert!(result.is_ok());
results.push(result.unwrap());
}
results.sort();
assert_eq!(results, vec![0, 2, 4, 6, 8]);
}
#[test]
fn test_catch_panic_with_mutable_data() {
let mut counter = 0;
let result = catch_panic(std::panic::AssertUnwindSafe(|| {
counter += 1;
counter
}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[tokio::test]
async fn test_spawn_catch_panic_with_shared_state() {
let counter = Arc::new(Mutex::new(0));
let counter_clone = counter.clone();
let handle = spawn_catch_panic(async move {
let mut count = counter_clone.lock().unwrap();
*count += 1;
*count
});
let result = handle.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let final_count = *counter.lock().unwrap();
assert_eq!(final_count, 1);
}
#[test]
fn test_panic_handler_module_exports() {
setup_panic_handler();
let result = catch_panic(|| 42);
assert!(result.is_ok());
let _spawn_fn_exists = spawn_catch_panic::<std::future::Ready<i32>>;
}
}