use std::ffi::CString;
use std::marker::PhantomData;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use rayforce_sys as sys;
use crate::env::{bind_unary, UnaryFn};
use crate::error::{code_of, ErrorCode, RayError, Result};
use crate::poll::Poll;
use crate::raw;
use crate::runtime::{assert_on_runtime_thread, eval, on_runtime_thread};
use crate::value::Value;
const GUARD: &str = ".serve.guard";
const PAYLOAD: &str = "servePayload";
const TICK: Duration = Duration::from_millis(10);
static INSTALLED: Mutex<Option<Arc<Shared>>> = Mutex::new(None);
fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(PoisonError::into_inner)
}
pub trait Clock: Send + Sync + 'static {
fn now(&self) -> Instant;
}
pub struct Monotonic;
impl Clock for Monotonic {
fn now(&self) -> Instant {
Instant::now()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ended {
Answered,
Failed(ErrorCode),
Cancelled(By),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum By {
Budget,
Hangup,
Client,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestEnd {
pub text: String,
pub duration: Duration,
pub ended: Ended,
}
impl Poll {
pub fn serve_ipc(&self, port: u16) -> ServeIpc<'_> {
ServeIpc {
poll: self,
port,
budget: None,
clock: Arc::new(Monotonic),
}
}
}
#[must_use = "nothing is served until `start`"]
pub struct ServeIpc<'p> {
poll: &'p Poll,
port: u16,
budget: Option<Duration>,
clock: Arc<dyn Clock>,
}
impl<'p> ServeIpc<'p> {
pub fn budget(mut self, budget: Duration) -> Self {
self.budget = Some(budget);
self
}
pub fn clock(mut self, clock: impl Clock) -> Self {
self.clock = Arc::new(clock);
self
}
pub fn start(self) -> Result<IpcServer<'p>> {
assert_on_runtime_thread("ServeIpc::start");
if !self.poll.is_current() {
return Err(RayError::binding("IPC: the loop this was built on is gone"));
}
if self.budget == Some(Duration::ZERO) {
return Err(RayError::binding(
"IPC: a zero budget would cancel every request",
));
}
if self.port == 0 {
return Err(RayError::binding("IPC: port 0 is not a port to serve"));
}
let shared = Arc::new(Shared {
budget: self.budget,
clock: self.clock,
running: Mutex::new(None),
ends: Mutex::new(Vec::new()),
stop: AtomicBool::new(false),
});
{
let mut installed = lock(&INSTALLED);
if installed.is_some() {
return Err(RayError::binding(
"IPC: this process already serves IPC through serve_ipc",
));
}
*installed = Some(Arc::clone(&shared));
}
if let Err(error) = install_hooks().and_then(|()| listen(self.port)) {
uninstall_hooks();
return Err(error);
}
let watched = Arc::clone(&shared);
let watcher = thread::Builder::new()
.name("rayforce-ipc-guard".into())
.spawn(move || watched.watch());
let watcher = match watcher {
Ok(watcher) => watcher,
Err(error) => {
uninstall_hooks();
return Err(RayError::binding(format!(
"IPC: no watcher thread: {error}"
)));
}
};
Ok(IpcServer {
port: self.port,
shared,
watcher: Some(watcher),
_poll: PhantomData,
_not_send: PhantomData,
})
}
}
fn install_hooks() -> Result<()> {
bind_unary::<Guard>(GUARD)?;
for hook in [".ipc.on.sync", ".ipc.on.async"] {
eval(&format!(
"(set {hook} (fn [{PAYLOAD}] ({GUARD} {PAYLOAD})))"
))?;
}
Ok(())
}
fn listen(port: u16) -> Result<()> {
eval(&format!("(.sys.listen {port})")).map(drop)
}
fn uninstall_hooks() {
if on_runtime_thread() {
for hook in [".ipc.on.sync", ".ipc.on.async"] {
let _ = eval(&format!("(del {hook})"));
}
}
*lock(&INSTALLED) = None;
}
pub struct IpcServer<'p> {
port: u16,
shared: Arc<Shared>,
watcher: Option<JoinHandle<()>>,
_poll: PhantomData<&'p Poll>,
_not_send: PhantomData<*mut ()>,
}
impl IpcServer<'_> {
pub fn port(&self) -> u16 {
self.port
}
pub fn drain(&self) -> Vec<RequestEnd> {
std::mem::take(&mut *lock(&self.shared.ends))
}
}
impl Drop for IpcServer<'_> {
fn drop(&mut self) {
self.shared.stop.store(true, Ordering::SeqCst);
if let Some(watcher) = self.watcher.take() {
let _ = watcher.join();
}
uninstall_hooks();
}
}
struct Shared {
budget: Option<Duration>,
clock: Arc<dyn Clock>,
running: Mutex<Option<Running>>,
ends: Mutex<Vec<RequestEnd>>,
stop: AtomicBool,
}
struct Running {
fd: i64,
time_started: Instant,
stopped_by: Option<By>,
}
impl Shared {
fn begin(&self, fd: i64) -> bool {
let mut running = lock(&self.running);
if running.is_some() {
return false;
}
*running = Some(Running {
fd,
time_started: self.clock.now(),
stopped_by: None,
});
true
}
fn finish(&self, text: String, answer: &Value) {
let Some(run) = ({
let mut running = lock(&self.running);
unsafe { sys::ray_clear_interrupt() };
running.take()
}) else {
return;
};
let duration = self.clock.now().saturating_duration_since(run.time_started);
let raw = answer.as_ptr();
let code = unsafe { (!raw.is_null() && raw::is_err(raw)).then(|| code_of(raw)) };
let ended = match (code, run.stopped_by) {
(Some(ErrorCode::Cancel), Some(by)) => Ended::Cancelled(by),
(Some(ErrorCode::Cancel), None) => Ended::Cancelled(By::Client),
(Some(code), _) => Ended::Failed(code),
(None, _) => Ended::Answered,
};
lock(&self.ends).push(RequestEnd {
text,
duration,
ended,
});
}
fn watch(&self) {
while !self.stop.load(Ordering::SeqCst) {
self.check();
thread::sleep(TICK);
}
}
fn check(&self) {
let mut running = lock(&self.running);
let Some(run) = running.as_mut().filter(|run| run.stopped_by.is_none()) else {
return;
};
let elapsed = self.clock.now().saturating_duration_since(run.time_started);
let by = if self.budget.is_some_and(|budget| elapsed >= budget) {
Some(By::Budget)
} else if peer_gone(run.fd) {
Some(By::Hangup)
} else {
None
};
if let Some(by) = by {
run.stopped_by = Some(by);
unsafe { sys::ray_request_interrupt() };
}
}
}
#[cfg(unix)]
fn peer_gone(fd: i64) -> bool {
use std::mem::ManuallyDrop;
use std::net::TcpStream;
use std::os::fd::FromRawFd;
let Ok(fd) = i32::try_from(fd) else {
return false;
};
if fd < 0 {
return false;
}
let sock = ManuallyDrop::new(unsafe { TcpStream::from_raw_fd(fd) });
matches!(sock.peek(&mut [0u8; 1]), Ok(0))
}
#[cfg(not(unix))]
fn peer_gone(_fd: i64) -> bool {
false
}
struct Guard;
impl UnaryFn for Guard {
fn call(payload: Value) -> Value {
let installed = lock(&INSTALLED).clone();
let Some(shared) = installed else {
return evaluate(&payload);
};
let registered = shared.begin(connection_fd());
let answer = evaluate(&payload);
if registered {
shared.finish(text_of(&payload), &answer);
}
answer
}
}
fn is_source(payload: &Value) -> bool {
let raw = payload.as_ptr();
!raw.is_null() && i64::from(unsafe { raw::type_code(raw) }) == -i64::from(sys::RAY_STR)
}
fn evaluate(payload: &Value) -> Value {
let raw = if is_source(payload) {
let text = payload.as_string().unwrap_or_default();
let text = text.split('\0').next().unwrap_or_default();
let source = CString::new(text).unwrap_or_default();
unsafe { sys::ray_eval_str(source.as_ptr()) }
} else {
unsafe { sys::ray_eval(payload.as_ptr()) }
};
if raw.is_null() {
Value::null()
} else {
unsafe { Value::from_owned(raw) }
}
}
fn text_of(payload: &Value) -> String {
if is_source(payload) {
payload.as_string().unwrap_or_default()
} else {
payload.format()
}
}
fn connection_fd() -> i64 {
let Ok(handle) = eval("(.ipc.handle)").and_then(|v| v.as_i64()) else {
return -1;
};
unsafe {
let poll = sys::ray_runtime_get_poll();
if poll.is_null() {
return -1;
}
let selector = sys::ray_poll_get(poll.cast(), handle);
if selector.is_null() {
-1
} else {
(*selector).fd
}
}
}