use std::cell::{Cell, RefCell};
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use mozjs::jsapi::JSContext as RawJSContext;
use mozjs::jsapi::JS_RequestInterruptCallback;
use mozjs::rust::wrappers2::JS_AddInterruptCallback;
use crate::context::JsContext;
use crate::error::JsError;
use crate::value::JsValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalState {
Running,
Completed,
Errored,
Cancelled,
TimedOut,
}
const ST_RUNNING: u8 = 0;
const ST_COMPLETED: u8 = 1;
const ST_ERRORED: u8 = 2;
const ST_CANCELLED: u8 = 3;
const ST_TIMED_OUT: u8 = 4;
impl TerminalState {
fn from_u8(v: u8) -> Self {
match v {
ST_COMPLETED => TerminalState::Completed,
ST_ERRORED => TerminalState::Errored,
ST_CANCELLED => TerminalState::Cancelled,
ST_TIMED_OUT => TerminalState::TimedOut,
_ => TerminalState::Running,
}
}
fn to_u8(self) -> u8 {
match self {
TerminalState::Running => ST_RUNNING,
TerminalState::Completed => ST_COMPLETED,
TerminalState::Errored => ST_ERRORED,
TerminalState::Cancelled => ST_CANCELLED,
TerminalState::TimedOut => ST_TIMED_OUT,
}
}
}
struct InterruptRequester {
cx: *mut RawJSContext,
}
unsafe impl Send for InterruptRequester {}
unsafe impl Sync for InterruptRequester {}
impl InterruptRequester {
fn request(&self) {
unsafe { JS_RequestInterruptCallback(self.cx) };
}
}
struct ControlShared {
cancelled: AtomicBool,
terminal: AtomicU8,
requester: InterruptRequester,
}
impl ControlShared {
fn latch(&self, state: TerminalState) {
let _ = self.terminal.compare_exchange(
ST_RUNNING,
state.to_u8(),
Ordering::AcqRel,
Ordering::Acquire,
);
}
}
struct ArmedExecution {
control: Arc<ControlShared>,
deadline: Option<Instant>,
}
thread_local! {
static ARMED: RefCell<Vec<ArmedExecution>> = const { RefCell::new(Vec::new()) };
static CALLBACK_INSTALLED_ON: Cell<*mut RawJSContext> = const { Cell::new(ptr::null_mut()) };
}
pub(crate) fn ensure_callback_installed(cx: *mut RawJSContext) {
CALLBACK_INSTALLED_ON.with(|c| {
if c.get() == cx {
return;
}
let cx_wrap = unsafe {
mozjs::context::JSContext::from_ptr(ptr::NonNull::new_unchecked(cx))
};
let ok = unsafe { JS_AddInterruptCallback(&cx_wrap, Some(bao_interrupt_callback)) };
assert!(ok, "JS_AddInterruptCallback failed (OOM appending callback)");
c.set(cx);
});
}
pub(crate) fn on_context_destroyed() {
CALLBACK_INSTALLED_ON.with(|c| c.set(ptr::null_mut()));
}
pub(crate) fn on_runtime_created() {
CALLBACK_INSTALLED_ON.with(|c| c.set(ptr::null_mut()));
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bao_interrupt_callback(_cx: *mut RawJSContext) -> bool {
let mut decision = true;
ARMED.with(|stack| {
let mut s = stack.borrow_mut();
let Some(armed) = s.last_mut() else {
return; };
if armed.control.cancelled.load(Ordering::Acquire) {
armed.control.latch(TerminalState::Cancelled);
decision = false;
} else if armed
.deadline
.map_or(false, |d| Instant::now() >= d)
{
armed.control.latch(TerminalState::TimedOut);
decision = false;
}
});
decision
}
struct WatcherSignal {
done: Mutex<bool>,
fire: Condvar,
}
struct ArmedExecutionGuard {
watcher: Option<std::thread::JoinHandle<()>>,
signal: Arc<WatcherSignal>,
}
impl ArmedExecutionGuard {
fn new(control: Arc<ControlShared>, deadline: Option<Instant>) -> Self {
ARMED.with(|s| s.borrow_mut().push(ArmedExecution {
control: control.clone(),
deadline,
}));
let signal = Arc::new(WatcherSignal {
done: Mutex::new(false),
fire: Condvar::new(),
});
let watcher = deadline.map(|d| {
let sig = Arc::clone(&signal);
let shared = Arc::clone(&control);
std::thread::spawn(move || {
let mut done = sig.done.lock().unwrap();
while !*done {
let now = Instant::now();
if now >= d {
shared.requester.request();
return;
}
let (guard, _) = sig
.fire
.wait_timeout(done, d - now)
.unwrap_or_else(|e| e.into_inner());
done = guard;
}
})
});
ArmedExecutionGuard { watcher, signal }
}
}
impl Drop for ArmedExecutionGuard {
fn drop(&mut self) {
*self.signal.done.lock().unwrap_or_else(|e| e.into_inner()) = true;
self.signal.fire.notify_all();
if let Some(handle) = self.watcher.take() {
let _ = handle.join();
}
ARMED.with(|s| {
s.borrow_mut().pop();
});
}
}
#[derive(Clone)]
pub struct ExecutionControl {
shared: Arc<ControlShared>,
}
#[doc(hidden)]
impl ExecutionControl {
pub fn new() -> Self {
let cx = mozjs::rust::Runtime::get()
.expect("ExecutionControl::new must run on the owner JSContext thread (Runtime alive)")
.as_ptr();
ExecutionControl {
shared: Arc::new(ControlShared {
cancelled: AtomicBool::new(false),
terminal: AtomicU8::new(ST_RUNNING),
requester: InterruptRequester { cx },
}),
}
}
pub fn cancel(&self) {
self.shared.cancelled.store(true, Ordering::Release);
self.shared.requester.request();
}
pub fn terminal_state(&self) -> TerminalState {
TerminalState::from_u8(self.shared.terminal.load(Ordering::Acquire))
}
pub fn reset(&self) {
self.shared.cancelled.store(false, Ordering::Release);
self.shared
.terminal
.store(ST_RUNNING, Ordering::Release);
}
fn termination_error(&self, state: TerminalState) -> JsError {
let message = match state {
TerminalState::TimedOut => "Script terminated: deadline exceeded (timeout)".to_string(),
TerminalState::Cancelled => "Script terminated: execution cancelled".to_string(),
other => format!("Script terminated: {:?}", other),
};
JsError {
message,
filename: "<execution-control>".to_string(),
line: 0,
column: 0,
stack: None,
}
}
}
impl JsContext {
#[doc(hidden)]
pub fn eval_with_control(
&mut self,
control: &ExecutionControl,
source: &str,
filename: &str,
timeout: Option<Duration>,
) -> Result<JsValue, JsError> {
assert_eq!(
control.shared.requester.cx,
self.raw_cx(),
"ExecutionControl is bound to a different JSContext than the eval target"
);
ensure_callback_installed(self.raw_cx());
control.reset();
let _armed = ArmedExecutionGuard::new(
Arc::clone(&control.shared),
timeout.map(|t| Instant::now() + t),
);
let result = self.eval(source, filename);
match &result {
Ok(_) => control.shared.latch(TerminalState::Completed),
Err(_) => {
control.shared.latch(TerminalState::Errored);
}
}
match control.terminal_state() {
TerminalState::TimedOut => Err(control.termination_error(TerminalState::TimedOut)),
TerminalState::Cancelled => Err(control.termination_error(TerminalState::Cancelled)),
_ => result,
}
}
}