use log::error;
use std::io::Write;
use std::panic::{self, PanicHookInfo};
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(test)]
use std::sync::Arc;
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 Default for TuiGuard {
fn default() -> Self {
Self::new()
}
}
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 Default for TestPanicHook {
fn default() -> Self {
Self::new()
}
}
#[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| {
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()
};
if msg.starts_with("Test panic message for thread") {
panicked.store(true, Ordering::SeqCst);
*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 proptest::prelude::*;
use std::panic;
use std::sync::Mutex;
static PANIC_TEST_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_panic_hook_captures_panic() {
let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let original_hook = panic::take_hook();
let hook = TestPanicHook::new();
hook.install();
let test_id = std::thread::current().id();
let panic_msg = format!("Test panic message for thread {:?}", test_id);
let expected_msg = panic_msg.clone();
let panic_result = panic::catch_unwind(move || {
panic!("{}", panic_msg);
});
assert!(panic_result.is_err(), "Expected panic to occur");
let captured = hook.get_panic_message();
panic::set_hook(original_hook);
assert!(hook.did_panic(), "Hook should have detected panic");
assert_eq!(
captured,
Some(expected_msg),
"Hook should have captured our specific message"
);
}
#[test]
fn test_manual_tui_state_behavior() {
let initial_state = TUI_ACTIVE.load(Ordering::SeqCst);
set_tui_active(true);
assert!(TUI_ACTIVE.load(Ordering::SeqCst));
set_tui_active(false);
assert!(!TUI_ACTIVE.load(Ordering::SeqCst));
set_tui_active(initial_state);
}
#[test]
fn test_panic_hook_installation_behavior() {
let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let original_hook = panic::take_hook();
install();
let result = std::panic::catch_unwind(|| {
let x = 1 + 1;
assert_eq!(x, 2);
});
assert!(result.is_ok());
panic::set_hook(original_hook);
}
#[test]
fn test_custom_cleanup_behavior() {
let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let original_hook = panic::take_hook();
let cleanup_called = Arc::new(AtomicBool::new(false));
let cleanup_called_clone = Arc::clone(&cleanup_called);
install_with_cleanup(move || {
cleanup_called_clone.store(true, Ordering::SeqCst);
});
let test_id = std::thread::current().id();
let panic_msg = format!("Test cleanup panic for thread {:?}", test_id);
let _ = panic::catch_unwind(move || {
panic!("{}", panic_msg);
});
panic::set_hook(original_hook);
}
proptest! {
#[test]
fn prop_tui_state_atomic(states in prop::collection::vec(prop::bool::ANY, 1..10)) {
for &state in &states {
set_tui_active(state);
prop_assert_eq!(TUI_ACTIVE.load(Ordering::SeqCst), state);
}
}
}
}