//! Probe of SDK transport and lifecycle behavior. use sausage_plugin::serde_json::{Value, json}; use sausage_plugin::{Ctx, Result, call, lock, rpc, schema}; struct Probe; /// A virtual path to observe. #[schema] struct WatchPath { /// Directory or file, not necessarily present yet. path: String, } /// The values to add. #[schema] struct Add { /// The first number. a: i32, /// The second number. b: i32, } /// The retired version. #[schema] struct Retired { /// The previous hash. previous: String, } /// The replacement version. #[schema] struct Superseded { /// The current hash. current: String, } /// A large payload. #[schema] struct Echo { /// The text to return unchanged. text: String, } #[rpc(grace_seconds = 12,endpoints(control=Control))] impl Probe { /// Scan only after the watch is armed; keep the call open until process death. async fn watch(&self, p: WatchPath) -> Result<()> { let path = p.path.clone(); call("core.watch", json!({"path":p.path})) .on_notify(move |name, _| { if name == "armed" && path == "/self" { std::fs::write("/self/owned", "visible via proc").unwrap(); } let mut entries: Vec<_> = std::fs::read_dir(&path) .into_iter() .flatten() .filter_map(std::result::Result::ok) .map(|e| e.file_name().into_string().unwrap()) .collect(); entries.sort(); let text = std::fs::read_to_string(format!("{path}/before")).ok(); Ctx::default() .notify( name, json!({"pid":sausage_plugin::pid(), "entries":entries, "text":text}), ) .unwrap(); }) .await?; Ok(()) } /// Poll the watch in the handler, then return idle without a background task. async fn watch_once(&self, p: WatchPath) -> Result<()> { use std::{ cell::RefCell, rc::Rc, task::{Poll, Waker}, }; let state = Rc::new(RefCell::new((false, None::))); let received = state.clone(); let mut watch = std::pin::pin!(std::future::IntoFuture::into_future( call("core.watch", json!({"path":p.path})).on_notify(move |name, _| { if name == "armed" { let mut state = received.borrow_mut(); state.0 = true; if let Some(waker) = state.1.take() { waker.wake(); } } }) )); std::future::poll_fn(|cx| { if let Poll::Ready(result) = watch.as_mut().poll(cx) { return Poll::Ready(result.map(|_| ())); } let mut state = state.borrow_mut(); state.1 = Some(cx.waker().clone()); if state.0 { Poll::Ready(Ok(())) } else { Poll::Pending } }) .await } /// Drop one watch while another still delivers, in a process that remains alive. async fn drop_watch(&self) -> Result> { use std::{cell::RefCell, future::IntoFuture, rc::Rc, task::Poll}; let events = Rc::new(RefCell::new(Vec::new())); let captured = events.clone(); let mut dropped = call("core.watch", json!({"path":"/dropped"})) .on_notify(move |_, _| captured.borrow_mut().push("dropped".to_owned())) .into_future(); std::future::poll_fn(|cx| { assert!(dropped.as_mut().poll(cx).is_pending()); Poll::Ready(()) }) .await; drop(dropped); let captured = events.clone(); let mut kept = call("core.watch", json!({"path":"/kept"})) .on_notify(move |name, _| captured.borrow_mut().push(name.to_owned())) .into_future(); std::future::poll_fn(|cx| { assert!(kept.as_mut().poll(cx).is_pending()); Poll::Ready(()) }) .await; // The harness sends late traffic and kept-watch events before answering. call("other.barrier", json!({})).await?; drop(kept); let result = events.borrow().clone(); Ok(result) } /// Publish a control endpoint until EOF. async fn serve(&self) -> Result<()> { sausage_plugin::listen("/endpoint", std::rc::Rc::new(Control)).await } /// Forward detached progress through the real core. async fn relay(&self) -> Result { call("probe.progress", json!({})) .detach() .on_notify(move |name, params| { Ctx::default().notify(name, params).unwrap(); }) .await .map(|value| value.as_i64().unwrap() as i32) } /// Send progress before the final result. fn progress(&self, ctx: &Ctx) -> Result { ctx.notify("step", json!({"done":true}))?; Ok(42) } /// Start a detached service that survives this caller. async fn launch(&self) -> Result { Ok(sausage_plugin::serde_json::from_value( call("probe.service", json!({})).detach().await?, )?) } /// Keep the process alive until the core closes stdin. async fn service(&self) -> Result { sausage_plugin::listen("/state/control", std::rc::Rc::new(Control)).await?; Ok(sausage_plugin::pid()) } /// Keep an idle listener and ownership lock until opt-in drain ends this process. async fn drain_service(&self) -> Result { lock("/drain-owner").await?.keep(); self.service().await } /// Add two numbers. /// /// The result is their sum. fn add(&self, p: Add) -> Result { Ok(p.a + p.b) } /// Echo a payload larger than a host I/O buffer. fn echo(&self, p: Echo) -> Result { Ok(p.text) } /// Return an internal serialization error. fn serde_error(&self) -> Result { Ok(sausage_plugin::serde_json::from_str::("invalid")?) } /// Return a deliberate error. fn fail(&self) -> Result { Err(sausage_plugin::Error { code: -32010, message: "deliberate".into(), data: Some(json!({"diagnostic":"probe error details"})), }) } /// Exercise calls, notifications, and advisory locks. async fn exercise(&self, ctx: &Ctx) -> Result { let a = call("other.first", json!({})).on_notify(|name, _| eprintln!("notification: {name}")); let b = call("other.second", json!({})).detach(); let (a, b) = join(a, b).await; a?; b?; ctx.notify("progress", json!({"done":true}))?; { let guard = lock("/state/probe/file").await?; assert!(guard.held()); } assert!(!lock("/state/probe/file").try_now().await?.held()); Ok("done".into()) } /// Drop an acquisition before the core grants its lock. async fn late(&self) -> Result { { let mut future = std::pin::pin!(std::future::IntoFuture::into_future(lock("/late"))); std::future::poll_fn(|cx| { assert!(future.as_mut().poll(cx).is_pending()); std::task::Poll::Ready(()) }) .await; } call("other.barrier", json!({})).await?; lock("/kept").await?.keep(); Ok("late done".into()) } /// Mark a retired version. #[sausage_plugin::on_retired] fn retired(&self, p: Retired) -> Result { Ok(p.previous) } /// Drain on replacement while calls are still available. #[sausage_plugin::on_superseded] async fn superseded(&self, p: Superseded) -> Result<()> { call("other.superseded", json!({"current":p.current})).await?; sausage_plugin::drain(); sausage_plugin::drain(); Ctx::default().notify("draining", json!({}))?; Ok(()) } /// Wait until EOF, then run the stop hook. async fn wait(&self) -> Result { call("other.wait", json!({})).await?; Ok("done".into()) } /// Keep the process alive with a background call. fn start(&self) -> Result { sausage_plugin::background(async { let _ = call("other.background", json!({})).await; }); Ok("started".into()) } /// Leave an unfinished diagnostic before the SDK writes its framed response. fn framed_output(&self) -> Result { print!("partial diagnostic"); Ok("intact".into()) } /// Keep the process alive until the test releases its background task. fn background_hold(&self) -> Result { sausage_plugin::background(async { while !std::path::Path::new("/release-background").exists() { wstd::task::sleep(wstd::time::Duration::from_millis(10)).await; } std::fs::write("/background-ended", "done").unwrap(); }); Ok(sausage_plugin::pid()) } /// Keep a pollable alive for one hour. fn sleepy(&self) -> Result<()> { sausage_plugin::background(async { wstd::task::sleep(wstd::time::Duration::from_secs(3600)).await; }); Ok(()) } /// Exit after the response despite a pending background pollable. fn sleepy_exit(&self, ctx: &Ctx) -> Result<()> { self.sleepy()?; ctx.exit_after_response(); Ok(()) } /// Save an EOF marker. #[sausage_plugin::on_stop] async fn stop(&self) { std::fs::write("/stopped", b"yes").unwrap(); std::fs::write(format!("/stopped-{}", sausage_plugin::pid()), b"yes").unwrap(); Ctx::default().notify("stopped", json!({})).unwrap(); assert!(call("other.no", json!({})).await.is_err()); } } async fn join( a: impl std::future::IntoFuture>, b: impl std::future::IntoFuture>, ) -> (Result, Result) { let mut a = std::pin::pin!(a.into_future()); let mut b = std::pin::pin!(b.into_future()); let (mut av, mut bv) = (None, None); std::future::poll_fn(move |cx| { if av.is_none() { if let std::task::Poll::Ready(v) = a.as_mut().poll(cx) { av = Some(v); } } if bv.is_none() { if let std::task::Poll::Ready(v) = b.as_mut().poll(cx) { bv = Some(v); } } if av.is_some() && bv.is_some() { std::task::Poll::Ready((av.take().unwrap(), bv.take().unwrap())) } else { std::task::Poll::Pending } }) .await } struct Control; #[rpc] impl Control { /// Report progress and return a stable counter. fn stats(&self, ctx: &Ctx) -> Result { ctx.notify("endpoint-progress", json!({"value":7}))?; Ok(7) } /// Finish an in-flight call after draining begins. async fn wait(&self) -> Result { call("other.inflight", json!({})).await?; Ok("finished".into()) } /// End the endpoint process after the response. fn stop(&self, ctx: &Ctx) -> Result<()> { ctx.exit_after_response(); Ok(()) } } fn main() { sausage_plugin::run(Probe) }