tawasuyu

tawasuyu/brahman

Rust 521TOML 202Markdown 57JSON 197505 Definiciones

feat(sandokan): B1.5 — umbrella + Engine::auto()

Crate sandokan (umbrella): re-exporta core/local/daemon y provee la
selección de transporte.

- auto(socket) — patrón "el primero que arranca gana": prueba si hay
  un daemon escuchando; si lo hay devuelve DaemonEngine, si no
  LocalEngine. Box<dyn Engine> (el trait es object-safe vía async_trait).
- auto_default() — auto() con default_socket_path().
- default_socket_path() — $XDG_RUNTIME_DIR/sandokan.sock o
  /run/brahman/sandokan.sock.

3 tests: fallback a Local sin daemon, pick Daemon con serve() activo,
default path absoluto. cargo check --workspace verde.

sandokan ya es usable end-to-end en modo local y daemon. Falta
RemoteEngine (B1.4, depende de brahman-ssh-multiplex).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

8cd8003dd5fa3de2409835f819886b876b49967f · por sergio · (hace 5 meses) · padre b7d9d7abd · ver el árbol en este commit

Delta semántico · 7

crates/runtime/sandokan/src/lib.rs

4 archivos · +128 −0

diff --git a/Cargo.lock b/Cargo.lock
index 6a0aeff..5ea7e91 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10127,6 +10127,18 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "sandokan"
+version = "0.1.0"
+dependencies = [
+ "sandokan-core",
+ "sandokan-daemon",
+ "sandokan-lifecycle",
+ "sandokan-local",
+ "tempfile",
+ "tokio",
+]
+
 [[package]]
 name = "sandokan-core"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index cfb8ffe..6798717 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,7 @@ members = [
     "crates/runtime/sandokan-core",
     "crates/runtime/sandokan-local",
     "crates/runtime/sandokan-daemon",
+    "crates/runtime/sandokan",
 
     # ============================================================
     # compat/ — Shims D-Bus para correr software systemd-aware
diff --git a/crates/runtime/sandokan/Cargo.toml b/crates/runtime/sandokan/Cargo.toml
new file mode 100644
index 0000000..66786bd
--- /dev/null
+++ b/crates/runtime/sandokan/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "sandokan"
+description = "Orquestador sandokan (umbrella): re-exporta core/local/daemon + Engine::auto() que elige transporte según haya un daemon escuchando."
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+
+[dependencies]
+sandokan-core = { path = "../sandokan-core" }
+sandokan-lifecycle = { path = "../sandokan-lifecycle" }
+sandokan-local = { path = "../sandokan-local" }
+sandokan-daemon = { path = "../sandokan-daemon" }
+tokio = { workspace = true }
+
+[dev-dependencies]
+tempfile = { workspace = true }
diff --git a/crates/runtime/sandokan/src/lib.rs b/crates/runtime/sandokan/src/lib.rs
new file mode 100644
index 0000000..afd72f5
--- /dev/null
+++ b/crates/runtime/sandokan/src/lib.rs
@@ -0,0 +1,97 @@
+//! sandokan — el orquestador del ecosistema brahman (umbrella).
+//!
+//! `sandokan` es una **library horizontal embebible**, no un daemon
+//! supremo. Cualquier binario lo embebe y elige cómo correrlo:
+//!
+//! - [`LocalEngine`] — orquesta in-process (encarna Cards localmente).
+//! - [`DaemonEngine`] — delega a otro proceso vía Unix socket.
+//! - `RemoteEngine` — delega a otro host vía SSH (crate `sandokan-remote`).
+//!
+//! [`auto`] implementa el patrón "el primero que arranca gana": prueba
+//! si hay un daemon escuchando y, si lo hay, se le suma como
+//! `DaemonEngine`; si no, corre su propio `LocalEngine`.
+
+pub use sandokan_core::{
+    Engine, EngineError, ExecContext, ExecHandle, Intent, IsolationLevel,
+    LifecycleEvent, TelemetryFrame,
+};
+pub use sandokan_daemon::{serve, DaemonEngine, DaemonRequest, DaemonResponse};
+pub use sandokan_local::LocalEngine;
+
+/// Re-export de las primitivas de lifecycle.
+pub use sandokan_lifecycle as lifecycle;
+
+use std::path::{Path, PathBuf};
+
+/// Path por defecto del socket del daemon sandokan.
+///
+/// `$XDG_RUNTIME_DIR/sandokan.sock` si la variable está; si no,
+/// `/run/brahman/sandokan.sock`.
+pub fn default_socket_path() -> PathBuf {
+    match std::env::var_os("XDG_RUNTIME_DIR") {
+        Some(rt) => PathBuf::from(rt).join("sandokan.sock"),
+        None => PathBuf::from("/run/brahman/sandokan.sock"),
+    }
+}
+
+/// Elige el engine según el entorno: si hay un daemon escuchando en
+/// `socket_path`, devuelve un [`DaemonEngine`] (delega); si no, un
+/// [`LocalEngine`] (orquesta in-process).
+pub async fn auto(socket_path: &Path) -> Box<dyn Engine> {
+    let daemon = DaemonEngine::new(socket_path);
+    if daemon.is_reachable().await {
+        Box::new(daemon)
+    } else {
+        Box::new(LocalEngine::new())
+    }
+}
+
+/// [`auto`] con [`default_socket_path`].
+pub async fn auto_default() -> Box<dyn Engine> {
+    auto(&default_socket_path()).await
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::sync::Arc;
+    use std::time::Duration;
+
+    #[tokio::test]
+    async fn auto_falls_back_to_local_without_daemon() {
+        // Socket inexistente → debe caer a LocalEngine (list() vacío).
+        let dir = tempfile::tempdir().unwrap();
+        let sock = dir.path().join("nope.sock");
+        let engine = auto(&sock).await;
+        assert!(engine.list().await.unwrap().is_empty());
+    }
+
+    #[tokio::test]
+    async fn auto_picks_daemon_when_listening() {
+        let dir = tempfile::tempdir().unwrap();
+        let sock = dir.path().join("sandokan.sock");
+
+        let served = Arc::new(LocalEngine::new());
+        let sock_srv = sock.clone();
+        let srv = tokio::spawn(async move {
+            let _ = serve(served, &sock_srv).await;
+        });
+        for _ in 0..50 {
+            if sock.exists() {
+                break;
+            }
+            tokio::time::sleep(Duration::from_millis(10)).await;
+        }
+
+        // Con el daemon escuchando, auto() debe conectar y operar vía wire.
+        let engine = auto(&sock).await;
+        assert!(engine.list().await.unwrap().is_empty());
+
+        srv.abort();
+    }
+
+    #[test]
+    fn default_socket_path_is_absolute() {
+        assert!(default_socket_path().is_absolute());
+    }
+}