use crate::prelude::*;
use std::cell::Cell;
use std::future::Future;
use std::pin::Pin;
use std::task;
use std::task::Poll;
thread_local! {
static IN_SCOPE: Cell<bool> = Cell::new(false);
static CONTEXT: Cell<Option<PanicContext>> = Cell::new(None);
}
static INITIALIZED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
pub struct PanicContext {
payload: Option<String>,
location: Option<FileSpan>,
}
impl PanicContext {
pub fn catch(func: impl FnOnce() -> Result<(), String>) -> PanicResult {
match Self::catch_poll(|| Poll::Ready(func())) {
Poll::Ready(result) => result,
Poll::Pending => {
unreachable!("catch should not return pending")
}
}
}
pub fn catch_async<Fut>(fut: Fut) -> impl Future<Output = PanicResult>
where
Fut: Future<Output = Result<(), String>>,
{
PanicContextFuture::new(async move { fut.await })
}
fn catch_poll(
func: impl FnOnce() -> Poll<Result<(), String>>,
) -> Poll<PanicResult> {
if INITIALIZED.get().is_none() {
Self::init();
}
let prev_cx = CONTEXT.with(|cx| cx.take());
let prev_scope = IN_SCOPE.with(|in_scope| in_scope.get());
IN_SCOPE.with(|in_scope| in_scope.set(true));
#[cfg(target_arch = "wasm32")]
let result = {
let mut poll_result = None;
let catch_result = js_runtime::catch_no_abort(|| {
poll_result = Some(func());
Ok(())
});
match catch_result {
Ok(_) => Ok(poll_result.expect("func not called")),
Err(()) => Err(()),
}
};
#[cfg(not(target_arch = "wasm32"))]
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
let result = match result {
Ok(Poll::Ready(Ok(()))) => Poll::Ready(PanicResult::Ok),
Ok(Poll::Ready(Err(err))) => Poll::Ready(PanicResult::Err(err)),
Ok(Poll::Pending) => Poll::Pending,
Err(_) => {
let context = CONTEXT.with(|cx| {
cx.take().expect(
"panic without context, has the panic hook been overridden?",
)
});
Poll::Ready(PanicResult::Panic {
payload: context.payload,
location: context.location,
})
}
};
IN_SCOPE.with(|in_scope| in_scope.set(prev_scope));
CONTEXT.with(|cx| cx.set(prev_cx));
result
}
fn init() {
INITIALIZED.get_or_init(|| true);
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if IN_SCOPE.with(|in_scope| in_scope.get()) {
CONTEXT.with(|cx| {
let payload = downcast_str(info.payload());
let location =
info.location().map(FileSpan::new_from_location);
cx.set(Some(PanicContext { payload, location }));
});
} else {
default_hook(info);
return;
}
}));
}
}
fn downcast_str(payload: &dyn std::any::Any) -> Option<String> {
if let Some(str) = payload.downcast_ref::<&str>() {
Some(str.to_string())
} else if let Some(str) = payload.downcast_ref::<String>() {
Some(str.clone())
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PanicResult {
Ok,
Err(String),
Panic {
payload: Option<String>,
location: Option<FileSpan>,
},
}
impl PanicResult {
pub fn is_ok(&self) -> bool { matches!(self, PanicResult::Ok) }
pub fn is_err(&self) -> bool { matches!(self, PanicResult::Err(_)) }
pub fn is_panic(&self) -> bool { matches!(self, PanicResult::Panic { .. }) }
}
struct PanicContextFuture<F> {
inner: F,
}
impl<F> PanicContextFuture<F> {
pub fn new(inner: F) -> Self { Self { inner } }
}
impl<F: Future<Output = Result<(), String>>> Future for PanicContextFuture<F> {
type Output = PanicResult;
fn poll(
self: Pin<&mut Self>,
cx: &mut task::Context,
) -> Poll<Self::Output> {
let inner = unsafe { self.map_unchecked_mut(|s| &mut s.inner) };
PanicContext::catch_poll(|| inner.poll(cx))
}
}
#[cfg(test)]
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use crate::prelude::*;
#[test]
fn works() {
PanicContext::catch(|| Ok(())).xpect_eq(PanicResult::Ok);
PanicContext::catch(|| Err("foobar".into()))
.xpect_eq(PanicResult::Err("foobar".into()));
PanicContext::catch(|| panic!("foobar")).xpect_eq(PanicResult::Panic {
payload: Some("foobar".into()),
location: Some(FileSpan::new_with_start(file!(), line!() - 2, 31)),
});
}
#[crate::test]
async fn works_async() {
PanicContext::catch_async(async { Ok(()) })
.await
.xpect_eq(PanicResult::Ok);
PanicContext::catch_async(async { Err("foobar".into()) })
.await
.xpect_eq(PanicResult::Err("foobar".into()));
PanicContext::catch_async(async {
async_ext::yield_now().await;
async_ext::yield_now().await;
async_ext::yield_now().await;
panic!("foobar")
})
.await
.xpect_eq(PanicResult::Panic {
payload: Some("foobar".into()),
location: Some(FileSpan::new_with_start(file!(), line!() - 5, 12)),
});
}
}