use std::panic::{AssertUnwindSafe, catch_unwind};
use crate::boundary::{PluginError, PluginErrorCode, PluginResult};
pub fn guard<T>(f: impl FnOnce() -> Result<T, PluginError>) -> PluginResult<T> {
let result = catch_unwind(AssertUnwindSafe(f));
match result {
Ok(Ok(t)) => PluginResult::Ok(t),
Ok(Err(e)) => PluginResult::Err(e),
Err(payload) => {
let message = panic_message(payload.as_ref());
drop_payload(payload);
PluginResult::Err(PluginError::new(PluginErrorCode::Panic, message))
}
}
}
pub fn guard_infallible<T>(thunk_name: &str, f: impl FnOnce() -> T) -> T {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(t) => t,
Err(payload) => {
let msg = panic_message(payload.as_ref());
drop_payload(payload);
log::error!(
target: "nautilus_plugin",
"plug-in panicked in `{thunk_name}` thunk; aborting process: {msg}",
);
std::process::abort();
}
}
}
pub fn guard_or_null<T>(thunk_name: &str, f: impl FnOnce() -> *mut T) -> *mut T {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(ptr) => ptr,
Err(payload) => {
let msg = panic_message(payload.as_ref());
drop_payload(payload);
catch_unwind(AssertUnwindSafe(|| {
log::error!(
target: "nautilus_plugin",
"plug-in panicked in `{thunk_name}` thunk; returning null: {msg}",
);
}))
.unwrap_or_else(drop_payload);
std::ptr::null_mut()
}
}
}
pub fn guard_drop(thunk_name: &str, f: impl FnOnce()) {
if let Err(payload) = catch_unwind(AssertUnwindSafe(f)) {
let msg = panic_message(payload.as_ref());
drop_payload(payload);
catch_unwind(AssertUnwindSafe(|| {
log::error!(
target: "nautilus_plugin",
"plug-in panicked in `{thunk_name}` thunk; value leaked: {msg}",
);
}))
.unwrap_or_else(drop_payload);
}
}
pub fn drop_payload(payload: Box<dyn std::any::Any + Send>) {
if let Err(nested) = catch_unwind(AssertUnwindSafe(move || drop(payload))) {
#[allow(
clippy::mem_forget,
reason = "the replacement payload can also panic on drop"
)]
std::mem::forget(nested);
}
}
pub(crate) fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"plug-in panicked with non-string payload".to_string()
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use rstest::rstest;
use super::*;
#[rstest]
fn returns_ok_on_success() {
let r = guard(|| Ok::<u32, PluginError>(7));
assert_eq!(r.into_result().unwrap(), 7);
}
#[rstest]
fn returns_err_on_returned_error() {
let r = guard(|| Err::<u32, _>(PluginError::generic("boom")));
let e = r.into_result().unwrap_err();
assert_eq!(e.code, PluginErrorCode::Generic);
assert_eq!(e.message_string(), "boom");
}
#[rstest]
fn returns_err_on_string_panic() {
let r = guard(|| -> Result<u32, PluginError> { panic!("oops") });
let e = r.into_result().unwrap_err();
assert_eq!(e.code, PluginErrorCode::Panic);
assert!(e.message_string().contains("oops"));
}
#[rstest]
fn returns_err_on_non_string_panic() {
let r = guard(|| -> Result<u32, PluginError> {
std::panic::panic_any(42_u32);
});
let e = r.into_result().unwrap_err();
assert_eq!(e.code, PluginErrorCode::Panic);
assert!(e.message_string().contains("non-string"));
}
#[rstest]
fn guard_infallible_returns_inner_on_success() {
let v = guard_infallible("test", || 42u64);
assert_eq!(v, 42);
}
#[rstest]
fn guard_or_null_returns_inner_on_success() {
let boxed = Box::into_raw(Box::new(7u32));
let v = guard_or_null("test", || boxed);
assert_eq!(v, boxed);
unsafe { drop(Box::from_raw(boxed)) };
}
#[rstest]
fn guard_or_null_returns_null_on_panic() {
let v: *mut u32 = guard_or_null("test", || panic!("create panic"));
assert!(v.is_null());
}
#[rstest]
fn guard_drop_runs_inner_on_success() {
let mut ran = false;
guard_drop("test", || ran = true);
assert!(ran);
}
#[rstest]
fn guard_drop_swallows_panic() {
guard_drop("test", || panic!("drop panic"));
}
#[rstest]
fn drop_payload_swallows_panicking_drop() {
use std::{
any::Any,
sync::atomic::{AtomicUsize, Ordering},
};
static DROPS_OBSERVED: AtomicUsize = AtomicUsize::new(0);
struct Bomb;
impl Drop for Bomb {
fn drop(&mut self) {
DROPS_OBSERVED.fetch_add(1, Ordering::SeqCst);
panic!("drop panic");
}
}
DROPS_OBSERVED.store(0, Ordering::SeqCst);
let payload: Box<dyn Any + Send> = Box::new(Bomb);
drop_payload(payload);
assert_eq!(DROPS_OBSERVED.load(Ordering::SeqCst), 1);
}
#[rstest]
fn guard_survives_panic_any_with_panicking_drop() {
static DROPS_OBSERVED: AtomicUsize = AtomicUsize::new(0);
struct Bomb;
impl Drop for Bomb {
fn drop(&mut self) {
DROPS_OBSERVED.fetch_add(1, Ordering::SeqCst);
panic!("drop panic");
}
}
DROPS_OBSERVED.store(0, Ordering::SeqCst);
let r = guard(|| -> Result<u32, PluginError> {
std::panic::panic_any(Bomb);
});
let e = r.into_result().unwrap_err();
assert_eq!(e.code, PluginErrorCode::Panic);
assert_eq!(DROPS_OBSERVED.load(Ordering::SeqCst), 1);
}
#[rstest]
fn guard_contains_successive_panicking_payload_destructors() {
use std::sync::Arc;
struct Payload {
drops: Arc<AtomicUsize>,
}
impl Drop for Payload {
fn drop(&mut self) {
self.drops.fetch_add(1, Ordering::SeqCst);
std::panic::panic_any(Self {
drops: Arc::clone(&self.drops),
});
}
}
let drops = Arc::new(AtomicUsize::new(0));
let result = catch_unwind(AssertUnwindSafe(|| {
guard(|| -> Result<(), PluginError> {
std::panic::panic_any(Payload {
drops: Arc::clone(&drops),
});
})
}));
let result = match result {
Ok(result) => Some(result.into_result().unwrap_err()),
Err(payload) => {
#[allow(
clippy::mem_forget,
reason = "preserve the assertion failure without another panic"
)]
std::mem::forget(payload);
None
}
};
assert_eq!(drops.load(Ordering::SeqCst), 1);
let error = result.expect("panic cleanup must not unwind out of the guard");
assert_eq!(error.code, PluginErrorCode::Panic);
assert_eq!(
error.message_string(),
"plug-in panicked with non-string payload"
);
}
#[rstest]
fn guards_contain_panicking_logger_payloads() {
const CHILD: &str = "NAUTILUS_TEST_PANIC_LOGGER_CHILD";
static DROPS: AtomicUsize = AtomicUsize::new(0);
struct Payload;
impl Drop for Payload {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::SeqCst);
std::panic::panic_any(Self);
}
}
struct Logger;
impl log::Log for Logger {
fn enabled(&self, _: &log::Metadata<'_>) -> bool {
true
}
fn log(&self, _: &log::Record<'_>) {
std::panic::panic_any(Payload);
}
fn flush(&self) {}
}
static LOGGER: Logger = Logger;
extern "C" fn exercise() {
let pointer = guard_or_null::<u8>("logger", || panic!("constructor panic"));
assert!(pointer.is_null());
guard_drop("logger", || panic!("destructor panic"));
}
if std::env::var_os(CHILD).is_none() {
let current = std::thread::current();
let name = current.name().expect("the test harness names its thread");
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", name])
.env(CHILD, "1")
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
return;
}
log::set_logger(&LOGGER).unwrap();
log::set_max_level(log::LevelFilter::Error);
exercise();
assert_eq!(DROPS.load(Ordering::SeqCst), 2);
}
}