//! Stdin routing, stdout framing, and process lifetime. #[cfg(target_os = "wasi")] use crate::lines::Lines; use crate::*; use futures::{channel::oneshot, future::LocalBoxFuture}; use serde_json::{Value, json}; use std::{ cell::RefCell, collections::HashMap, future::{Future, IntoFuture}, io::Write, path::Path, }; #[path = "calls.rs"] mod calls; #[path = "locks.rs"] mod locks; pub use calls::*; pub use locks::*; /// Generated main-interface dispatch. pub trait Interface: 'static { /// Describe this interface without constructing its value. fn describe() -> Value where Self: Sized; /// Add this type's methods to shared document generators. #[doc(hidden)] fn add_methods(doc: &mut crate::Discovery) where Self: Sized; /// Describe only this type's methods, without endpoint declarations. fn methods_document() -> Value where Self: Sized; /// List the declared endpoint types and their names. fn endpoint_types(&self) -> Vec<(std::any::TypeId, String)> { vec![] } /// Describe this interface as OpenRPC. fn discover(&self) -> Value; /// Dispatch a request to a generated handler. fn dispatch<'a>( &'a self, method: &'a str, params: Value, ctx: &'a Ctx, ) -> LocalBoxFuture<'a, Result>; /// Run the EOF hook. fn stop(&self) -> LocalBoxFuture<'_, ()> { Box::pin(async {}) } } /// Context of an incoming call. #[derive(Default)] pub struct Ctx { pub(crate) exit: std::cell::Cell, pub(crate) sink: Option>, } impl Ctx { /// Send progress to the caller of this request. pub fn notify(&self, name: &str, params: impl serde::Serialize) -> Result<()> { let params = json!({"name":name,"params":params}); if let Some(sink) = &self.sink { sink.unbounded_send(json!({"jsonrpc":"2.0","method":"core.notify","params":params})) .map_err(|_| Error::new(-32000, "endpoint closed")) } else { notify("core.notify", params) } } /// End the process after this request receives its response. pub fn exit_after_response(&self) { self.exit.set(true); } } type Notify = Box; struct Pending { tx: oneshot::Sender>, notify: Option, unlock: Option>, } struct Lease { path: String, held: std::cell::Cell, } impl Drop for Lease { fn drop(&mut self) { if self.held.get() { let _ = notify("core.unlock", json!({"path":self.path})); } } } #[derive(Default)] struct State { active: bool, endpoints: HashMap, stopping: bool, draining: bool, next: u64, pending: HashMap, // Only leases, never callbacks or response senders, survive dropped lock acquisitions. abandoned_locks: HashMap>, #[cfg(target_os = "wasi")] tasks: usize, #[cfg(target_os = "wasi")] wake: Option, #[cfg(target_os = "wasi")] exit: bool, #[cfg(target_os = "wasi")] listeners: HashMap, } thread_local! { static STATE: RefCell = RefCell::new(State::default()); } #[cfg(any(target_os = "wasi", test))] fn clear_pending() { let pending = STATE.with(|s| { let mut s = s.borrow_mut(); ( std::mem::take(&mut s.pending), std::mem::take(&mut s.abandoned_locks), ) }); // Callback captures may own other call futures whose drop guards use STATE. drop(pending); } pub(crate) fn endpoint_name() -> Result { STATE .with(|s| { s.borrow() .endpoints .get(&std::any::TypeId::of::()) .cloned() }) .ok_or_else(|| { Error::new( -32602, "endpoint type is not declared by the main interface", ) }) } #[cfg(target_os = "wasi")] pub(crate) fn endpoint_task(future: impl Future + 'static) { background(async move { let _ = until_drained(future).await; }); } /// Cancel an accept/read wait when draining, but never an active handler. #[cfg(target_os = "wasi")] pub(crate) async fn until_drained(future: F) -> Option { let (handle, registration) = futures::future::AbortHandle::new_pair(); let id = STATE.with(|s| { let mut s = s.borrow_mut(); if s.draining { return None; } s.next += 1; let id = s.next; s.listeners.insert(id, handle); Some(id) })?; let result = futures::future::Abortable::new(future, registration) .await .ok(); STATE.with(|s| s.borrow_mut().listeners.remove(&id)); result } /// Stop accepting SDK endpoint work, then exit when handlers and background tasks finish. /// /// Calls and notifications still work. This does not run the EOF stop hook or cancel /// background tasks: services with their own listeners/tasks must arrange for those /// to finish too. Repeated calls are harmless. Core's upgrade grace remains the bound. pub fn drain() { STATE.with(|s| { let mut s = s.borrow_mut(); s.draining = true; #[cfg(target_os = "wasi")] { for (_, listener) in s.listeners.drain() { listener.abort(); } if let Some(w) = s.wake.take() { w.wake(); } } }); } #[cfg(target_os = "wasi")] pub(crate) fn exit() { STATE.with(|s| { let mut s = s.borrow_mut(); s.exit = true; if let Some(w) = s.wake.take() { w.wake(); } }); } fn send(value: Value) -> Result<()> { let bytes = format!("\n{value}\n").into_bytes(); let mut stdout = std::io::stdout().lock(); stdout.write_all(&bytes)?; stdout.flush()?; Ok(()) } fn notify(method: &str, params: Value) -> Result<()> { if !STATE.with(|s| s.borrow().active) { return Err(Error::new(-32000, "no core transport")); } send(json!({"jsonrpc":"2.0","method":method,"params":params})) } #[cfg(target_os = "wasi")] /// Execute a future with the SDK runtime. pub fn block_on(f: F) -> F::Output { wstd::runtime::block_on(f) } #[cfg(not(target_os = "wasi"))] /// Execute a future with the SDK runtime. pub fn block_on(f: F) -> F::Output { futures::executor::block_on(f) } /// Run an SDK test with a core transport when the WASI runner supplies one. #[cfg(target_os = "wasi")] /// Execute an async test with the available core transport. pub fn test_run(f: F) -> F::Output { block_on(async { STATE.with(|s| s.borrow_mut().active = true); let reader = wstd::runtime::spawn(async { let mut input = wstd::io::stdin(); let mut lines = Lines::default(); while let Ok(Some(line)) = lines.next(&mut input).await { if let Ok(v) = serde_json::from_slice::(&line) && v["jsonrpc"] == "2.0" { receive(v); } } STATE.with(|s| { let mut s = s.borrow_mut(); s.stopping = true; }); clear_pending(); }); let result = f.await; drop(reader); STATE.with(|s| { let mut s = s.borrow_mut(); s.active = false; }); clear_pending(); result }) } #[cfg(not(target_os = "wasi"))] /// Execute an async test with the available core transport. pub fn test_run(f: F) -> F::Output { block_on(f) } /// Register a task that keeps the process alive. #[cfg(target_os = "wasi")] /// Keep this process alive while a future runs. pub fn background(f: impl Future + 'static) { STATE.with(|s| s.borrow_mut().tasks += 1); wstd::runtime::spawn(async move { f.await; STATE.with(|s| { let mut s = s.borrow_mut(); s.tasks -= 1; if let Some(w) = s.wake.take() { w.wake(); } }); }) .detach(); } #[cfg(not(target_os = "wasi"))] /// Keep this process alive while a future runs. pub fn background(_f: impl Future + 'static) { panic!("background requires the WASI runtime"); } #[doc(hidden)] pub fn receive(value: Value) { if value["method"] == "core.notify" { if let Some(id) = value["params"]["call"].as_u64() { let callback = STATE.with(|s| { s.borrow_mut() .pending .get_mut(&id) .and_then(|p| p.notify.take()) }); if let Some(mut f) = callback { f( value["params"]["name"].as_str().unwrap_or_default(), value["params"]["params"].clone(), ); STATE.with(|s| { if let Some(p) = s.borrow_mut().pending.get_mut(&id) { p.notify = Some(f); } }); } } return; } let pending = value["id"] .as_u64() .and_then(|id| STATE.with(|s| s.borrow_mut().pending.remove(&id))); if let Some(p) = pending { let result = if value.get("error").is_some() { serde_json::from_value(value["error"].clone()).map_or_else(|e| Err(e.into()), Err) } else { Ok(value["result"].clone()) }; if let Some(lease) = &p.unlock { lease .held .set(result.as_ref().is_ok_and(|v| v["held"] == true)); } let _ = p.tx.send(result); } else { let lease = value["id"] .as_u64() .and_then(|id| STATE.with(|s| s.borrow_mut().abandoned_locks.remove(&id))); if let Some(lease) = lease { lease .held .set(value.get("error").is_none() && value["result"]["held"] == true); } else { eprintln!("note: response with unknown ID {}", value["id"]); } } } /// Serve the main interface until its work ends or stdin reaches EOF. #[cfg(target_os = "wasi")] /// Serve the main interface over stdin and stdout. pub fn run(value: T) { block_on(serve(value)); } #[cfg(not(target_os = "wasi"))] /// Serve the main interface over stdin and stdout. pub fn run(_value: T) { panic!("run requires WASI stdin/stdout"); } #[cfg(target_os = "wasi")] async fn serve(value: T) { use std::{rc::Rc, task::Poll}; STATE.with(|s| { *s.borrow_mut() = State { active: true, endpoints: value.endpoint_types().into_iter().collect(), ..State::default() } }); let value = Rc::new(value); let mut input = wstd::io::stdin(); let mut lines = Lines::default(); let mut started = false; loop { let read = lines.next(&mut input); let idle = futures::future::poll_fn(|cx| { STATE.with(|s| { let mut s = s.borrow_mut(); s.wake = Some(cx.waker().clone()); if started && (s.tasks == 0 || s.exit) { Poll::Ready(()) } else { Poll::Pending } }) }); futures::pin_mut!(read, idle); let bytes = match futures::future::select(read, idle).await { futures::future::Either::Left((Ok(Some(b)), _)) => b, futures::future::Either::Left((_, _)) => { STATE.with(|s| { let mut s = s.borrow_mut(); s.stopping = true; for (_, listener) in s.listeners.drain() { listener.abort(); } }); clear_pending(); value.stop().await; break; } futures::future::Either::Right(_) => break, }; let Ok(msg) = serde_json::from_slice::(&bytes) else { continue; }; if msg["jsonrpc"] != "2.0" { continue; } if let Some(method) = msg["method"].as_str() { if method == "core.notify" { receive(msg); continue; } started = true; let value = value.clone(); let method = method.to_owned(); background(async move { let ctx = Ctx::default(); let result = if method == "rpc.discover" { Ok(value.discover()) } else { value .dispatch( &method, msg.get("params").cloned().unwrap_or(json!({})), &ctx, ) .await }; if let Some(id) = msg.get("id") { let response = match result { Ok(v) => json!({"jsonrpc":"2.0","id":id,"result":v}), Err(e) => json!({"jsonrpc":"2.0","id":id,"error":e}), }; let _ = send(response); } if ctx.exit.get() { STATE.with(|s| s.borrow_mut().exit = true); } }); } else { receive(msg); } } STATE.with(|s| { let mut s = s.borrow_mut(); s.active = false; }); clear_pending(); let _ = std::io::stdout().flush(); std::process::exit(0); }