use std::any::Any;
use std::cell::RefCell;
use std::future::Future;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::pin::Pin;
use std::sync::Once;
use std::task::{Context, Poll};
#[derive(Debug, Clone)]
pub struct PanicLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
thread_local! {
static LAST_LOCATION: RefCell<Option<PanicLocation>> = const { RefCell::new(None) };
}
pub fn install_hook() {
static INSTALLED: Once = Once::new();
INSTALLED.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if let Some(location) = info.location() {
LAST_LOCATION.with(|slot| {
*slot.borrow_mut() = Some(PanicLocation {
file: location.file().to_string(),
line: location.line(),
column: location.column(),
});
});
}
if !in_request() {
previous(info);
}
}));
});
}
thread_local! {
static IN_REQUEST: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn in_request() -> bool {
IN_REQUEST.with(std::cell::Cell::get)
}
pub fn take_location() -> Option<PanicLocation> {
LAST_LOCATION.with(|slot| slot.borrow_mut().take())
}
pub fn message_of(payload: &(dyn Any + Send)) -> String {
if let Some(text) = payload.downcast_ref::<&str>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"the handler panicked".to_string()
}
}
pub struct CatchUnwind<F> {
inner: F,
}
impl<F> CatchUnwind<F> {
pub fn new(inner: F) -> Self {
CatchUnwind { inner }
}
}
impl<F: Future> Future for CatchUnwind<F> {
type Output = Result<F::Output, Box<dyn Any + Send>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let inner = unsafe { self.map_unchecked_mut(|this| &mut this.inner) };
let was_in_request = IN_REQUEST.with(|flag| flag.replace(true));
let result = catch_unwind(AssertUnwindSafe(|| inner.poll(cx)));
IN_REQUEST.with(|flag| flag.set(was_in_request));
match result {
Ok(Poll::Pending) => Poll::Pending,
Ok(Poll::Ready(value)) => Poll::Ready(Ok(value)),
Err(payload) => Poll::Ready(Err(payload)),
}
}
}
pub async fn catch<F: Future>(future: F) -> Result<F::Output, String> {
CatchUnwind::new(future).await.map_err(|payload| message_of(payload.as_ref()))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_panicking_future_is_caught() {
install_hook();
let caught = catch(async { panic!("boom") }).await;
assert_eq!(caught.unwrap_err(), "boom");
assert!(take_location().is_some());
}
#[tokio::test]
async fn a_healthy_future_passes_through() {
install_hook();
let value = catch(async { 42 }).await;
assert_eq!(value.unwrap(), 42);
}
#[tokio::test]
async fn panics_across_await_points_are_caught() {
install_hook();
let caught = catch(async {
tokio::task::yield_now().await;
panic!("after yielding");
})
.await;
assert_eq!(caught.unwrap_err(), "after yielding");
}
}