use ::std::cell::{Cell, RefCell};
use ::std::ptr::NonNull;
use ::std::sync::atomic::{AtomicU64, Ordering};
use mozjs::jsapi::*;
use mozjs::jsval::{JSVal, ObjectValue, StringValue, UndefinedValue};
use mozjs::realm::AutoRealm;
use mozjs::rooted;
use crate::gc_store::{gc_store_get_ns, gc_store_insert_ns, gc_store_remove_ns};
static UNHANDLED_COUNTER: AtomicU64 = AtomicU64::new(0);
const NS: &str = "unhandled";
thread_local! {
static DISPATCHING: Cell<bool> = const { Cell::new(false) };
static FLUSHING: Cell<bool> = const { Cell::new(false) };
static PENDING_REJECTIONS: RefCell<Vec<PendingRejection>> =
const { RefCell::new(Vec::new()) };
static CAPTURING: Cell<bool> = const { Cell::new(false) };
static CAPTURED: RefCell<String> = const { RefCell::new(String::new()) };
}
struct PendingRejection {
promise: *mut JSObject,
gc_key: String,
}
struct LatchReset;
impl Drop for LatchReset {
fn drop(&mut self) {
DISPATCHING.with(|c| c.set(false));
}
}
pub fn install(cx: &mut mozjs::context::JSContext) {
unsafe {
mozjs_sys::jsapi::JS::SetPromiseRejectionTrackerCallback(
cx.raw_cx(),
Some(promise_rejection_tracker),
::std::ptr::null_mut(),
);
}
bao_engine::job_queue::set_uncaught_hooks(
uncaught_exception_hook,
flush_rejections_hook,
);
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn promise_rejection_tracker(
cx: *mut JSContext,
_muted_errors: bool,
promise: mozjs_sys::jsapi::JS::HandleObject,
state: mozjs_sys::jsapi::JS::PromiseRejectionHandlingState,
_data: *mut ::std::os::raw::c_void,
) {
let promise_ptr = *promise.ptr;
if promise_ptr.is_null() {
return;
}
match state {
mozjs_sys::jsapi::JS::PromiseRejectionHandlingState::Unhandled => {
let id = UNHANDLED_COUNTER.fetch_add(1, Ordering::Relaxed);
let gc_key = format!("promise_{id}");
gc_store_insert_ns(cx, NS, &gc_key, promise_ptr);
PENDING_REJECTIONS.with(|p| {
p.borrow_mut().push(PendingRejection {
promise: promise_ptr,
gc_key,
})
});
}
mozjs_sys::jsapi::JS::PromiseRejectionHandlingState::Handled => {
let cancelled: Vec<String> = PENDING_REJECTIONS.with(|p| {
let mut pending = p.borrow_mut();
let mut keys = Vec::new();
pending.retain(|e| {
if ::std::ptr::eq(e.promise, promise_ptr) {
keys.push(e.gc_key.clone());
false
} else {
true
}
});
keys
});
for key in cancelled {
gc_store_remove_ns(cx, NS, &key);
}
}
}
}
unsafe fn uncaught_exception_hook(cx: *mut JSContext, reason: JSVal) {
unsafe { route_uncaught_exception(cx, reason) };
}
unsafe fn flush_rejections_hook(cx: *mut JSContext) {
flush_pending_rejections(cx);
}
pub unsafe fn route_uncaught_exception(raw_cx: *mut JSContext, reason: JSVal) {
if DISPATCHING.with(|c| c.get()) {
report_default(raw_cx, "uncaught exception (inside exception handler)", reason);
crate::request_exit(1);
return;
}
DISPATCHING.with(|c| c.set(true));
let _latch = LatchReset;
match emit_process_event(raw_cx, c"uncaughtException", &[reason], 1) {
EmitOutcome::Handled => {
}
EmitOutcome::NoListeners => {
report_default(raw_cx, "uncaught exception", reason);
crate::request_exit(1);
}
EmitOutcome::NoProcess => {
report_default(raw_cx, "uncaught exception", reason);
}
}
}
pub unsafe fn route_unhandled_rejection(
raw_cx: *mut JSContext,
reason: JSVal,
promise: *mut JSObject,
) {
match emit_process_event(
raw_cx,
c"unhandledRejection",
&[reason, ObjectValue(promise)],
2,
) {
EmitOutcome::Handled => return,
EmitOutcome::NoProcess => {
report_default(raw_cx, "unhandled promise rejection", reason);
return;
}
EmitOutcome::NoListeners => {}
}
unsafe { route_uncaught_exception(raw_cx, reason) };
}
enum EmitOutcome {
Handled,
NoListeners,
NoProcess,
}
unsafe fn emit_process_event(
raw_cx: *mut JSContext,
event_name: &::std::ffi::CStr,
args: &[JSVal],
argc: usize,
) -> EmitOutcome {
debug_assert!(argc <= 2 && args.len() >= argc, "emit_process_event: max 2 args");
let global = unsafe { CurrentGlobalOrNull(raw_cx) };
let global = if global.is_null() {
match bao_engine::context::thread_realm_global() {
Some(g) if !g.is_null() => g,
_ => return EmitOutcome::NoProcess,
}
} else {
global
};
let mut cx_ref =
unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx)) };
let mut realm = AutoRealm::new(&mut cx_ref, NonNull::new_unchecked(global));
let cx_ref: &mut mozjs::context::JSContext = &mut realm;
rooted!(&in(cx_ref) let global_root = global);
let mut proc_val = UndefinedValue();
unsafe {
JS_GetProperty(
raw_cx,
global_root.handle().into(),
c"process".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut proc_val,
},
);
}
if !proc_val.is_object() {
return EmitOutcome::NoProcess;
}
rooted!(&in(cx_ref) let proc_obj = proc_val.to_object());
rooted!(&in(cx_ref) let arg0 = args.first().copied().unwrap_or_else(UndefinedValue));
rooted!(&in(cx_ref) let arg1 = args.get(1).copied().unwrap_or_else(UndefinedValue));
let event_str = unsafe { JS_NewStringCopyZ(raw_cx, event_name.as_ptr()) };
if event_str.is_null() {
return EmitOutcome::NoListeners;
}
rooted!(&in(cx_ref) let event_str_val = unsafe { StringValue(&*event_str) });
let call_vals = [
*event_str_val.handle(),
*arg0.handle(),
*arg1.handle(),
];
let call_args = HandleValueArray {
length_: 1 + argc,
elements_: call_vals.as_ptr(),
};
let mut rval = UndefinedValue();
let ok = unsafe {
JS_CallFunctionName(
raw_cx,
proc_obj.handle().into(),
c"emit".as_ptr(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
)
};
if !ok {
unsafe { JS_ClearPendingException(raw_cx) };
return EmitOutcome::NoListeners;
}
if rval.is_boolean() && rval.to_boolean() {
EmitOutcome::Handled
} else {
EmitOutcome::NoListeners
}
}
pub unsafe fn flush_pending_rejections(raw_cx: *mut JSContext) {
if FLUSHING.with(|c| c.get()) {
return;
}
FLUSHING.with(|c| c.set(true));
struct Reset;
impl Drop for Reset {
fn drop(&mut self) {
FLUSHING.with(|c| c.set(false));
}
}
let _reset = Reset;
let entries: Vec<PendingRejection> =
PENDING_REJECTIONS.with(|p| ::std::mem::take(&mut *p.borrow_mut()));
for entry in entries {
let promise = gc_store_get_ns(raw_cx, NS, &entry.gc_key);
gc_store_remove_ns(raw_cx, NS, &entry.gc_key);
let Some(promise) = promise.filter(|p| !p.is_null()) else {
continue;
};
let mut cx_ref =
unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx)) };
rooted!(&in(cx_ref) let promise_root = promise);
let mut reason = UndefinedValue();
{
let mut promise_realm = AutoRealm::new(&mut cx_ref, NonNull::new_unchecked(promise));
let promise_cx: &mut mozjs::context::JSContext = &mut promise_realm;
rooted!(&in(promise_cx) let p_root = promise);
unsafe {
mozjs_sys::glue::JS_GetPromiseResult(
p_root.handle().into(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut reason,
},
);
}
}
unsafe { route_unhandled_rejection(raw_cx, reason, promise) };
}
}
fn report_default(cx: *mut JSContext, label: &str, reason: JSVal) {
let text = value_display(cx, reason);
let report = format!("bao: {label}:\n{text}\n");
if CAPTURING.with(|c| c.get()) {
CAPTURED.with(|c| c.borrow_mut().push_str(&report));
} else {
eprint!("{report}");
}
}
fn value_display(cx: *mut JSContext, val: JSVal) -> String {
if val.is_object() {
let cx_ref = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
rooted!(&in(cx_ref) let obj = val.to_object());
let read_prop = |name: &::std::ffi::CStr| -> Option<String> {
let mut v = UndefinedValue();
unsafe {
bao_stealth::engine_props::get_property_clearing(
cx,
obj.handle().into(),
name,
&mut v,
);
}
if v.is_string() {
Some(unsafe { crate::js_to_rust_string(cx, v) })
} else {
None
}
};
let stack = read_prop(c"stack");
let msg = read_prop(c"message");
return match (msg, stack) {
(Some(m), Some(s)) if s.contains(&m) => s,
(Some(m), Some(s)) => format!("Error: {m}\n{s}"),
(Some(m), None) => format!("Error: {m}"),
(None, Some(s)) => s,
(None, None) => "<non-error object>".to_string(),
};
}
if val.is_string() {
return unsafe { crate::js_to_rust_string(cx, val) };
}
if val.is_int32() {
return val.to_int32().to_string();
}
if val.is_double() {
return val.to_double().to_string();
}
if val.is_boolean() {
return val.to_boolean().to_string();
}
if val.is_null() {
return "null".to_string();
}
if val.is_undefined() {
return "undefined".to_string();
}
"<unprintable value>".to_string()
}
pub fn begin_capture() {
CAPTURED.with(|c| c.borrow_mut().clear());
CAPTURING.with(|c| c.set(true));
}
pub fn take_capture() -> String {
CAPTURING.with(|c| c.set(false));
CAPTURED.with(|c| ::std::mem::take(&mut *c.borrow_mut()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_roundtrip() {
begin_capture();
report_default(::std::ptr::null_mut(), "uncaught exception", UndefinedValue());
let out = take_capture();
assert!(out.contains("uncaught exception"), "capture holds report: {out}");
assert!(out.contains("undefined"), "undefined throw value rendered: {out}");
assert!(out.ends_with('\n'), "report newline-terminated");
}
#[test]
fn value_display_primitives_without_cx() {
assert_eq!(
value_display(::std::ptr::null_mut(), mozjs::jsval::Int32Value(7)),
"7"
);
assert_eq!(
value_display(::std::ptr::null_mut(), mozjs::jsval::BooleanValue(true)),
"true"
);
assert_eq!(
value_display(::std::ptr::null_mut(), mozjs::jsval::NullValue()),
"null"
);
assert_eq!(
value_display(::std::ptr::null_mut(), mozjs::jsval::DoubleValue(1.5)),
"1.5"
);
}
#[test]
fn latched_route_reports_fatal_and_exits_1() {
crate::clear_exit();
begin_capture();
unsafe {
DISPATCHING.with(|c| c.set(true));
route_uncaught_exception(::std::ptr::null_mut(), UndefinedValue());
DISPATCHING.with(|c| c.set(false));
}
let out = take_capture();
assert!(
out.contains("inside exception handler"),
"latched route reports fatal handler failure: {out}"
);
assert!(crate::should_exit(), "fatal handler failure requests exit");
assert_eq!(crate::exit_code(), 1, "fatal handler failure exits 1");
crate::clear_exit();
}
}