3.4 KiB · text History 0eedfc0
//! apollos-dbus: Apollo's Crib on the session bus.
//!
//! Connects to any number of Ankyrae and Keryxiae and keeps what they say as
//! objects on the session bus, for Plasma widgets and anything else to bind.
//! See README.md for the interface.

mod bus;
mod config;
mod points;
mod source;
mod store;
mod wire;

use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

use anyhow::{Context, Result, bail};
use clap::Parser;
use tokio::sync::mpsc;
use tracing::info;

use crate::bus::Root;
use crate::config::{Config, Kind};
use crate::store::State;

#[derive(Parser)]
#[command(version, about)]
struct Args {
    /// The config file. See README.md for what goes in it.
    #[arg(short, long, env = "APOLLOS_DBUS_CONFIG")]
    config: Option<PathBuf>,
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "apollos_dbus=info".into()),
        )
        .init();

    let args = Args::parse();
    let path = args.config.unwrap_or_else(config::default_path);
    let config = Config::load(&path)?;
    let sources = config.sources()?;
    if sources.is_empty() {
        bail!(
            "{} names no [[ankyra]] or [[keryx]]; nothing to do",
            path.display()
        );
    }

    let client_id = config.client_id.clone().unwrap_or_else(|| {
        format!(
            "apollos-dbus-{}",
            gethostname::gethostname().to_string_lossy()
        )
    });

    let (events_tx, events_rx) = mpsc::channel(64);
    let mut commands = HashMap::new();
    let mut runners = vec![];
    for s in &sources {
        let (tx, rx) = mpsc::channel(8);
        commands.insert(s.label(), (matches!(s.kind, Kind::Keryx { .. }), tx));
        // One client id per connection: the broker closes the older of two
        // that share one. The first keeps the bare id, which is the one an
        // Ankyra's allow-list is most likely to have been given.
        let id = if runners.is_empty() {
            client_id.clone()
        } else {
            format!("{client_id}-{}", s.name)
        };
        runners.push((s.clone(), id, rx));
    }

    let mut state = State::new(
        sources.iter().map(|s| s.label()),
        Duration::from_secs(config.stale_after_secs),
    );
    let cache = store::cache_path();
    state.load(&cache);

    let root = Root {
        board: "{}".into(),
        points: "[]".into(),
        queries: vec![],
        sources: "[]".into(),
        connected: false,
        origins: HashMap::new(),
        commands,
    };
    let conn = zbus::connection::Builder::session()
        .serve_at(bus::ROOT, root)
        .serve_at(bus::ROOT, zbus::fdo::ObjectManager)
        .build()
        .await?;
    // Named only once the objects are there, so activation never hands a
    // caller a name with nothing behind it.
    conn.request_name(bus::NAME)
        .await
        .with_context(|| format!("claiming {} -- is another apollos-dbus running?", bus::NAME))?;
    info!("on the session bus as {}", bus::NAME);

    for (s, id, rx) in runners {
        tokio::spawn(source::run(s, id, events_tx.clone(), rx));
    }
    drop(events_tx);

    tokio::select! {
        _ = store::run(conn, state, events_rx, cache) => {}
        _ = tokio::signal::ctrl_c() => info!("interrupted"),
    }
    Ok(())
}