tawasuyu

tawasuyu/brahman

Rust 321TOML 122JSON 14Markdown 114332 Definitions

feat(sandokan): CLI de prueba + fix wire serialization

apps/sandokan (binario `sandokan`): CLI para probar el orquestador.
Subcomandos: daemon, run <exec> [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 <noreply@anthropic.com>

94ea0eaa539cf93d58167ea1687b5a242b196573 · by sergio · (5 months ago) · parent 590572b5b · browse the tree at this commit

Semantic delta · 8

crates/apps/sandokan/src/main.rs

crates/runtime/sandokan-core/src/intent.rs

6 files · +182 −0

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 <card-id>
+//!                sandokan stop <card-id>
+//!
+//! 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<PathBuf>,
+    #[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<String>,
+    },
+    /// 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> {
+    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<S: Serializer>(card: &Card, s: S) -> Result<S::Ok, S::Error> {
+        WireCard::from(card.clone()).serialize(s)
+    }
+
+    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Card, D::Error> {
+        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
+