mod config; mod detect; mod env_file; mod launcher; mod tui; use std::path::{Path, PathBuf}; use anyhow::{Context, Error, Result, bail}; use clap::{ArgAction, Parser, Subcommand}; use config::{Config, LauncherEntry, load_config}; use detect::detect_beamline; /// Config file installed by the RPM, preferred when it exists. const INSTALLED_CONFIG: &str = "/etc/aarel/defaults.json"; /// Config file shipped in the source tree, used as a fallback. const LOCAL_CONFIG: &str = "data/defaults.json"; /// Default env file added to the server's environment when launching. const DEFAULT_ENV_FILE: &str = "/etc/aaredaq/aaredaq.env"; #[derive(Parser)] #[command(name = "aarel", version, disable_version_flag = true)] struct Cli { /// Print version information. #[arg(short = 'v', long = "version", action = ArgAction::Version)] version: Option, /// Path to the launcher config file. Defaults to /etc/aarel/defaults.json, /// falling back to ./data/defaults.json. #[arg(long)] config: Option, #[command(subcommand)] command: Command, } /// Resolve the config path: the explicit `--config`, else the installed config /// if present, else the local one. fn resolve_config_path(config: Option) -> PathBuf { if let Some(path) = config { return path; } let installed = PathBuf::from(INSTALLED_CONFIG); if installed.is_file() { installed } else { PathBuf::from(LOCAL_CONFIG) } } #[derive(Subcommand)] enum Command { /// Enter the TUI to choose what to launch #[command(alias = "p")] Pick, /// Launch the AareGUI with default settings #[command(alias = "g")] Gui { /// Deployment to launch. Defaults to the detected beamline. #[arg(short = 'd', long)] deployment: Option, }, /// Launch the AareDAQ server with default settings #[command(alias = "s")] Server { /// Deployment to launch. Defaults to the detected beamline. #[arg(short = 'd', long)] deployment: Option, /// Env file (`key=value` per line) added to the server's environment. /// Ignored with a warning if it does not exist. #[arg(long, default_value = DEFAULT_ENV_FILE)] env_file: PathBuf, }, } fn main() -> Result<()> { let cli = Cli::parse(); let config_path = resolve_config_path(cli.config); let json = std::fs::read_to_string(&config_path) .with_context(|| format!("failed to read config file {}", config_path.display()))?; let mut config = load_config(&json)?; check_base_exists("deployments_base", &config.deployments_base, &config_path)?; // Populate the config with deployments found on disk so they are available // to both `lookup_entry` and the TUI. Configured entries take precedence. for (label, entry) in config.discovered_entries() { config.entries.entry(label).or_insert(entry); } match cli.command { Command::Pick => { if let Some(launch) = tui::run(&config, DEFAULT_ENV_FILE)? { let env = launch .env_file .as_deref() .map(env_file::load_env_file) .unwrap_or_default(); launcher::launch_with_env(&launch.python, &launch.entrypoint, &env)?; } } Command::Gui { deployment } => launcher::launch_gui(lookup_entry(&config, deployment)?)?, Command::Server { deployment, env_file, } => { let env = env_file::load_env_file(&env_file); launcher::launch_server(lookup_entry(&config, deployment)?, &env)? } } Ok(()) } /// Fail with a helpful message if a configured base directory is missing. fn check_base_exists(field: &str, base: &Path, config_file: &Path) -> Result<()> { if !base.is_dir() { bail!( "{field} '{}' does not exist — edit '{field}' in {} to point at a valid directory", base.display(), config_file.display(), ); } Ok(()) } /// Look up the launcher entry to use: the explicitly requested `entry`, or the /// detected beamline when none was given. fn lookup_entry(config: &Config, entry: Option) -> Result<&LauncherEntry> { let beamline = match entry { Some(entry) => entry, None => detect_beamline().map_err(Error::msg)?, }; config .entries .get(&beamline) .with_context(|| format!("no config entry for beamline '{beamline}'")) }