gioser

tawasuyu/brahman

Rust 467TOML 184Markdown 49JSON 176411 Sutichaykuna

⎇ main crates/runtime/sandokan/src/lib.rs Rust · 98 siq'ikuna · 3.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(sandokan-remote): B1.4 — RemoteEngine vía SSH socket-forward 05886022e · sergio ·

1//! sandokan — el orquestador del ecosistema brahman (umbrella).
2//!
3//! `sandokan` es una **library horizontal embebible**, no un daemon
4//! supremo. Cualquier binario lo embebe y elige cómo correrlo:
5//!
6//! - [`LocalEngine`] — orquesta in-process (encarna Cards localmente).
7//! - [`DaemonEngine`] — delega a otro proceso vía Unix socket.
8//! - `RemoteEngine` — delega a otro host vía SSH (crate `sandokan-remote`).
9//!
10//! [`auto`] implementa el patrón "el primero que arranca gana": prueba
11//! si hay un daemon escuchando y, si lo hay, se le suma como
12//! `DaemonEngine`; si no, corre su propio `LocalEngine`.
13
14pub use sandokan_core::{
15    Engine, EngineError, ExecContext, ExecHandle, Intent, IsolationLevel,
16    LifecycleEvent, TelemetryFrame,
17};
18pub use sandokan_daemon::{serve, DaemonEngine, DaemonRequest, DaemonResponse};
19pub use sandokan_local::LocalEngine;
20pub use sandokan_remote::RemoteEngine;
21
22/// Re-export de las primitivas de lifecycle.
23pub use sandokan_lifecycle as lifecycle;
24
25use std::path::{Path, PathBuf};
26
27/// Path por defecto del socket del daemon sandokan.
28///
29/// `$XDG_RUNTIME_DIR/sandokan.sock` si la variable está; si no,
30/// `/run/brahman/sandokan.sock`.
31pub fn default_socket_path() -> PathBuf {
32    match std::env::var_os("XDG_RUNTIME_DIR") {
33        Some(rt) => PathBuf::from(rt).join("sandokan.sock"),
34        None => PathBuf::from("/run/brahman/sandokan.sock"),
35    }
36}
37
38/// Elige el engine según el entorno: si hay un daemon escuchando en
39/// `socket_path`, devuelve un [`DaemonEngine`] (delega); si no, un
40/// [`LocalEngine`] (orquesta in-process).
41pub async fn auto(socket_path: &Path) -> Box<dyn Engine> {
42    let daemon = DaemonEngine::new(socket_path);
43    if daemon.is_reachable().await {
44        Box::new(daemon)
45    } else {
46        Box::new(LocalEngine::new())
47    }
48}
49
50/// [`auto`] con [`default_socket_path`].
51pub async fn auto_default() -> Box<dyn Engine> {
52    auto(&default_socket_path()).await
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use std::sync::Arc;
59    use std::time::Duration;
60
61    #[tokio::test]
62    async fn auto_falls_back_to_local_without_daemon() {
63        // Socket inexistente → debe caer a LocalEngine (list() vacío).
64        let dir = tempfile::tempdir().unwrap();
65        let sock = dir.path().join("nope.sock");
66        let engine = auto(&sock).await;
67        assert!(engine.list().await.unwrap().is_empty());
68    }
69
70    #[tokio::test]
71    async fn auto_picks_daemon_when_listening() {
72        let dir = tempfile::tempdir().unwrap();
73        let sock = dir.path().join("sandokan.sock");
74
75        let served = Arc::new(LocalEngine::new());
76        let sock_srv = sock.clone();
77        let srv = tokio::spawn(async move {
78            let _ = serve(served, &sock_srv).await;
79        });
80        for _ in 0..50 {
81            if sock.exists() {
82                break;
83            }
84            tokio::time::sleep(Duration::from_millis(10)).await;
85        }
86
87        // Con el daemon escuchando, auto() debe conectar y operar vía wire.
88        let engine = auto(&sock).await;
89        assert!(engine.list().await.unwrap().is_empty());
90
91        srv.abort();
92    }
93
94    #[test]
95    fn default_socket_path_is_absolute() {
96        assert!(default_socket_path().is_absolute());
97    }
98}