This commit is contained in:
sergio
2026-05-10 21:58:16 +00:00
parent 3d55f189c0
commit c22d2480b9
36 changed files with 5158 additions and 363 deletions
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "shipote-daemon"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "Daemon de shipote: dueño de los Workspaces, expone admin socket para shipote-cli."
[[bin]]
name = "shipote-daemon"
path = "src/main.rs"
[dependencies]
shipote-card = { path = "../../modules/shipote/shipote-card" }
shipote-protocol = { path = "../../modules/shipote/shipote-protocol" }
shipote-discern = { path = "../../modules/shipote/shipote-discern" }
shipote-core = { path = "../../modules/shipote/shipote-core" }
ente-incarnate = { path = "../../shared/ente-incarnate" }
brahman-card = { path = "../../core/brahman-card" }
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
anyhow = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
ulid = { workspace = true }
nix = { workspace = true }
+361
View File
@@ -0,0 +1,361 @@
//! `shipote-daemon` — punto de entrada del runtime de shipote.
//!
//! Responsabilidades:
//! - Escuchar el Unix socket admin (default: `$XDG_RUNTIME_DIR/shipote.sock`).
//! - Despachar mensajes del [`shipote_protocol`] al [`WorkspaceManager`].
//! - Reapear hijos periódicamente.
//!
//! Lo que NO hace en v1:
//! - Sidecar al broker / handshake con Init (futuro: cuando un workspace
//! exponga `service_socket`, anunciar al broker).
//! - GUI (futuro `shipote-shell` con yahweh_launcher).
use anyhow::Context;
use brahman_card::{Card, CardKind, Flow, Flows, Lifecycle, Payload, Supervision, TypeRef};
use ente_incarnate::IncarnatorConfig;
use shipote_core::WorkspaceManager;
use shipote_discern::{DiscernPipeline, Hint};
use shipote_protocol::{
default_socket_path, read_frame, write_frame, CommandInfo as ProtoCommandInfo,
EdgeDiscernmentInfo, Request, Response, WorkspaceSummary,
};
use std::sync::Arc;
use tokio::net::{UnixListener, UnixStream};
use tracing::{error, info, warn};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
init_tracing();
let sock = default_socket_path();
if sock.exists() {
// Si ya existe, asumimos restart limpio. Si hubiera otro daemon vivo,
// bind fallaría con EADDRINUSE — más adelante: lockfile + check de PID.
let _ = std::fs::remove_file(&sock);
}
if let Some(parent) = sock.parent() {
let _ = std::fs::create_dir_all(parent);
}
let listener = UnixListener::bind(&sock).with_context(|| format!("bind {}", sock.display()))?;
info!(socket = %sock.display(), "shipote-daemon listening");
// Sidecar al broker: shipote se anuncia como sesión. Si el Init no
// está corriendo, el sidecar loguea y termina; el daemon sigue
// standalone (UX de v1: ningún feature requiere broker).
brahman_sidecar::spawn(build_daemon_card(&sock));
let mgr = Arc::new(WorkspaceManager::new(IncarnatorConfig {
// El daemon aún no se conecta al broker; cuando lo haga, este path
// se llenará desde el handshake.
bus_sock: None,
notify_socket: None,
extra_env: vec![("SHIPOTE_DAEMON".into(), "1".into())],
// strict_caps=false en v1: queremos UX permisiva (correr en non-root
// sin user_ns y avisar via warnings, no abortar).
strict_caps: false,
}));
// Restaurar snapshot previo si existe. Workspaces se recrean; los
// pids de comandos viejos NO se recuperan (kernel los mató).
let snapshot_path = shipote_core::persist::default_snapshot_path();
if let Err(e) = mgr.restore_snapshot(&snapshot_path).await {
warn!(?e, "restore_snapshot falló — start fresh");
}
// Save-on-shutdown via SIGTERM/SIGINT handler. tokio::signal soporta
// ambos en Linux.
{
let mgr = mgr.clone();
let path = snapshot_path.clone();
tokio::spawn(async move {
let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("SIGTERM handler");
let mut int = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
.expect("SIGINT handler");
tokio::select! {
_ = term.recv() => info!("SIGTERM — saving snapshot"),
_ = int.recv() => info!("SIGINT — saving snapshot"),
}
if let Err(e) = mgr.save_snapshot(&path).await {
warn!(?e, "save_snapshot falló");
}
std::process::exit(0);
});
}
let discerner = Arc::new(DiscernPipeline::default_pipeline());
// Reaper periódico cada 500 ms.
{
let mgr = mgr.clone();
tokio::spawn(async move {
let mut tick = tokio::time::interval(std::time::Duration::from_millis(500));
loop {
tick.tick().await;
mgr.reap_dead().await;
}
});
}
loop {
match listener.accept().await {
Ok((stream, _)) => {
let mgr = mgr.clone();
let disc = discerner.clone();
tokio::spawn(async move {
if let Err(e) = handle_client(stream, mgr, disc).await {
warn!(?e, "client handler error");
}
});
}
Err(e) => {
error!(?e, "accept failed");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
}
}
async fn handle_client(
mut stream: UnixStream,
mgr: Arc<WorkspaceManager>,
disc: Arc<DiscernPipeline>,
) -> anyhow::Result<()> {
loop {
let req: Request = match read_frame(&mut stream).await {
Ok(r) => r,
Err(shipote_protocol::ProtocolError::Closed) => return Ok(()),
Err(e) => return Err(e.into()),
};
let resp = dispatch(&mgr, &disc, req).await;
write_frame(&mut stream, &resp).await?;
}
}
async fn dispatch(mgr: &Arc<WorkspaceManager>, disc: &DiscernPipeline, req: Request) -> Response {
match req {
Request::Ping => Response::Pong,
Request::WorkspaceCreate { spec } => match mgr.create(spec).await {
Ok((id, warnings)) => Response::WorkspaceCreated { id, warnings },
Err(e) => Response::Error { message: format!("{e}") },
},
Request::WorkspaceList => {
let items = mgr
.list()
.await
.into_iter()
.map(|s| WorkspaceSummary {
id: s.id,
label: s.label,
commands: s.commands,
uptime_ms: s.uptime_ms,
})
.collect();
Response::WorkspaceList { items }
}
Request::WorkspaceStop { id } => match mgr.stop(id).await {
Ok(reaped) => Response::WorkspaceStopped { id, reaped },
Err(e) => Response::Error { message: format!("{e}") },
},
Request::Run { workspace, exec, argv, envp } => {
match mgr.run(workspace, exec, argv, envp).await {
Ok(s) => Response::RunStarted {
workspace,
command_id: s.id,
pid: s.pid,
},
Err(e) => Response::Error { message: format!("{e}") },
}
}
Request::PipelineRun { spec, tap } => {
let disc = DiscernPipeline::default_pipeline();
let inc = mgr.incarnator_handle();
let ws_label = mgr.workspace_label(spec.workspace).await.unwrap_or_default();
match shipote_core::pipeline::run_pipeline(
&spec,
&ws_label,
tap,
std::sync::Arc::new(disc),
inc,
)
.await
{
Ok(launch) => Response::PipelineStarted {
pipeline: launch.pipeline,
command_pids: launch.command_pids,
edges: launch
.edge_discernments
.into_iter()
.map(|e| EdgeDiscernmentInfo {
from_label: e.from_label,
from_output: e.from_output,
to_label: e.to_label,
to_input: e.to_input,
ty: e.discernment.as_ref().map(|d| format!("{:?}", d.ty)),
mime: e.discernment.as_ref().and_then(|d| d.mime.clone()),
lens: e.discernment.as_ref().and_then(|d| d.lens.clone()),
confidence: e.discernment.as_ref().map(|d| d.confidence).unwrap_or(0.0),
})
.collect(),
},
Err(e) => Response::Error { message: format!("{e}") },
}
}
Request::Discern { sample, hint_path } => {
let path_str = hint_path.as_ref().and_then(|p| p.to_str());
let hint = Hint {
path: path_str,
size_total: None,
};
match disc.discern(&sample, &hint) {
Some(d) => Response::Discernment {
ty: format!("{:?}", d.ty),
confidence: d.confidence,
mime: d.mime,
lens: d.lens,
},
None => Response::Error { message: "no discernment".into() },
}
}
Request::CommandList { workspace } => {
let items: Vec<ProtoCommandInfo> = mgr
.list_commands(workspace)
.await
.into_iter()
.map(|c| ProtoCommandInfo {
id: c.id,
label: c.label,
pid: c.pid,
alive: c.alive,
exit_status: c.exit_status,
log_bytes: c.log_bytes,
})
.collect();
Response::CommandList { items }
}
Request::CommandLogs { workspace, command, tail_bytes } => {
match mgr.get_command_logs(workspace, command, tail_bytes).await {
Some(bytes) => Response::CommandLogs { bytes },
None => Response::Error {
message: format!("no logs for command {command} in workspace {workspace}"),
},
}
}
Request::PipelineSave { name, spec } => {
mgr.save_pipeline(name.clone(), spec).await;
Response::PipelineSaved { name }
}
Request::PipelineSavedList => {
let names = mgr.list_saved_pipelines().await;
Response::PipelineSavedList { names }
}
Request::PipelineDrop { name } => {
let existed = mgr.drop_saved_pipeline(&name).await;
Response::PipelineDropped { name, existed }
}
Request::PipelineRunSaved { name, tap } => match mgr.get_saved_pipeline(&name).await {
Some(spec) => {
let disc = DiscernPipeline::default_pipeline();
let inc = mgr.incarnator_handle();
let ws_label = mgr.workspace_label(spec.workspace).await.unwrap_or_default();
match shipote_core::pipeline::run_pipeline(
&spec,
&ws_label,
tap,
std::sync::Arc::new(disc),
inc,
)
.await
{
Ok(launch) => Response::PipelineStarted {
pipeline: launch.pipeline,
command_pids: launch.command_pids,
edges: launch
.edge_discernments
.into_iter()
.map(|e| EdgeDiscernmentInfo {
from_label: e.from_label,
from_output: e.from_output,
to_label: e.to_label,
to_input: e.to_input,
ty: e.discernment.as_ref().map(|d| format!("{:?}", d.ty)),
mime: e.discernment.as_ref().and_then(|d| d.mime.clone()),
lens: e.discernment.as_ref().and_then(|d| d.lens.clone()),
confidence: e.discernment.as_ref().map(|d| d.confidence).unwrap_or(0.0),
})
.collect(),
},
Err(e) => Response::Error { message: format!("{e}") },
}
}
None => Response::Error {
message: format!("pipeline `{name}` no encontrado"),
},
},
Request::Capabilities => {
let c = mgr.incarnator().capabilities();
Response::Capabilities {
kernel_version: c.kernel_version,
user_ns: format!("{:?}", c.user_ns),
cgroup_v2: format!("{:?}", c.cgroup_v2),
cgroup_delegated: c.cgroup_delegated,
has_cap_sys_admin: c.has_cap_sys_admin,
}
}
}
}
/// Card del daemon. La presentamos al broker así otras sesiones pueden
/// descubrir que shipote está corriendo y, eventualmente, conectarse
/// como consumidoras del flow `workspaces` (futuro: que la GUI o el
/// broker-explorer los listen vía broker en lugar de socket directo).
fn build_daemon_card(service_socket: &std::path::Path) -> Card {
let mut card = Card::new("shipote.daemon");
card.kind = CardKind::Ente;
card.lifecycle = Lifecycle::Daemon;
card.payload = Payload::Virtual; // el daemon ya está corriendo (no es PID 1 quien lo encarna)
card.supervision = Supervision::Delegate;
card.service_socket = Some(service_socket.to_path_buf());
card.flow = Flows {
input: Vec::new(),
output: vec![
Flow {
name: "workspaces".into(),
ty: TypeRef::Wit {
package: "shipote:admin".into(),
interface: None,
name: "workspace-list".into(),
},
pin_to: None,
},
Flow {
name: "discern".into(),
ty: TypeRef::Wit {
package: "shipote:admin".into(),
interface: None,
name: "discernment".into(),
},
pin_to: None,
},
],
};
card
}
fn init_tracing() {
use tracing_subscriber::{fmt, EnvFilter};
let filter = EnvFilter::try_from_env("SHIPOTE_LOG").unwrap_or_else(|_| EnvFilter::new("info"));
fmt().with_env_filter(filter).init();
}