//! `hammerd` — daemon de hammer. Tres responsabilidades: //! 1. Diario de mutaciones: fanotify sobre /bin,/sbin,/lib,/etc → `hammer-journal`. //! 2. FIFO de control humano: `/run/init.control` (texto crudo, una línea por comando). //! 3. Bus de agente: `/run/agent.sock` (JSON-líneas, SO_PEERCRED). Ver `docs/07-agent-bus.md`. //! //! Cada subsistema vive en su thread. Si uno falla en init (típicamente el watcher sin //! CAP_SYS_ADMIN), los demás siguen arrancando: el daemon en dev puede correr sin sudo y aún //! así probar el bus. use std::path::PathBuf; use std::thread; use clap::Parser; mod arje_link; mod bus; mod control; mod crashes; mod events; mod watcher; #[derive(Parser)] #[command(name = "hammerd", version, about = "Daemon de hammer: bus de agente + diario.")] struct Args { /// Socket del bus de agente. #[arg(long, default_value = "/run/agent.sock")] agent_sock: String, /// FIFO de control humano del init. #[arg(long, default_value = "/run/init.control")] init_control: PathBuf, /// Directorio del diario de mutaciones. #[arg(long, default_value = "/var/lib/hammer/journal")] journal: String, /// Directorio raíz de overlays (para que el watcher pueda filtrar eventos bajo overlays /// activos). Default coincide con `hammer try`. #[arg(long, default_value = hammer_overlay::DEFAULT_STATE_ROOT)] overlay_state_root: String, /// Directorios extra a vigilar; si vacío, usa los defaults del FHS (SDD 05 §2). #[arg(long = "watch")] extra_watch: Vec, /// Store CAS donde sellar artefactos de COMPILE. #[arg(long, default_value = "/store")] store: PathBuf, /// Modo dev: desactiva el watcher fanotify (útil cuando hammerd corre sin CAP_SYS_ADMIN /// y sólo queremos probar el bus). Igual que arrancar sin permisos, pero explícito. #[arg(long)] no_watcher: bool, /// Fichero de política de capacidades del bus. Si existe, sustituye a la política por /// defecto (misma-UID ⇒ casi todo; otros ⇒ query). Ver `docs/07-agent-bus.md` §4. #[arg(long, default_value = "/etc/hammer/agent-caps.toml")] agent_caps: PathBuf, } fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info".into()), ) .init(); let args = Args::parse(); tracing::info!( agent_sock = %args.agent_sock, init_control = %args.init_control.display(), journal = %args.journal, store = %args.store.display(), "hammerd: arranque" ); let event_bus = events::EventBus::new(); // Puente con el bus del init (arje): si ENTE_BUS_SOCK está definido, se suscribe a los // eventos de ciclo de vida y publica los crashes en el bus de agente. No-op si no hay init. arje_link::spawn_if_configured(event_bus.clone()); // FIFO de control humano: lo creamos siempre que se pueda (no es fatal). let init_fifo_ok = match control::ensure_fifo(&args.init_control) { Ok(path) => { let reader_path = path.clone(); thread::Builder::new() .name("init-ctl-reader".into()) .spawn(move || { if let Err(e) = control::run_reader(&reader_path) { tracing::warn!(error = %e, "init-ctl-reader: terminó"); } }) .ok(); true } Err(e) => { tracing::warn!(error = %e, "init-control: no pude crear FIFO; INIT del bus fallará"); false } }; // Watcher fanotify: opcional. Si falla (no caps), el daemon sigue con el bus solo. if !args.no_watcher { let journal = hammer_journal::Journal::open(&args.journal)?; let mut dirs = watcher::default_watch_dirs(); dirs.extend(args.extra_watch.clone()); let overlay_root = PathBuf::from(&args.overlay_state_root); let bus_for_watcher = event_bus.clone(); match watcher::Watcher::start_with_events( &dirs, journal, Some(overlay_root), Some(bus_for_watcher), ) { Ok(w) => { tracing::info!("watcher fanotify activo"); thread::Builder::new() .name("watcher".into()) .spawn(move || { if let Err(e) = w.run_forever() { tracing::warn!(error = %e, "watcher: terminó con error"); } }) .ok(); } Err(e) => { tracing::warn!( error = %e, "watcher fanotify NO arrancó; daemon sigue (sin diario en este run)" ); } } } else { tracing::info!("--no-watcher: omitiendo fanotify"); } // Bus de agente: bloqueante en este thread. Si falla, fin del daemon. // Política de caps: del fichero si existe, si no la heurística por-UID por defecto. let policy = match hammer_core::AgentCapsConfig::load(&args.agent_caps) { Ok(Some(cfg)) => { tracing::info!(path = %args.agent_caps.display(), rules = cfg.rule.len(), "bus: política de caps cargada de fichero"); bus::policy_from_config(cfg) } Ok(None) => { tracing::info!(path = %args.agent_caps.display(), "bus: sin fichero de caps; uso la política por defecto (por-UID)"); bus::default_policy() } Err(e) => { // Config presente pero inválida: fallar cerrado sería más seguro, pero romper el // arranque del daemon por un typo es peor en la práctica. Avisamos fuerte y caemos // a la política por defecto (restrictiva para no-dueños). tracing::error!(path = %args.agent_caps.display(), error = %e, "bus: fichero de caps inválido; uso la política por defecto"); bus::default_policy() } }; let ctx = bus::BusContext { store_root: args.store, init_control: if init_fifo_ok { args.init_control } else { // Apuntamos a un path inexistente: cualquier INIT fallará con "init_send_failed" // y un mensaje claro. Mejor que callar. PathBuf::from("/dev/null/init-control-missing") }, events: event_bus, }; bus::serve_agent_bus(std::path::Path::new(&args.agent_sock), policy, ctx)?; Ok(()) }