use anyhow::Result; use bento_daemon::{config, db, events, metrics, ota, routes, state, topology}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; /// MNW base URL the `tauri-mnw` OTA backend publishes to. Overridable so a /// staging host can point elsewhere; defaults to production. fn mnw_base_url() -> String { std::env::var("BENTO_MNW_BASE_URL").unwrap_or_else(|_| "https://makenot.work".into()) } #[tokio::main] async fn main() -> Result<()> { // `--check-config`: load the daemon config + topology (which reads every // app's in-repo manifest) and exit 0, or fail with the parse error and a // non-zero code. Run before swapping in a freshly built bentod so a schema // change that can't parse the live config fails the install instead of // bricking the daemon — a per-app-config move once left it down for 20h. // Mirrors `sandod --check-config`. if std::env::args().skip(1).any(|a| a == "--check-config") { let cfg = config::Config::load()?; let topo = topology::Topology::load(&cfg.topology_path)?; println!( "bentod config OK: {} host(s), {} app(s) from {}", topo.hosts.len(), topo.app.len(), cfg.topology_path.display() ); return Ok(()); } tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "bento_daemon=info,bentod=info,tower_http=info".into()), ) .init(); let cfg = Arc::new(config::Config::load()?); let topo = Arc::new(topology::Topology::load(&cfg.topology_path)?); tokio::fs::create_dir_all(&cfg.dist_root).await?; tokio::fs::create_dir_all(&cfg.logs_root).await?; let pool = db::open(&cfg.db_path).await?; // Reconcile any builds left `running` by a previous process (the finalizer // and target tasks don't survive a restart) so they don't orphan forever. match db::recover_orphaned_running(&pool).await { Ok(0) => {} Ok(n) => tracing::warn!( reconciled = n, "marked orphaned `running` builds failed after restart" ), Err(e) => tracing::error!(error = %e, "failed to reconcile orphaned running builds"), } tracing::info!( hosts = topo.hosts.len(), apps = topo.app.len(), "topology loaded" ); let prom = metrics::init(); let addr: SocketAddr = cfg.listen.parse()?; // Build-API auth (CF2). Token from BENTO_API_TOKEN (systemd EnvironmentFile); // refuse to expose build triggers unauthenticated on a non-loopback bind. let api_token: Option> = std::env::var("BENTO_API_TOKEN") .ok() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .map(|s| Arc::from(s.as_str())); if api_token.is_none() && !addr.ip().is_loopback() { anyhow::bail!( "BENTO_API_TOKEN is unset but listen={addr} is not loopback; refusing to expose build \ triggers unauthenticated. Set BENTO_API_TOKEN (via EnvironmentFile) or bind 127.0.0.1." ); } match &api_token { Some(_) => tracing::info!("build endpoints require a bearer token"), None => { tracing::warn!(%addr, "BENTO_API_TOKEN unset; build endpoints are UNAUTHENTICATED (loopback bind)"); } } let executors = Arc::new(state::build_executors(&topo)); let syncs = Arc::new(state::build_syncs(&topo)); let host_locks = state::build_host_locks(&topo); let app_state = state::AppState { pool, topo, cfg, prom, events: events::channel(), ota: Arc::new(ota::OtaRegistry::standard(mnw_base_url())), executors, syncs, active: Arc::new(tokio::sync::Mutex::new(HashMap::new())), api_token, host_locks, }; // Disk retention: prune logs_root + dist_root to the newest N versions per // app, at startup and every 6h, so neither root grows without bound. Skips // any (app, version) that must survive: one an in-flight build is writing // to, or one a published release still points at (so a slow-adopting // release that has fallen outside the keep window is never deleted out from // under clients still fetching it). { let cfg = app_state.cfg.clone(); let pool = app_state.pool.clone(); tokio::spawn(async move { let mut tick = tokio::time::interval(std::time::Duration::from_hours(6)); loop { tick.tick().await; // fires immediately, then every 6h let protected: bento_daemon::retention::ProtectedVersions = sqlx::query_as::<_, (String, String)>( "SELECT DISTINCT app, version FROM target_runs WHERE status = 'running' \ UNION \ SELECT DISTINCT app, version FROM releases", ) .fetch_all(&pool) .await .unwrap_or_default() .into_iter() .collect(); let cfg = cfg.clone(); let _ = tokio::task::spawn_blocking(move || { bento_daemon::retention::prune_once(&cfg, &protected); }) .await; } }); } let app = routes::router(app_state); tracing::info!(%addr, "bento daemon listening"); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app).await?; Ok(()) }