1use std::panic::{AssertUnwindSafe, catch_unwind};
23
24use crate::boundary::{PluginError, PluginErrorCode, PluginResult};
25
26pub fn guard<T>(f: impl FnOnce() -> Result<T, PluginError>) -> PluginResult<T> {
31 let result = catch_unwind(AssertUnwindSafe(f));
32 match result {
33 Ok(Ok(t)) => PluginResult::Ok(t),
34 Ok(Err(e)) => PluginResult::Err(e),
35 Err(payload) => {
36 let message = panic_message(payload.as_ref());
37 drop_payload(payload);
38 PluginResult::Err(PluginError::new(PluginErrorCode::Panic, message))
39 }
40 }
41}
42
43pub fn guard_infallible<T>(thunk_name: &str, f: impl FnOnce() -> T) -> T {
51 match catch_unwind(AssertUnwindSafe(f)) {
52 Ok(t) => t,
53 Err(payload) => {
54 let msg = panic_message(payload.as_ref());
55 drop_payload(payload);
56 log::error!(
57 target: "nautilus_plugin",
58 "plug-in panicked in `{thunk_name}` thunk; aborting process: {msg}",
59 );
60 std::process::abort();
61 }
62 }
63}
64
65pub fn drop_payload(payload: Box<dyn std::any::Any + Send>) {
74 let _ = catch_unwind(AssertUnwindSafe(move || drop(payload)));
75}
76
77fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
78 if let Some(s) = payload.downcast_ref::<&'static str>() {
79 (*s).to_string()
80 } else if let Some(s) = payload.downcast_ref::<String>() {
81 s.clone()
82 } else {
83 "plug-in panicked with non-string payload".to_string()
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use std::sync::atomic::{AtomicUsize, Ordering};
90
91 use rstest::rstest;
92
93 use super::*;
94
95 #[rstest]
96 fn returns_ok_on_success() {
97 let r = guard(|| Ok::<u32, PluginError>(7));
98 assert_eq!(r.into_result().unwrap(), 7);
99 }
100
101 #[rstest]
102 fn returns_err_on_returned_error() {
103 let r = guard(|| Err::<u32, _>(PluginError::generic("boom")));
104 let e = r.into_result().unwrap_err();
105 assert_eq!(e.code, PluginErrorCode::Generic);
106 assert_eq!(e.message_string(), "boom");
107 }
108
109 #[rstest]
110 fn returns_err_on_string_panic() {
111 let r = guard(|| -> Result<u32, PluginError> { panic!("oops") });
112 let e = r.into_result().unwrap_err();
113 assert_eq!(e.code, PluginErrorCode::Panic);
114 assert!(e.message_string().contains("oops"));
115 }
116
117 #[rstest]
118 fn returns_err_on_non_string_panic() {
119 let r = guard(|| -> Result<u32, PluginError> {
120 std::panic::panic_any(42_u32);
121 });
122 let e = r.into_result().unwrap_err();
123 assert_eq!(e.code, PluginErrorCode::Panic);
124 assert!(e.message_string().contains("non-string"));
125 }
126
127 #[rstest]
128 fn guard_infallible_returns_inner_on_success() {
129 let v = guard_infallible("test", || 42u64);
130 assert_eq!(v, 42);
131 }
132
133 #[rstest]
134 fn drop_payload_swallows_panicking_drop() {
135 use std::{
140 any::Any,
141 sync::atomic::{AtomicUsize, Ordering},
142 };
143
144 static DROPS_OBSERVED: AtomicUsize = AtomicUsize::new(0);
145 struct Bomb;
146 impl Drop for Bomb {
147 fn drop(&mut self) {
148 DROPS_OBSERVED.fetch_add(1, Ordering::SeqCst);
149 panic!("drop panic");
150 }
151 }
152 DROPS_OBSERVED.store(0, Ordering::SeqCst);
153
154 let payload: Box<dyn Any + Send> = Box::new(Bomb);
155 drop_payload(payload);
156 assert_eq!(DROPS_OBSERVED.load(Ordering::SeqCst), 1);
157 }
158
159 #[rstest]
160 fn guard_survives_panic_any_with_panicking_drop() {
161 static DROPS_OBSERVED: AtomicUsize = AtomicUsize::new(0);
166 struct Bomb;
167 impl Drop for Bomb {
168 fn drop(&mut self) {
169 DROPS_OBSERVED.fetch_add(1, Ordering::SeqCst);
170 panic!("drop panic");
171 }
172 }
173
174 DROPS_OBSERVED.store(0, Ordering::SeqCst);
175 let r = guard(|| -> Result<u32, PluginError> {
176 std::panic::panic_any(Bomb);
177 });
178 let e = r.into_result().unwrap_err();
179 assert_eq!(e.code, PluginErrorCode::Panic);
180
181 assert_eq!(DROPS_OBSERVED.load(Ordering::SeqCst), 1);
183 }
184}