shell
This commit is contained in:
@@ -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 }
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user