Skip to main content

reratui_panic/
lib.rs

1use std::any::Any;
2use std::io::{self, Write};
3use std::panic;
4use std::sync::Once;
5use tokio::task::JoinHandle;
6use tracing::{error, info};
7use tracing_appender::non_blocking::WorkerGuard;
8use tracing_subscriber::Registry;
9use tracing_subscriber::{
10    filter::{EnvFilter, LevelFilter},
11    fmt,
12    prelude::*,
13    util::SubscriberInitExt,
14};
15
16#[cfg(debug_assertions)]
17use better_panic::{Settings, Verbosity};
18
19#[cfg(not(debug_assertions))]
20use human_panic::setup_panic;
21
22static INIT: Once = Once::new();
23static mut LOG_GUARD: Option<WorkerGuard> = None;
24
25/// Sets up a custom panic hook for the application with advanced features.
26///
27/// This function configures panic behavior based on the build profile:
28/// - **Debug builds**: Uses `better_panic` for verbose, immediate, and diagnostic-rich panics with full stack traces.
29/// - **Release builds**: Uses `human_panic` for graceful, user-friendly panics that log internally without exposing sensitive details, prioritizing user experience.
30///
31/// Additionally, it provides a mechanism to catch panics from spawned Tokio tasks.
32///
33/// This function should be called only once. Subsequent calls will be ignored.
34pub fn setup_panic_handler() {
35    INIT.call_once(|| {
36        // Initialize tracing subscriber for internal logging regardless of build type
37        let env_filter = EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into());
38        let console_layer = fmt::Layer::new().with_writer(io::stderr);
39
40        let subscriber = Registry::default().with(env_filter).with(console_layer);
41
42        // For file logging, we can still use tracing-appender
43        // This part is independent of debug/release panic behavior
44        let log_file_path = "logs".to_string(); // Example path, could be configurable
45        let file_appender = tracing_appender::rolling::daily(log_file_path, "application.log");
46        let (non_blocking_appender, guard) = tracing_appender::non_blocking(file_appender);
47        unsafe {
48            LOG_GUARD = Some(guard);
49        }
50        let file_layer = fmt::Layer::new().with_writer(non_blocking_appender).json();
51        subscriber.with(file_layer).init();
52
53        #[cfg(debug_assertions)]
54        {
55            // For debug builds, use better_panic for detailed output
56            Settings::auto()
57                .most_recent_first(false)
58                .lineno_suffix(true)
59                .verbosity(Verbosity::Full)
60                .install();
61            info!("Panic handler configured for DEBUG (better_panic).");
62        }
63
64        #[cfg(not(debug_assertions))]
65        {
66            // For release builds, use human_panic for user-friendly messages
67            setup_panic!();
68            info!("Panic handler configured for RELEASE (human_panic).");
69        }
70
71        // Custom panic hook to log to tracing system before the specific handler takes over
72        let original_hook = panic::take_hook();
73        panic::set_hook(Box::new(move |panic_info| {
74            // First, try to restore the terminal to normal mode
75            // This ensures panic messages are visible
76            use crossterm::execute;
77            use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
78            let _ = disable_raw_mode();
79            let _ = execute!(io::stdout(), LeaveAlternateScreen);
80            let _ = io::stdout().flush();
81
82            // Capture backtrace
83            let backtrace = std::backtrace::Backtrace::force_capture();
84            let backtrace_str = format!("{}", backtrace);
85
86            // Get panic location and payload
87            let location = panic_info.location().map_or("Unknown".to_string(), |l| {
88                format!("{}:{}:{}", l.file(), l.line(), l.column())
89            });
90
91            let payload = panic_info
92                .payload()
93                .downcast_ref::<&str>()
94                .map(|s| s.to_string())
95                .or_else(|| panic_info.payload().downcast_ref::<String>().cloned())
96                .unwrap_or_else(|| "<unknown>".to_string());
97
98            // Log with backtrace
99            error!(
100                target: "panic_handler",
101                location = %location,
102                payload = %payload,
103                backtrace = %backtrace_str,
104                "Application panicked"
105            );
106
107            // Call the original hook to ensure better_panic/human_panic are triggered
108            original_hook(panic_info);
109            let _ = io::stderr().flush();
110        }));
111    });
112}
113
114/// Spawns a new asynchronous task and catches any panics that occur within it.
115///
116/// If a panic occurs, it will be caught by the custom panic hook.
117pub fn spawn_catch_panic<F>(future: F) -> JoinHandle<F::Output>
118where
119    F: std::future::Future + Send + 'static,
120    F::Output: Send + 'static,
121{
122    tokio::spawn(async move {
123        let result = panic::catch_unwind(std::panic::AssertUnwindSafe(|| future));
124        match result {
125            Ok(output_future) => output_future.await,
126            Err(e) => {
127                // Re-panic on the main thread to trigger the custom panic hook
128                panic::resume_unwind(e);
129            }
130        }
131    })
132}
133
134/// Executes a closure and catches any panics that occur, returning a Result.
135///
136/// # Example
137/// ```
138/// use reratui_panic::catch_panic;
139///
140/// let ok = catch_panic(|| 42);
141/// assert!(ok.is_ok());
142/// assert_eq!(ok.unwrap(), 42);
143///
144/// let err = catch_panic(|| panic!("fail!"));
145/// assert!(err.is_err());
146/// ```
147pub fn catch_panic<T, F>(f: F) -> Result<T, Box<dyn Any + Send + 'static>>
148where
149    F: FnOnce() -> T + std::panic::UnwindSafe,
150{
151    std::panic::catch_unwind(f)
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use std::sync::{Arc, Mutex};
158    use std::thread;
159    use std::time::Duration;
160    use tokio::time::timeout;
161
162    #[test]
163    fn test_catch_panic_success() {
164        let result = catch_panic(|| 42);
165        assert!(result.is_ok());
166        assert_eq!(result.unwrap(), 42);
167    }
168
169    #[test]
170    fn test_catch_panic_with_panic() {
171        let result = catch_panic(|| panic!("test panic"));
172        assert!(result.is_err());
173    }
174
175    #[test]
176    fn test_catch_panic_with_string_panic() {
177        let result = catch_panic(|| panic!("string panic message"));
178        assert!(result.is_err());
179
180        // Verify we can downcast the panic payload
181        let panic_payload = result.unwrap_err();
182        let panic_str = panic_payload.downcast_ref::<&str>();
183        assert!(panic_str.is_some());
184        assert_eq!(*panic_str.unwrap(), "string panic message");
185    }
186
187    #[test]
188    fn test_catch_panic_with_custom_type() {
189        #[derive(Debug, PartialEq)]
190        struct CustomError(i32);
191
192        let result = catch_panic(|| {
193            std::panic::panic_any(CustomError(123));
194        });
195
196        assert!(result.is_err());
197        let panic_payload = result.unwrap_err();
198        let custom_error = panic_payload.downcast_ref::<CustomError>();
199        assert!(custom_error.is_some());
200        assert_eq!(*custom_error.unwrap(), CustomError(123));
201    }
202
203    #[test]
204    fn test_catch_panic_with_closure_capture() {
205        let value = 100;
206        let result = catch_panic(|| value * 2);
207        assert!(result.is_ok());
208        assert_eq!(result.unwrap(), 200);
209    }
210
211    #[tokio::test]
212    async fn test_spawn_catch_panic_success() {
213        let handle = spawn_catch_panic(async { 42 });
214        let result = handle.await;
215        assert!(result.is_ok());
216        assert_eq!(result.unwrap(), 42);
217    }
218
219    #[tokio::test]
220    async fn test_spawn_catch_panic_with_async_work() {
221        let handle = spawn_catch_panic(async {
222            tokio::time::sleep(Duration::from_millis(10)).await;
223            "async result"
224        });
225
226        let result = timeout(Duration::from_secs(1), handle).await;
227        assert!(result.is_ok());
228        let join_result = result.unwrap();
229        assert!(join_result.is_ok());
230        assert_eq!(join_result.unwrap(), "async result");
231    }
232
233    #[tokio::test]
234    async fn test_spawn_catch_panic_with_panic() {
235        let handle = spawn_catch_panic(async {
236            panic!("async panic");
237        });
238
239        // The task should complete but the panic should be caught
240        let result = handle.await;
241        // Since we resume_unwind, the task will actually panic
242        // This tests that the panic handling mechanism works
243        assert!(result.is_err());
244    }
245
246    #[test]
247    fn test_setup_panic_handler_idempotent() {
248        // Test that calling setup_panic_handler multiple times is safe
249        setup_panic_handler();
250        setup_panic_handler();
251        setup_panic_handler();
252
253        // If we get here without panicking, the test passes
254    }
255
256    #[test]
257    fn test_setup_panic_handler_thread_safety() {
258        let handles: Vec<_> = (0..10)
259            .map(|_| {
260                thread::spawn(|| {
261                    setup_panic_handler();
262                })
263            })
264            .collect();
265
266        for handle in handles {
267            handle.join().unwrap();
268        }
269
270        // If all threads complete successfully, the test passes
271    }
272
273    #[test]
274    fn test_catch_panic_return_types() {
275        // Test with different return types
276        let string_result = catch_panic(|| "hello".to_string());
277        assert!(string_result.is_ok());
278        assert_eq!(string_result.unwrap(), "hello");
279
280        let vec_result = catch_panic(|| vec![1, 2, 3]);
281        assert!(vec_result.is_ok());
282        assert_eq!(vec_result.unwrap(), vec![1, 2, 3]);
283
284        let option_result = catch_panic(|| Some(42));
285        assert!(option_result.is_ok());
286        assert_eq!(option_result.unwrap(), Some(42));
287    }
288
289    #[tokio::test]
290    async fn test_spawn_catch_panic_concurrent() {
291        let handles: Vec<_> = (0..5)
292            .map(|i| {
293                spawn_catch_panic(async move {
294                    tokio::time::sleep(Duration::from_millis(10)).await;
295                    i * 2
296                })
297            })
298            .collect();
299
300        let mut results = Vec::new();
301        for handle in handles {
302            let result = handle.await;
303            assert!(result.is_ok());
304            results.push(result.unwrap());
305        }
306
307        results.sort();
308        assert_eq!(results, vec![0, 2, 4, 6, 8]);
309    }
310
311    #[test]
312    fn test_catch_panic_with_mutable_data() {
313        let mut counter = 0;
314        let result = catch_panic(std::panic::AssertUnwindSafe(|| {
315            counter += 1;
316            counter
317        }));
318
319        assert!(result.is_ok());
320        assert_eq!(result.unwrap(), 1);
321    }
322
323    #[tokio::test]
324    async fn test_spawn_catch_panic_with_shared_state() {
325        let counter = Arc::new(Mutex::new(0));
326        let counter_clone = counter.clone();
327
328        let handle = spawn_catch_panic(async move {
329            let mut count = counter_clone.lock().unwrap();
330            *count += 1;
331            *count
332        });
333
334        let result = handle.await;
335        assert!(result.is_ok());
336        assert_eq!(result.unwrap(), 1);
337
338        let final_count = *counter.lock().unwrap();
339        assert_eq!(final_count, 1);
340    }
341
342    #[test]
343    fn test_panic_handler_module_exports() {
344        // Test that all public functions are accessible by calling them
345        setup_panic_handler();
346
347        let result = catch_panic(|| 42);
348        assert!(result.is_ok());
349
350        // Test spawn_catch_panic in an async context would require tokio runtime
351        // So we just verify the function exists by referencing it
352        let _spawn_fn_exists = spawn_catch_panic::<std::future::Ready<i32>>;
353
354        // If compilation succeeds, all exports are accessible
355    }
356}