b7d9d7abd9
DaemonEngine: implementación del trait Engine que delega a otro proceso vía Unix socket. Materializa el patrón horizontal de sandokan (el binario que arranca primero expone el engine; los demás se le suman). - protocol.rs — DaemonRequest/DaemonResponse (espejan los métodos de Engine) + framing postcard length-prefixed (u32 LE + bytes), con MAX_FRAME 16 MiB defensivo. - client.rs — DaemonEngine: stateless, un round-trip por llamada; is_reachable() para el probe de auto(). - server.rs — serve(engine, socket): envuelve cualquier Engine, una task por conexión, multi-request por conexión. EngineError ahora es Serialize/Deserialize (viaja por el wire); NotFound se propaga tipado a través del socket. 1 test de integración: roundtrip real DaemonEngine ↔ serve ↔ LocalEngine (list vacío + NotFound propagado). cargo check --workspace verde. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
124 lines
4.2 KiB
Rust
124 lines
4.2 KiB
Rust
//! Loop servidor: envuelve cualquier `Engine` y lo expone por Unix socket.
|
|
|
|
use crate::protocol::{read_frame, write_frame, DaemonRequest, DaemonResponse};
|
|
use sandokan_core::Engine;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::net::{UnixListener, UnixStream};
|
|
|
|
/// Sirve `engine` en `socket_path` hasta que el future se cancele.
|
|
///
|
|
/// Si el socket ya existe (daemon previo que no limpió), se borra antes
|
|
/// de bind. Cada conexión se atiende en su propia task; una conexión
|
|
/// puede mandar múltiples requests secuenciales.
|
|
pub async fn serve<E>(engine: Arc<E>, socket_path: &Path) -> std::io::Result<()>
|
|
where
|
|
E: Engine + 'static,
|
|
{
|
|
if socket_path.exists() {
|
|
let _ = std::fs::remove_file(socket_path);
|
|
}
|
|
let listener = UnixListener::bind(socket_path)?;
|
|
tracing::info!(socket = %socket_path.display(), "sandokan-daemon escuchando");
|
|
|
|
loop {
|
|
let (stream, _addr) = listener.accept().await?;
|
|
let engine = Arc::clone(&engine);
|
|
tokio::spawn(async move {
|
|
if let Err(e) = handle_conn(stream, engine).await {
|
|
tracing::debug!(error = %e, "conexión terminada");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Atiende una conexión: lee requests hasta EOF, responde cada uno.
|
|
async fn handle_conn<E>(mut stream: UnixStream, engine: Arc<E>) -> std::io::Result<()>
|
|
where
|
|
E: Engine,
|
|
{
|
|
loop {
|
|
let req: DaemonRequest = match read_frame(&mut stream).await {
|
|
Ok(r) => r,
|
|
// EOF limpio = el cliente cerró; no es error.
|
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(()),
|
|
Err(e) => return Err(e),
|
|
};
|
|
let resp = dispatch(&*engine, req).await;
|
|
write_frame(&mut stream, &resp).await?;
|
|
}
|
|
}
|
|
|
|
/// Traduce un request a la llamada `Engine` correspondiente.
|
|
async fn dispatch<E: Engine>(engine: &E, req: DaemonRequest) -> DaemonResponse {
|
|
match req {
|
|
DaemonRequest::Run(intent) => match engine.run(intent).await {
|
|
Ok(h) => DaemonResponse::Ran(h),
|
|
Err(e) => DaemonResponse::Err(e),
|
|
},
|
|
DaemonRequest::Stop { card_id, grace_ms } => {
|
|
match engine.stop(card_id, Duration::from_millis(grace_ms)).await {
|
|
Ok(()) => DaemonResponse::Stopped,
|
|
Err(e) => DaemonResponse::Err(e),
|
|
}
|
|
}
|
|
DaemonRequest::List => match engine.list().await {
|
|
Ok(v) => DaemonResponse::Listed(v),
|
|
Err(e) => DaemonResponse::Err(e),
|
|
},
|
|
DaemonRequest::Status { card_id } => match engine.status(card_id).await {
|
|
Ok(s) => DaemonResponse::Status(s),
|
|
Err(e) => DaemonResponse::Err(e),
|
|
},
|
|
DaemonRequest::Telemetry { card_id } => match engine.telemetry(card_id).await {
|
|
Ok(t) => DaemonResponse::Telemetry(t),
|
|
Err(e) => DaemonResponse::Err(e),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use sandokan_core::{Engine, EngineError};
|
|
use sandokan_local::LocalEngine;
|
|
use ulid::Ulid;
|
|
|
|
#[tokio::test]
|
|
async fn roundtrip_list_and_notfound() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let sock = dir.path().join("sandokan.sock");
|
|
|
|
let engine = Arc::new(LocalEngine::new());
|
|
let sock_srv = sock.clone();
|
|
let srv = tokio::spawn(async move {
|
|
let _ = serve(engine, &sock_srv).await;
|
|
});
|
|
|
|
// Espera a que el socket esté listo.
|
|
for _ in 0..50 {
|
|
if sock.exists() {
|
|
break;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
|
|
let client = crate::DaemonEngine::new(sock.clone());
|
|
assert!(client.is_reachable().await);
|
|
|
|
// list() sobre engine vacío → vacío.
|
|
let listed = client.list().await.expect("list");
|
|
assert!(listed.is_empty());
|
|
|
|
// status() de un id desconocido → NotFound propagado por el wire.
|
|
let unknown = Ulid::new();
|
|
match client.status(unknown).await {
|
|
Err(EngineError::NotFound(id)) => assert_eq!(id, unknown),
|
|
other => panic!("esperaba NotFound, fue {other:?}"),
|
|
}
|
|
|
|
srv.abort();
|
|
}
|
|
}
|