hojicha_runtime/
panic_recovery.rs1use hojicha_core::commands;
7use hojicha_core::core::{Cmd, Model};
8use hojicha_core::event::Event;
9use log::error;
10use std::panic::{self, AssertUnwindSafe};
11
12#[derive(Debug, Clone, Copy)]
14pub enum PanicRecoveryStrategy {
15 Continue,
17 Reset,
19 ShowError,
21 Quit,
23}
24
25impl Default for PanicRecoveryStrategy {
26 fn default() -> Self {
27 Self::Continue
28 }
29}
30
31pub 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 Cmd::noop()
47 }
48 PanicRecoveryStrategy::Quit => commands::quit(),
49 }
50 }
51 }
52}
53
54pub fn safe_update<M: Model>(
56 model: &mut M,
57 event: Event<M::Message>,
58 strategy: PanicRecoveryStrategy,
59) -> Cmd<M::Message> {
60 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 Cmd::noop()
75 }
76 PanicRecoveryStrategy::ShowError => {
77 Cmd::noop()
79 }
80 PanicRecoveryStrategy::Quit => commands::quit(),
81 }
82 }
83 }
84}
85
86pub 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), Err(panic_info) => {
95 let msg = format_panic_message(&panic_info, "Model::view");
96 error!("Panic in Model::view: {}", msg);
97
98 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 let should_quit = matches!(strategy, PanicRecoveryStrategy::Quit);
108 (error_view, should_quit)
109 }
110 }
111}
112
113fn 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}