use std::ffi::c_char;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use ext_php_rs::types::Zval;
use ext_php_rs::zend::{ExecuteData, Function};
use tracing::{debug, info, warn};
static ORIGINAL_OPCACHE_RESET: OnceLock<
unsafe extern "C" fn(
execute_data: *mut ext_php_rs::ffi::_zend_execute_data,
retval: *mut ext_php_rs::ffi::_zval_struct,
),
> = OnceLock::new();
static APPLY_PENDING: AtomicBool = AtomicBool::new(false);
pub(crate) fn enqueue_apply(event: crate::ipc::OwnedInvalEvent) {
match event {
crate::ipc::OwnedInvalEvent::OpcacheReset => {
if APPLY_PENDING.swap(true, Ordering::AcqRel) {
debug!(
target: "claviron::opcache_intercept",
"coalesced duplicate opcache_reset broadcast; reset already pending"
);
}
}
other => {
warn!(
target: "claviron::opcache_intercept",
event = ?other,
"opcache_reset apply queue: unexpected variant; dispatcher bug?"
);
}
}
}
pub(crate) fn drain_apply_queue() {
if !APPLY_PENDING.swap(false, Ordering::AcqRel) {
return;
}
let Some(opcache_reset) = Function::try_from_function("opcache_reset")
else {
warn!(
target: "claviron::opcache_intercept",
"apply: opcache_reset not registered; events dropped"
);
return;
};
let result = super::with_replay_guard(|| opcache_reset.try_call(vec![]));
match result {
Ok(retval) => {
debug!(
target: "claviron::opcache_intercept",
kind = "applied",
success = retval.is_true(),
"broadcast opcache_reset applied"
);
}
Err(e) => {
warn!(
target: "claviron::opcache_intercept",
kind = "applied",
error = %e,
"broadcast opcache_reset call failed"
);
}
}
}
extern "C" fn claviron_opcache_reset_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
match ORIGINAL_OPCACHE_RESET.get() {
Some(original) => unsafe {
original(ex as *mut ExecuteData, retval as *mut Zval);
},
None => {
retval.set_bool(false);
return;
}
}
if retval.is_true()
&& !super::replay_in_progress()
&& let Some(bus) = crate::bus::bus()
{
bus.publish(crate::ipc::InvalEvent::OpcacheReset);
}
info!(
target: "claviron::opcache_intercept",
success = retval.is_true(),
"opcache_reset intercepted"
);
}
pub(crate) fn ensure_opcache_reset_intercept_for_current_thread() {
use ext_php_rs::ffi::{_zend_function, zend_hash_str_find_ptr_lc};
type FunctionPointerHandler =
extern "C" fn(execute_data: *mut ExecuteData, retval: *mut Zval);
type RawFunctionPointerHandler = unsafe extern "C" fn(
execute_data: *mut ext_php_rs::ffi::_zend_execute_data,
retval: *mut ext_php_rs::ffi::_zval_struct,
);
unsafe {
let cg = ext_php_rs::ffi::ext_php_rs_compiler_globals();
if cg.is_null() {
warn!("opcache_reset intercept: compiler globals null");
return;
}
let function_table_ptr = (*cg).function_table;
if function_table_ptr.is_null() {
warn!("opcache_reset intercept: CG(function_table) is null");
return;
}
let key: &[u8] = b"opcache_reset";
let func_ptr = zend_hash_str_find_ptr_lc(
function_table_ptr,
key.as_ptr() as *const c_char,
key.len(),
) as *mut _zend_function;
if func_ptr.is_null() {
warn!(
"opcache_reset intercept: 'opcache_reset' not in CG(function_table); opcache \
extension not loaded for this thread — skipping observer"
);
return;
}
let our_handler_addr =
claviron_opcache_reset_handler as *const () as usize;
if let Some(current) = (*func_ptr).internal_function.handler {
if current as *const () as usize == our_handler_addr {
return;
}
let _ = ORIGINAL_OPCACHE_RESET.set(current);
} else {
warn!(
"opcache_reset intercept: 'opcache_reset' has null handler; skipping"
);
return;
}
let new_handler: FunctionPointerHandler = std::mem::transmute(
claviron_opcache_reset_handler as extern "C" fn(_, _),
);
(*func_ptr).internal_function.handler = Some(std::mem::transmute::<
FunctionPointerHandler,
RawFunctionPointerHandler,
>(new_handler));
#[cfg(debug_assertions)]
tracing::trace!(
"opcache_reset intercept: handler swapped for current thread"
);
}
}