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>
This commit is contained in:
Generated
+12
@@ -10127,6 +10127,18 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sandokan"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"sandokan-core",
|
||||||
|
"sandokan-daemon",
|
||||||
|
"sandokan-lifecycle",
|
||||||
|
"sandokan-local",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sandokan-core"
|
name = "sandokan-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ members = [
|
|||||||
"crates/runtime/sandokan-core",
|
"crates/runtime/sandokan-core",
|
||||||
"crates/runtime/sandokan-local",
|
"crates/runtime/sandokan-local",
|
||||||
"crates/runtime/sandokan-daemon",
|
"crates/runtime/sandokan-daemon",
|
||||||
|
"crates/runtime/sandokan",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# compat/ — Shims D-Bus para correr software systemd-aware
|
# compat/ — Shims D-Bus para correr software systemd-aware
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user