hojicha_runtime/
panic_handler.rs1use log::error;
7use std::io::Write;
8use std::panic::{self, PanicHookInfo};
9use std::sync::atomic::{AtomicBool, Ordering};
10
11static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);
13
14static mut CLEANUP_FN: Option<Box<dyn Fn() + Send + Sync>> = None;
16
17pub fn install() {
31 panic::set_hook(Box::new(|panic_info| {
32 handle_panic(panic_info);
33 }));
34}
35
36pub fn install_with_cleanup<F>(cleanup: F)
57where
58 F: Fn() + Send + Sync + 'static,
59{
60 unsafe {
61 CLEANUP_FN = Some(Box::new(cleanup));
62 }
63 install();
64}
65
66pub fn set_tui_active(active: bool) {
71 TUI_ACTIVE.store(active, Ordering::SeqCst);
72}
73
74pub struct TuiGuard;
76
77impl TuiGuard {
78 pub fn new() -> Self {
80 set_tui_active(true);
81 TuiGuard
82 }
83}
84
85impl Drop for TuiGuard {
86 fn drop(&mut self) {
87 set_tui_active(false);
88 }
89}
90
91fn handle_panic(panic_info: &PanicHookInfo) {
93 error!("PANIC: {}", panic_info);
95
96 unsafe {
98 if let Some(ref cleanup) = CLEANUP_FN {
99 cleanup();
100 }
101 }
102
103 if TUI_ACTIVE.load(Ordering::SeqCst) {
105 restore_terminal();
106 }
107
108 eprintln!("\n\n==================== PANIC ====================");
110 eprintln!("{}", panic_info);
111
112 if let Some(location) = panic_info.location() {
114 eprintln!(
115 "\nLocation: {}:{}:{}",
116 location.file(),
117 location.line(),
118 location.column()
119 );
120 }
121
122 if let Ok(var) = std::env::var("RUST_BACKTRACE") {
124 if var == "1" || var == "full" {
125 eprintln!("\nBacktrace:");
126 eprintln!("{:?}", std::backtrace::Backtrace::capture());
127 }
128 } else {
129 eprintln!("\nNote: Set RUST_BACKTRACE=1 to see a backtrace");
130 }
131
132 eprintln!("================================================\n");
133}
134
135fn restore_terminal() {
137 use crossterm::{
138 cursor,
139 event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture},
140 execute,
141 terminal::{self, LeaveAlternateScreen},
142 };
143
144 let _ = execute!(
146 std::io::stderr(),
147 LeaveAlternateScreen,
148 DisableMouseCapture,
149 DisableBracketedPaste,
150 DisableFocusChange,
151 cursor::Show,
152 );
153
154 let _ = terminal::disable_raw_mode();
156
157 let _ = std::io::stderr().flush();
159}
160
161#[cfg(test)]
163pub struct TestPanicHook {
164 pub panicked: Arc<AtomicBool>,
165 pub panic_message: Arc<std::sync::Mutex<Option<String>>>,
166}
167
168#[cfg(test)]
169impl TestPanicHook {
170 pub fn new() -> Self {
172 Self {
173 panicked: Arc::new(AtomicBool::new(false)),
174 panic_message: Arc::new(std::sync::Mutex::new(None)),
175 }
176 }
177
178 pub fn install(&self) {
180 let panicked = Arc::clone(&self.panicked);
181 let panic_message = Arc::clone(&self.panic_message);
182
183 panic::set_hook(Box::new(move |panic_info| {
184 panicked.store(true, Ordering::SeqCst);
185
186 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
187 s.to_string()
188 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
189 s.clone()
190 } else {
191 "Unknown panic".to_string()
192 };
193
194 *panic_message.lock().unwrap() = Some(msg);
195 }));
196 }
197
198 pub fn did_panic(&self) -> bool {
200 self.panicked.load(Ordering::SeqCst)
201 }
202
203 pub fn get_panic_message(&self) -> Option<String> {
205 self.panic_message.lock().unwrap().clone()
206 }
207
208 pub fn reset(&self) {
210 self.panicked.store(false, Ordering::SeqCst);
211 *self.panic_message.lock().unwrap() = None;
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::panic;
219
220 #[test]
221 fn test_panic_hook_captures_panic() {
222 let hook = TestPanicHook::new();
223 hook.install();
224
225 let result = panic::catch_unwind(|| {
226 panic!("Test panic message");
227 });
228
229 assert!(result.is_err());
230 assert!(hook.did_panic());
231 assert_eq!(
232 hook.get_panic_message(),
233 Some("Test panic message".to_string())
234 );
235
236 let _ = panic::take_hook();
238 }
239}