hojicha_runtime/
panic_handler.rs1use log::error;
7use std::io::Write;
8use std::panic::{self, PanicHookInfo};
9use std::sync::atomic::{AtomicBool, Ordering};
10#[cfg(test)]
11use std::sync::Arc;
12
13static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);
15
16static mut CLEANUP_FN: Option<Box<dyn Fn() + Send + Sync>> = None;
18
19pub 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)
55where
56 F: Fn() + Send + Sync + 'static,
57{
58 unsafe {
59 CLEANUP_FN = Some(Box::new(cleanup));
60 }
61 install();
62}
63
64pub fn set_tui_active(active: bool) {
69 TUI_ACTIVE.store(active, Ordering::SeqCst);
70}
71
72pub struct TuiGuard;
74
75impl Default for TuiGuard {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl TuiGuard {
82 pub fn new() -> Self {
84 set_tui_active(true);
85 TuiGuard
86 }
87}
88
89impl Drop for TuiGuard {
90 fn drop(&mut self) {
91 set_tui_active(false);
92 }
93}
94
95fn handle_panic(panic_info: &PanicHookInfo) {
97 error!("PANIC: {}", panic_info);
99
100 unsafe {
102 if let Some(ref cleanup) = CLEANUP_FN {
103 cleanup();
104 }
105 }
106
107 if TUI_ACTIVE.load(Ordering::SeqCst) {
109 restore_terminal();
110 }
111
112 eprintln!("\n\n==================== PANIC ====================");
114 eprintln!("{}", panic_info);
115
116 if let Some(location) = panic_info.location() {
118 eprintln!(
119 "\nLocation: {}:{}:{}",
120 location.file(),
121 location.line(),
122 location.column()
123 );
124 }
125
126 if let Ok(var) = std::env::var("RUST_BACKTRACE") {
128 if var == "1" || var == "full" {
129 eprintln!("\nBacktrace:");
130 eprintln!("{:?}", std::backtrace::Backtrace::capture());
131 }
132 } else {
133 eprintln!("\nNote: Set RUST_BACKTRACE=1 to see a backtrace");
134 }
135
136 eprintln!("================================================\n");
137}
138
139fn restore_terminal() {
141 use crossterm::{
142 cursor,
143 event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture},
144 execute,
145 terminal::{self, LeaveAlternateScreen},
146 };
147
148 let _ = execute!(
150 std::io::stderr(),
151 LeaveAlternateScreen,
152 DisableMouseCapture,
153 DisableBracketedPaste,
154 DisableFocusChange,
155 cursor::Show,
156 );
157
158 let _ = terminal::disable_raw_mode();
160
161 let _ = std::io::stderr().flush();
163}
164
165#[cfg(test)]
167pub struct TestPanicHook {
168 pub panicked: Arc<AtomicBool>,
170 pub panic_message: Arc<std::sync::Mutex<Option<String>>>,
172}
173
174#[cfg(test)]
175impl Default for TestPanicHook {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181#[cfg(test)]
182impl TestPanicHook {
183 pub fn new() -> Self {
185 Self {
186 panicked: Arc::new(AtomicBool::new(false)),
187 panic_message: Arc::new(std::sync::Mutex::new(None)),
188 }
189 }
190
191 pub fn install(&self) {
193 let panicked = Arc::clone(&self.panicked);
194 let panic_message = Arc::clone(&self.panic_message);
195
196 panic::set_hook(Box::new(move |panic_info| {
197 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
198 s.to_string()
199 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
200 s.clone()
201 } else {
202 "Unknown panic".to_string()
203 };
204
205 if msg.starts_with("Test panic message for thread") {
207 panicked.store(true, Ordering::SeqCst);
208 *panic_message.lock().unwrap() = Some(msg);
209 }
210 }));
211 }
212
213 pub fn did_panic(&self) -> bool {
215 self.panicked.load(Ordering::SeqCst)
216 }
217
218 pub fn get_panic_message(&self) -> Option<String> {
220 self.panic_message.lock().unwrap().clone()
221 }
222
223 pub fn reset(&self) {
225 self.panicked.store(false, Ordering::SeqCst);
226 *self.panic_message.lock().unwrap() = None;
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use proptest::prelude::*;
234 use std::panic;
235 use std::sync::Mutex;
236
237 static PANIC_TEST_MUTEX: Mutex<()> = Mutex::new(());
239
240 #[test]
241 fn test_panic_hook_captures_panic() {
242 let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
244
245 let original_hook = panic::take_hook();
247
248 let hook = TestPanicHook::new();
250 hook.install();
251
252 let test_id = std::thread::current().id();
254 let panic_msg = format!("Test panic message for thread {:?}", test_id);
255 let expected_msg = panic_msg.clone();
256
257 let panic_result = panic::catch_unwind(move || {
259 panic!("{}", panic_msg);
260 });
261
262 assert!(panic_result.is_err(), "Expected panic to occur");
264
265 let captured = hook.get_panic_message();
267
268 panic::set_hook(original_hook);
270
271 assert!(hook.did_panic(), "Hook should have detected panic");
273 assert_eq!(
274 captured,
275 Some(expected_msg),
276 "Hook should have captured our specific message"
277 );
278 }
279
280 #[test]
282 fn test_manual_tui_state_behavior() {
283 let initial_state = TUI_ACTIVE.load(Ordering::SeqCst);
285
286 set_tui_active(true);
287 assert!(TUI_ACTIVE.load(Ordering::SeqCst));
288
289 set_tui_active(false);
290 assert!(!TUI_ACTIVE.load(Ordering::SeqCst));
291
292 set_tui_active(initial_state);
294 }
295
296 #[test]
298 fn test_panic_hook_installation_behavior() {
299 let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
300
301 let original_hook = panic::take_hook();
303
304 install();
306
307 let result = std::panic::catch_unwind(|| {
309 let x = 1 + 1;
311 assert_eq!(x, 2);
312 });
313
314 assert!(result.is_ok());
315
316 panic::set_hook(original_hook);
318 }
319
320 #[test]
322 fn test_custom_cleanup_behavior() {
323 let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
324 let original_hook = panic::take_hook();
325
326 let cleanup_called = Arc::new(AtomicBool::new(false));
327 let cleanup_called_clone = Arc::clone(&cleanup_called);
328
329 install_with_cleanup(move || {
331 cleanup_called_clone.store(true, Ordering::SeqCst);
332 });
333
334 let test_id = std::thread::current().id();
335 let panic_msg = format!("Test cleanup panic for thread {:?}", test_id);
336
337 let _ = panic::catch_unwind(move || {
339 panic!("{}", panic_msg);
340 });
341
342 panic::set_hook(original_hook);
347
348 }
350
351 proptest! {
352 #[test]
353 fn prop_tui_state_atomic(states in prop::collection::vec(prop::bool::ANY, 1..10)) {
354 for &state in &states {
355 set_tui_active(state);
356 prop_assert_eq!(TUI_ACTIVE.load(Ordering::SeqCst), state);
357 }
358 }
359 }
360}