From 94ea0eaa539cf93d58167ea1687b5a242b196573 Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 20 May 2026 15:05:03 +0000 Subject: [PATCH] feat(sandokan): CLI de prueba + fix wire serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/sandokan (binario `sandokan`): CLI para probar el orquestador. Subcomandos: daemon, run [args], list, status, telemetry, stop. Fix: Intent serializaba Card directo, pero Card tiene un campo `#[serde(flatten)] extensions` incompatible con postcard ("sequence length must be known"). Intent::card ahora usa #[serde(with)] que proyecta Card↔WireCard en el límite de serialización (las extensions locales se descartan al cruzar el wire — comportamiento correcto). Smoke test verificado end-to-end: daemon + run /bin/sleep + list + status Running + telemetry + stop + status Killed. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 12 ++ Cargo.toml | 1 + crates/apps/sandokan/Cargo.toml | 20 ++++ crates/apps/sandokan/src/main.rs | 125 +++++++++++++++++++++ crates/runtime/sandokan-core/src/intent.rs | 19 ++++ nohup.out | 5 + 6 files changed, 182 insertions(+) create mode 100644 crates/apps/sandokan/Cargo.toml create mode 100644 crates/apps/sandokan/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5ea7e91..042b4a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10139,6 +10139,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "sandokan-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "brahman-card", + "clap", + "sandokan", + "tokio", + "ulid", +] + [[package]] name = "sandokan-core" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6798717..14a3afd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,6 +177,7 @@ members = [ # ============================================================ "crates/apps/brahman-broker-explorer", "crates/apps/brahman-demo", + "crates/apps/sandokan", "crates/apps/nahual-shell", "crates/apps/nahual-file-explorer", "crates/apps/nahual-database-explorer", diff --git a/crates/apps/sandokan/Cargo.toml b/crates/apps/sandokan/Cargo.toml new file mode 100644 index 0000000..4973a95 --- /dev/null +++ b/crates/apps/sandokan/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "sandokan-cli" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "CLI de prueba del orquestador sandokan: daemon + run/list/status/stop/telemetry." + +[[bin]] +name = "sandokan" +path = "src/main.rs" + +[dependencies] +sandokan = { path = "../../runtime/sandokan" } +brahman-card = { path = "../../protocol/brahman-card" } +clap = { workspace = true } +tokio = { workspace = true } +ulid = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/apps/sandokan/src/main.rs b/crates/apps/sandokan/src/main.rs new file mode 100644 index 0000000..961e19e --- /dev/null +++ b/crates/apps/sandokan/src/main.rs @@ -0,0 +1,125 @@ +//! `sandokan` — CLI de prueba del orquestador. +//! +//! Uso típico (dos terminales): +//! terminal 1: sandokan daemon +//! terminal 2: sandokan run /bin/sleep 300 +//! sandokan list +//! sandokan status +//! sandokan stop +//! +//! Sin daemon, `run` igual encarna el proceso, pero el registro vive en +//! el proceso del CLI y se pierde al salir — `list` no lo verá. Para +//! probar el lifecycle completo, corré el daemon primero. + +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use brahman_card::{Card, Payload}; +use clap::{Parser, Subcommand}; +use sandokan::{auto, default_socket_path, serve, Intent, LocalEngine}; +use ulid::Ulid; + +#[derive(Parser)] +#[command(name = "sandokan", about = "Orquestador brahman — CLI de prueba")] +struct Cli { + /// Socket del daemon (default: $XDG_RUNTIME_DIR/sandokan.sock). + #[arg(long, global = true)] + socket: Option, + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand)] +enum Cmd { + /// Corre el daemon: sirve un LocalEngine sobre el socket. + Daemon, + /// Encarna un ejecutable como Card y lo orquesta. + Run { + /// Ruta del ejecutable. + exec: String, + /// Argumentos del ejecutable. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Lista las entidades activas. + List, + /// Estado de una entidad. + Status { card_id: String }, + /// Telemetría puntual de una entidad. + Telemetry { card_id: String }, + /// Detiene una entidad (SIGTERM + gracia + SIGKILL). + Stop { + card_id: String, + #[arg(long, default_value = "1000")] + grace_ms: u64, + }, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + let socket = cli.socket.unwrap_or_else(default_socket_path); + + match cli.cmd { + Cmd::Daemon => { + if let Some(parent) = socket.parent() { + let _ = std::fs::create_dir_all(parent); + } + println!("sandokan-daemon escuchando en {}", socket.display()); + println!("(Ctrl-C para salir)"); + let engine = Arc::new(LocalEngine::new()); + serve(engine, &socket).await?; + } + Cmd::Run { exec, args } => { + let mut card = Card::new(format!("run:{exec}")); + card.payload = Payload::Native { + exec, + argv: args, + envp: vec![], + }; + let engine = auto(&socket).await; + let handle = engine.run(Intent::new(card)).await?; + println!("encarnado:"); + println!(" card_id : {}", handle.card_id); + println!(" label : {}", handle.label); + } + Cmd::List => { + let engine = auto(&socket).await; + let list = engine.list().await?; + if list.is_empty() { + println!("(sin entidades activas)"); + } + for h in list { + println!("{} {}", h.card_id, h.label); + } + } + Cmd::Status { card_id } => { + let engine = auto(&socket).await; + let state = engine.status(parse_id(&card_id)?).await?; + println!("{state:?}"); + } + Cmd::Telemetry { card_id } => { + let engine = auto(&socket).await; + let t = engine.telemetry(parse_id(&card_id)?).await?; + println!( + "mem={} KiB nproc={} cpu={:.1}%", + t.mem_bytes / 1024, + t.nproc, + t.cpu_pct + ); + } + Cmd::Stop { card_id, grace_ms } => { + let engine = auto(&socket).await; + engine + .stop(parse_id(&card_id)?, Duration::from_millis(grace_ms)) + .await?; + println!("detenido"); + } + } + Ok(()) +} + +fn parse_id(s: &str) -> anyhow::Result { + Ulid::from_string(s).map_err(|e| anyhow::anyhow!("card-id inválido `{s}`: {e}")) +} diff --git a/crates/runtime/sandokan-core/src/intent.rs b/crates/runtime/sandokan-core/src/intent.rs index 7151af0..2fb278a 100644 --- a/crates/runtime/sandokan-core/src/intent.rs +++ b/crates/runtime/sandokan-core/src/intent.rs @@ -32,11 +32,30 @@ pub struct ExecContext { /// Una intención de ejecución: la `Card` a encarnar + su contexto. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Intent { + /// La Card se serializa vía `WireCard` (proyección postcard-friendly): + /// el campo `extensions` de `Card` usa `#[serde(flatten)]`, que no es + /// compatible con formatos no auto-descriptivos como postcard. + #[serde(with = "card_wire")] pub card: Card, #[serde(default)] pub context: ExecContext, } +/// Serde adapter: `Card` ↔ `WireCard` en el límite de serialización. +/// Las `extensions` locales de la Card se descartan al cruzar el wire. +mod card_wire { + use brahman_card::{Card, WireCard}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(card: &Card, s: S) -> Result { + WireCard::from(card.clone()).serialize(s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + Ok(Card::from(WireCard::deserialize(d)?)) + } +} + impl Intent { /// Intención mínima: encarnar una Card con el contexto por defecto. pub fn new(card: Card) -> Self { diff --git a/nohup.out b/nohup.out index 23dff26..fcc1e82 100644 --- a/nohup.out +++ b/nohup.out @@ -178,3 +178,8 @@ WARNING: Restricted methods will be blocked in a future release unless native ac (thunar:793578): thunar-WARNING **: 18:49:46.732: ThunarThumbnailer: failed to create proxy: Cannot autolaunch D-Bus without X11 $DISPLAY Gdk-Message: 05:42:34.451: Error reading events from display: Broken pipe +[Parent 2593, Main Thread] WARNING: Failed to create DBus proxy for org.a11y.Bus: Cannot autolaunch D-Bus without X11 $DISPLAY +: 'glib warning', file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/toolkit/xre/nsSigHandlers.cpp:201 + +** (zen:2593): WARNING **: 15:00:19.012: Failed to create DBus proxy for org.a11y.Bus: Cannot autolaunch D-Bus without X11 $DISPLAY +