feat(sandokan-daemon): B1.3 — DaemonEngine + protocolo wire
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>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
//! Protocolo wire del daemon: requests/responses + framing.
|
||||
//!
|
||||
//! Encoding: postcard. Framing: prefijo de longitud `u32` little-endian
|
||||
//! seguido de los bytes postcard. Mismo patrón que el wire de shuma.
|
||||
|
||||
use sandokan_core::{EngineError, ExecHandle, Intent, TelemetryFrame};
|
||||
use sandokan_lifecycle::LifecycleState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use ulid::Ulid;
|
||||
|
||||
/// Request del cliente al daemon. Espeja los métodos de `Engine`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DaemonRequest {
|
||||
Run(Intent),
|
||||
Stop { card_id: Ulid, grace_ms: u64 },
|
||||
List,
|
||||
Status { card_id: Ulid },
|
||||
Telemetry { card_id: Ulid },
|
||||
}
|
||||
|
||||
/// Response del daemon al cliente. Una variante por resultado posible.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DaemonResponse {
|
||||
Ran(ExecHandle),
|
||||
Stopped,
|
||||
Listed(Vec<ExecHandle>),
|
||||
Status(LifecycleState),
|
||||
Telemetry(TelemetryFrame),
|
||||
Err(EngineError),
|
||||
}
|
||||
|
||||
/// Límite defensivo de tamaño de frame (16 MiB). Un Intent con una Card
|
||||
/// grande sigue cabiendo; protege contra frames corruptos/maliciosos.
|
||||
pub const MAX_FRAME: u32 = 16 * 1024 * 1024;
|
||||
|
||||
/// Escribe un valor serializable como frame length-prefixed.
|
||||
pub async fn write_frame<W, T>(w: &mut W, value: &T) -> std::io::Result<()>
|
||||
where
|
||||
W: AsyncWriteExt + Unpin,
|
||||
T: Serialize,
|
||||
{
|
||||
let bytes = postcard::to_stdvec(value)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
if bytes.len() as u64 > MAX_FRAME as u64 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"frame excede MAX_FRAME",
|
||||
));
|
||||
}
|
||||
w.write_all(&(bytes.len() as u32).to_le_bytes()).await?;
|
||||
w.write_all(&bytes).await?;
|
||||
w.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lee un frame length-prefixed y lo deserializa.
|
||||
pub async fn read_frame<R, T>(r: &mut R) -> std::io::Result<T>
|
||||
where
|
||||
R: AsyncReadExt + Unpin,
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let mut len_buf = [0u8; 4];
|
||||
r.read_exact(&mut len_buf).await?;
|
||||
let len = u32::from_le_bytes(len_buf);
|
||||
if len > MAX_FRAME {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"frame entrante excede MAX_FRAME",
|
||||
));
|
||||
}
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
r.read_exact(&mut buf).await?;
|
||||
postcard::from_bytes(&buf)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
Reference in New Issue
Block a user