//! `brokerd`: the broker role. `serve` takes the audit lock, binds both sockets, and serves. use std::ffi::OsString; use std::path::PathBuf; use std::process::ExitCode; use std::sync::Arc; use brokerd::audit::{AuditError, RECOVERED_NOTICE}; use brokerd::config::Config; use brokerd::container::Podman; use brokerd::memory_port::{MEMORY_WAIT, MemoryPort, Routed}; use brokerd::runner::{Refusing, Runtime}; use brokerd::serve::{self, ServeError}; use bxcore::instance; const USAGE: &str = "usage: brokerd serve --config [--accept-break]"; /// Where the owner looks when brokerd cannot start: its config, a directory or a socket. const START_FAILED: &str = "see docs/runbook.md#brokerd-start-failed"; /// Where the owner looks when brokerd stops serving after it started. const LISTENER_LOST: &str = "see docs/runbook.md#brokerd-listener-lost"; fn main() -> ExitCode { // The first argument must be `serve`. // `args_os`: a path need not be UTF-8, and `args` would panic on one that is not. let mut args = std::env::args_os().skip(1); match args.next() { Some(command) if command == "serve" => {} _ => return usage(), } // `--config ` exactly once and `--accept-break` at most once, in any order. let (config_path, accept_break) = match parse_serve(args) { Ok(pair) => pair, Err(()) => return usage(), }; // Read the config before touching the filesystem: a bad config must make nothing. let cfg = match Config::load(&config_path) { Ok(cfg) => cfg, Err(e) => { eprintln!("brokerd: {e}\n{START_FAILED}"); return ExitCode::from(1); } }; // One brokerd per home (M5b spec, section 9a). The lock comes before anything is made, the // audit log's own lock included, so a second copy never takes the sockets of the one that // runs. Held until the process ends. let lock_path = cfg.paths.home.join("run/brokerd.lock"); let _instance = match instance::lock(&lock_path) { Ok(held) => held, Err(e) => { eprintln!("{}", e.line("brokerd", &lock_path)); return ExitCode::from(1); } }; // Keep the socket paths for the serving notice, before `cfg` is moved into `start`. let broker_path = cfg.broker_socket(); let admin_path = cfg.admin_socket(); // The runtime and the line that says which. A `[runner]` runs calls in containers; without it, // every call is refused, as in M3a. let log: Arc = Arc::new(|line: &str| eprintln!("{line}")); let runtime: Box; let runtime_notice: String; match &cfg.runner { Some(runner) => { let image = runner.image.clone(); runtime = Box::new(Podman::new( runner.clone(), cfg.egress_dir(), Arc::clone(&log), )); runtime_notice = format!("tools run in containers from {image}"); } None => { runtime = Box::new(Refusing); runtime_notice = "no [runner] section: every tool call is refused".to_string(); } } // The memory tools go to `memoryd`, whatever runs the others (M5a spec, section 8). let runtime: Box = Box::new(Routed { memory: MemoryPort::new(cfg.memory_socket(), MEMORY_WAIT), other: runtime, }); let started = match serve::start(cfg, accept_break, runtime, log) { Ok(started) => started, Err(ServeError::Audit(e @ AuditError::NothingToAccept)) => { eprintln!("brokerd: {e}"); return ExitCode::from(2); } // The audit errors carry their own entry; the rest are this one. Err(ServeError::Audit(e)) => { eprintln!("brokerd: {e}"); return ExitCode::from(1); } Err(e) => { eprintln!("brokerd: {e}\n{START_FAILED}"); return ExitCode::from(1); } }; if started.recovered { eprintln!("{RECOVERED_NOTICE}"); } if let Some(failure) = &started.accepted { eprintln!( "audit: accepted the break at {}:{}: {}", failure.file, failure.line, failure.what ); } eprintln!( "brokerd: serving tools on {} and approvals on {}", broker_path.display(), admin_path.display() ); eprintln!("brokerd: {runtime_notice}"); match started.run() { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("brokerd: stopped serving: {e}\n{LISTENER_LOST}"); ExitCode::from(1) } } } /// The argument list was not `serve --config [--accept-break]`. fn usage() -> ExitCode { eprintln!("{USAGE}"); ExitCode::from(2) } /// The `--config` and `--accept-break` flags after `serve`, or `Err(())` for a bad list. /// Flags are compared as `OsStr`, so one that is not UTF-8 is simply unknown; the path is kept as /// given. fn parse_serve>(mut args: I) -> Result<(PathBuf, bool), ()> { let mut config: Option = None; let mut accept_break = false; loop { match args.next() { None => { return match config { Some(path) => Ok((PathBuf::from(path), accept_break)), None => Err(()), }; } Some(arg) if arg == "--config" => match args.next() { Some(path) => { if config.is_some() { return Err(()); } config = Some(path); } None => return Err(()), }, Some(arg) if arg == "--accept-break" => { if accept_break { return Err(()); } accept_break = true; } Some(_) => return Err(()), } } }