use log::error;
use std::io::Write;
use std::panic::{self, PanicHookInfo};
use std::sync::atomic::{AtomicBool, Ordering};
static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);
static mut CLEANUP_FN: Option<Box<dyn Fn() + Send + Sync>> = None;
pub fn install() {
panic::set_hook(Box::new(|panic_info| {
handle_panic(panic_info);
}));
}
pub fn install_with_cleanup<F>(cleanup: F)
where
F: Fn() + Send + Sync + 'static,
{
unsafe {
CLEANUP_FN = Some(Box::new(cleanup));
}
install();
}
pub fn set_tui_active(active: bool) {
TUI_ACTIVE.store(active, Ordering::SeqCst);
}
pub struct TuiGuard;
impl TuiGuard {
pub fn new() -> Self {
set_tui_active(true);
TuiGuard
}
}
impl Drop for TuiGuard {
fn drop(&mut self) {
set_tui_active(false);
}
}
fn handle_panic(panic_info: &PanicHookInfo) {
error!("PANIC: {}", panic_info);
unsafe {
if let Some(ref cleanup) = CLEANUP_FN {
cleanup();
}
}
if TUI_ACTIVE.load(Ordering::SeqCst) {
restore_terminal();
}
eprintln!("\n\n==================== PANIC ====================");
eprintln!("{}", panic_info);
if let Some(location) = panic_info.location() {
eprintln!(
"\nLocation: {}:{}:{}",
location.file(),
location.line(),
location.column()
);
}
if let Ok(var) = std::env::var("RUST_BACKTRACE") {
if var == "1" || var == "full" {
eprintln!("\nBacktrace:");
eprintln!("{:?}", std::backtrace::Backtrace::capture());
}
} else {
eprintln!("\nNote: Set RUST_BACKTRACE=1 to see a backtrace");
}
eprintln!("================================================\n");
}
fn restore_terminal() {
use crossterm::{
cursor,
event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture},
execute,
terminal::{self, LeaveAlternateScreen},
};
let _ = execute!(
std::io::stderr(),
LeaveAlternateScreen,
DisableMouseCapture,
DisableBracketedPaste,
DisableFocusChange,
cursor::Show,
);
let _ = terminal::disable_raw_mode();
let _ = std::io::stderr().flush();
}
#[cfg(test)]
pub struct TestPanicHook {
pub panicked: Arc<AtomicBool>,
pub panic_message: Arc<std::sync::Mutex<Option<String>>>,
}
#[cfg(test)]
impl TestPanicHook {
pub fn new() -> Self {
Self {
panicked: Arc::new(AtomicBool::new(false)),
panic_message: Arc::new(std::sync::Mutex::new(None)),
}
}
pub fn install(&self) {
let panicked = Arc::clone(&self.panicked);
let panic_message = Arc::clone(&self.panic_message);
panic::set_hook(Box::new(move |panic_info| {
panicked.store(true, Ordering::SeqCst);
let msg = 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()
};
*panic_message.lock().unwrap() = Some(msg);
}));
}
pub fn did_panic(&self) -> bool {
self.panicked.load(Ordering::SeqCst)
}
pub fn get_panic_message(&self) -> Option<String> {
self.panic_message.lock().unwrap().clone()
}
pub fn reset(&self) {
self.panicked.store(false, Ordering::SeqCst);
*self.panic_message.lock().unwrap() = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::panic;
#[test]
fn test_panic_hook_captures_panic() {
let hook = TestPanicHook::new();
hook.install();
let result = panic::catch_unwind(|| {
panic!("Test panic message");
});
assert!(result.is_err());
assert!(hook.did_panic());
assert_eq!(
hook.get_panic_message(),
Some("Test panic message".to_string())
);
let _ = panic::take_hook();
}
}