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 { + 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 { + 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()); + } +}