hojicha_runtime/
panic_recovery.rs

1//! Panic recovery utilities for Model trait methods
2//!
3//! This module provides safe wrappers for Model trait methods that catch
4//! panics and allow the application to continue running.
5
6use hojicha_core::commands;
7use hojicha_core::core::{Cmd, Model};
8use hojicha_core::event::Event;
9use log::error;
10use std::panic::{self, AssertUnwindSafe};
11
12/// Strategy for handling panics in Model methods
13#[derive(Debug, Clone, Copy)]
14pub enum PanicRecoveryStrategy {
15    /// Continue with current state (default)
16    Continue,
17    /// Reset model to initial state
18    Reset,
19    /// Show error screen
20    ShowError,
21    /// Quit the application
22    Quit,
23}
24
25impl Default for PanicRecoveryStrategy {
26    fn default() -> Self {
27        Self::Continue
28    }
29}
30
31/// Safely call Model::init with panic recovery
32pub fn safe_init<M: Model>(model: &mut M, strategy: PanicRecoveryStrategy) -> Cmd<M::Message> {
33    let result = panic::catch_unwind(AssertUnwindSafe(|| model.init()));
34
35    match result {
36        Ok(cmd) => cmd,
37        Err(panic_info) => {
38            let msg = format_panic_message(&panic_info, "Model::init");
39            error!("Panic in Model::init: {}", msg);
40
41            match strategy {
42                PanicRecoveryStrategy::Continue => Cmd::noop(),
43                PanicRecoveryStrategy::Reset => Cmd::noop(),
44                PanicRecoveryStrategy::ShowError => {
45                    // Could return a command to show error screen
46                    Cmd::noop()
47                }
48                PanicRecoveryStrategy::Quit => commands::quit(),
49            }
50        }
51    }
52}
53
54/// Safely call Model::update with panic recovery
55pub fn safe_update<M: Model>(
56    model: &mut M,
57    event: Event<M::Message>,
58    strategy: PanicRecoveryStrategy,
59) -> Cmd<M::Message> {
60    // We can't clone the event, so we need to move it into the closure
61    // This means we can't log the specific event on panic
62    let result = panic::catch_unwind(AssertUnwindSafe(move || model.update(event)));
63
64    match result {
65        Ok(cmd) => cmd,
66        Err(panic_info) => {
67            let msg = format_panic_message(&panic_info, "Model::update");
68            error!("Panic in Model::update: {}", msg);
69
70            match strategy {
71                PanicRecoveryStrategy::Continue => Cmd::noop(),
72                PanicRecoveryStrategy::Reset => {
73                    // Could trigger a reset command
74                    Cmd::noop()
75                }
76                PanicRecoveryStrategy::ShowError => {
77                    // Could return a command to show error screen
78                    Cmd::noop()
79                }
80                PanicRecoveryStrategy::Quit => commands::quit(),
81            }
82        }
83    }
84}
85
86/// Safely call Model::view with panic recovery
87///
88/// Returns the view string or an error message string, and a bool indicating if the app should quit
89pub fn safe_view<M: Model>(model: &M, strategy: PanicRecoveryStrategy) -> (String, bool) {
90    let result = panic::catch_unwind(AssertUnwindSafe(|| model.view()));
91
92    match result {
93        Ok(view_string) => (view_string, false), // Success, don't quit
94        Err(panic_info) => {
95            let msg = format_panic_message(&panic_info, "Model::view");
96            error!("Panic in Model::view: {}", msg);
97
98            // Create an error view string
99            let error_view = match strategy {
100                PanicRecoveryStrategy::ShowError => {
101                    format!("ERROR: {}\nPress 'q' to quit", msg)
102                }
103                _ => "An error occurred. Press 'q' to quit.".to_string(),
104            };
105
106            // Return error view and whether to quit based on strategy
107            let should_quit = matches!(strategy, PanicRecoveryStrategy::Quit);
108            (error_view, should_quit)
109        }
110    }
111}
112
113/// Format panic information into a readable message
114fn format_panic_message(panic_info: &dyn std::any::Any, context: &str) -> String {
115    if let Some(s) = panic_info.downcast_ref::<String>() {
116        format!("{} panicked: {}", context, s)
117    } else if let Some(s) = panic_info.downcast_ref::<&str>() {
118        format!("{} panicked: {}", context, s)
119    } else {
120        format!("{} panicked with unknown error", context)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    struct PanickyModel {
129        should_panic_init: bool,
130        should_panic_update: bool,
131        should_panic_view: bool,
132    }
133
134    impl Model for PanickyModel {
135        type Message = ();
136
137        fn init(&mut self) -> Cmd<Self::Message> {
138            if self.should_panic_init {
139                panic!("Init panic!");
140            }
141            Cmd::noop()
142        }
143
144        fn update(&mut self, _event: Event<Self::Message>) -> Cmd<Self::Message> {
145            if self.should_panic_update {
146                panic!("Update panic!");
147            }
148            Cmd::noop()
149        }
150
151        fn view(&self) -> String {
152            if self.should_panic_view {
153                panic!("View panic!");
154            }
155            "Test view".to_string()
156        }
157    }
158
159    #[test]
160    fn test_safe_init_catches_panic() {
161        let mut model = PanickyModel {
162            should_panic_init: true,
163            should_panic_update: false,
164            should_panic_view: false,
165        };
166
167        let cmd = safe_init(&mut model, PanicRecoveryStrategy::Continue);
168        assert!(cmd.is_noop());
169    }
170
171    #[test]
172    fn test_safe_update_catches_panic() {
173        let mut model = PanickyModel {
174            should_panic_init: false,
175            should_panic_update: true,
176            should_panic_view: false,
177        };
178
179        let cmd = safe_update(&mut model, Event::Tick, PanicRecoveryStrategy::Continue);
180        assert!(cmd.is_noop());
181    }
182}