use std::cell::Cell;
use std::ffi::c_char;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use ext_php_rs::args::Arg;
use ext_php_rs::flags::DataType;
use ext_php_rs::types::Zval;
use ext_php_rs::zend::{ExecuteData, SapiGlobals};
use tokio_util::sync::CancellationToken;
use tracing::{debug, warn};
use crate::engine::{ClavironContext, PhpExecution};
use crate::sapi::ServerContext;
type Handler = unsafe extern "C" fn(
execute_data: *mut ext_php_rs::ffi::_zend_execute_data,
retval: *mut ext_php_rs::ffi::_zval_struct,
);
static ENABLED: AtomicBool = AtomicBool::new(false);
pub(crate) fn configure(enabled: bool) {
ENABLED.store(enabled, Ordering::SeqCst);
}
static ORIGINAL_SLEEP: OnceLock<Handler> = OnceLock::new();
static ORIGINAL_USLEEP: OnceLock<Handler> = OnceLock::new();
const POLL_SLICE: Duration = Duration::from_millis(25);
fn cancellation() -> Option<CancellationToken> {
let sapi_globals = SapiGlobals::get();
if sapi_globals.server_context.is_null() {
return None;
}
let context =
ClavironContext::from_server_context(sapi_globals.server_context);
match &context.execution {
PhpExecution::Program(program) => Some(program.cancellation.clone()),
PhpExecution::Request(_) => None,
}
}
thread_local! {
static SIGNALLED: Cell<bool> = const { Cell::new(false) };
}
fn wait(
total: Duration,
token: &CancellationToken,
) -> Duration {
if !token.is_cancelled() {
SIGNALLED.with(|signalled| signalled.set(false));
}
let deadline = Instant::now() + total;
loop {
if token.is_cancelled() && !SIGNALLED.with(Cell::get) {
SIGNALLED.with(|signalled| signalled.set(true));
return deadline.saturating_duration_since(Instant::now());
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Duration::ZERO;
}
std::thread::sleep(remaining.min(POLL_SLICE));
}
}
pub(crate) extern "C" fn claviron_sleep_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
let mut seconds = Arg::new("seconds", DataType::Long);
if ex.parser().arg(&mut seconds).parse().is_err() {
delegate(&ORIGINAL_SLEEP, ex, retval);
return;
}
let Some(seconds) = seconds.val::<i64>() else {
delegate(&ORIGINAL_SLEEP, ex, retval);
return;
};
let Ok(seconds) = u64::try_from(seconds) else {
delegate(&ORIGINAL_SLEEP, ex, retval);
return;
};
let Some(token) = cancellation() else {
delegate(&ORIGINAL_SLEEP, ex, retval);
return;
};
let left = wait(Duration::from_secs(seconds), &token);
if left.is_zero() {
retval.set_long(0);
return;
}
debug!(
target: "claviron::sleep_intercept",
requested_secs = seconds,
remaining_ms = left.as_millis() as u64,
"sleep cut short by cancellation"
);
retval.set_long(i64::try_from(left.as_secs().max(1)).unwrap_or(i64::MAX));
}
pub(crate) extern "C" fn claviron_usleep_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
let mut micros = Arg::new("microseconds", DataType::Long);
if ex.parser().arg(&mut micros).parse().is_err() {
delegate(&ORIGINAL_USLEEP, ex, retval);
return;
}
let Some(micros) = micros.val::<i64>() else {
delegate(&ORIGINAL_USLEEP, ex, retval);
return;
};
let Ok(micros) = u64::try_from(micros) else {
delegate(&ORIGINAL_USLEEP, ex, retval);
return;
};
let Some(token) = cancellation() else {
delegate(&ORIGINAL_USLEEP, ex, retval);
return;
};
let left = wait(Duration::from_micros(micros), &token);
if !left.is_zero() {
debug!(
target: "claviron::sleep_intercept",
requested_us = micros,
remaining_ms = left.as_millis() as u64,
"usleep cut short by cancellation"
);
}
retval.set_null();
}
fn delegate(
original: &OnceLock<Handler>,
ex: &mut ExecuteData,
retval: &mut Zval,
) {
match original.get().copied() {
Some(original) => unsafe {
original(ex as *mut ExecuteData, retval as *mut Zval);
},
None => retval.set_null(),
}
}
pub(crate) fn ensure_sleep_intercept_for_current_thread() {
if !ENABLED.load(Ordering::SeqCst) {
return;
}
install(b"sleep", &ORIGINAL_SLEEP, claviron_sleep_handler);
install(b"usleep", &ORIGINAL_USLEEP, claviron_usleep_handler);
}
fn install(
name: &[u8],
original: &OnceLock<Handler>,
replacement: extern "C" fn(&mut ExecuteData, &mut Zval),
) {
use ext_php_rs::ffi::{_zend_function, zend_hash_str_find_ptr_lc};
let label = String::from_utf8_lossy(name).into_owned();
unsafe {
let cg = ext_php_rs::ffi::ext_php_rs_compiler_globals();
if cg.is_null() {
warn!(function = %label, "sleep intercept: compiler globals null");
return;
}
let function_table_ptr = (*cg).function_table;
if function_table_ptr.is_null() {
warn!(function = %label, "sleep intercept: CG(function_table) is null");
return;
}
let func_ptr = zend_hash_str_find_ptr_lc(
function_table_ptr,
name.as_ptr() as *const c_char,
name.len(),
) as *mut _zend_function;
if func_ptr.is_null() {
warn!(function = %label, "sleep intercept: function not in CG(function_table)");
return;
}
let replacement_addr = replacement as *const () as usize;
if let Some(current) = (*func_ptr).internal_function.handler {
if current as *const () as usize == replacement_addr {
return;
}
let _ = original.set(current);
} else {
warn!(function = %label, "sleep intercept: null handler; skipping");
return;
}
(*func_ptr).internal_function.handler = Some(std::mem::transmute::<
extern "C" fn(&mut ExecuteData, &mut Zval),
Handler,
>(replacement));
}
}