feat: env file
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::detect::detect_venvs;
|
||||
|
||||
/// A launcher entry as it appears in `defaults.json`, where `gui_file` and
|
||||
/// `server_file` may be omitted (falling back to the top-level defaults).
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -44,10 +47,52 @@ pub struct LauncherEntry {
|
||||
pub struct Config {
|
||||
/// Base directory containing the source/app directories.
|
||||
pub deployments_base: PathBuf,
|
||||
/// Default GUI entry point, relative to a deployment directory.
|
||||
pub default_gui_file: PathBuf,
|
||||
/// Default server entry point, relative to a deployment directory.
|
||||
pub default_server_file: PathBuf,
|
||||
/// Resolved launcher entries, keyed by beamline.
|
||||
pub entries: HashMap<String, LauncherEntry>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Discover launchable deployments on disk under `deployments_base`, one
|
||||
/// entry per (deployment, virtualenv) pair, labelled `"<deployment>/<venv>"`.
|
||||
///
|
||||
/// These are the "custom" deployments that are present on the machine but
|
||||
/// not named in the config. Entry points use the configured defaults.
|
||||
pub fn discovered_entries(&self) -> Vec<(String, LauncherEntry)> {
|
||||
let mut found = Vec::new();
|
||||
let Ok(dirs) = fs::read_dir(&self.deployments_base) else {
|
||||
return found;
|
||||
};
|
||||
|
||||
for deployment in dirs.flatten() {
|
||||
if !deployment.file_type().map(|t| t.is_dir()).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let dir = deployment.path();
|
||||
let name = deployment.file_name().to_string_lossy().into_owned();
|
||||
|
||||
for venv in detect_venvs(&dir).unwrap_or_default() {
|
||||
let venv_name = venv
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
found.push((
|
||||
format!("{name}/{venv_name}"),
|
||||
LauncherEntry {
|
||||
python: venv.join("bin/python"),
|
||||
gui_file: join_relative(&dir, &self.default_gui_file),
|
||||
server_file: join_relative(&dir, &self.default_server_file),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
/// Join `rel` onto `base`, treating `rel` as relative even if it starts with a
|
||||
/// leading separator (as the entry points in `defaults.json` could).
|
||||
fn join_relative(base: &Path, rel: &Path) -> PathBuf {
|
||||
@@ -99,6 +144,8 @@ pub fn load_config(json: &str) -> Result<Config> {
|
||||
|
||||
Ok(Config {
|
||||
deployments_base: raw.deployments_base,
|
||||
default_gui_file: raw.default_gui_file,
|
||||
default_server_file: raw.default_server_file,
|
||||
entries,
|
||||
})
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
use std::path::Path;
|
||||
|
||||
/// Load `key=value` pairs from an env file to add to a process's environment.
|
||||
///
|
||||
/// If the file does not exist (or cannot be read) this logs a warning and
|
||||
/// returns an empty set rather than failing — the env file is optional.
|
||||
pub fn load_env_file(path: &Path) -> Vec<(String, String)> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(contents) => parse_env(&contents),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
eprintln!(
|
||||
"warning: env file {} not found, continuing without it",
|
||||
path.display()
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"warning: could not read env file {} ({err}), continuing without it",
|
||||
path.display()
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `key=value` lines into pairs. Blank lines, `#` comments, and lines
|
||||
/// without a `=` are ignored. Keys and values are trimmed of surrounding
|
||||
/// whitespace.
|
||||
fn parse_env(contents: &str) -> Vec<(String, String)> {
|
||||
contents
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
let (key, value) = line.split_once('=')?;
|
||||
Some((key.trim().to_string(), value.trim().to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_basic_pairs() {
|
||||
let env = parse_env("A=1\nB=two");
|
||||
assert_eq!(
|
||||
env,
|
||||
vec![
|
||||
("A".to_string(), "1".to_string()),
|
||||
("B".to_string(), "two".to_string())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_blank_lines_and_comments() {
|
||||
let env = parse_env("\n# a comment\nA=1\n\n # indented comment\nB=2\n");
|
||||
assert_eq!(
|
||||
env,
|
||||
vec![
|
||||
("A".to_string(), "1".to_string()),
|
||||
("B".to_string(), "2".to_string())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_surrounding_whitespace() {
|
||||
let env = parse_env(" KEY = value ");
|
||||
assert_eq!(env, vec![("KEY".to_string(), "value".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_equals_signs_in_value() {
|
||||
let env = parse_env("URL=postgres://h/db?a=1&b=2");
|
||||
assert_eq!(
|
||||
env,
|
||||
vec![("URL".to_string(), "postgres://h/db?a=1&b=2".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_lines_without_equals() {
|
||||
let env = parse_env("A=1\ngarbage\nB=2");
|
||||
assert_eq!(
|
||||
env,
|
||||
vec![
|
||||
("A".to_string(), "1".to_string()),
|
||||
("B".to_string(), "2".to_string())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input_yields_nothing() {
|
||||
assert!(parse_env("").is_empty());
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -5,10 +5,12 @@ use anyhow::{Context, Result, bail};
|
||||
|
||||
use crate::config::LauncherEntry;
|
||||
|
||||
/// Run `python <entrypoint>` on the system, waiting for it to finish.
|
||||
pub fn launch(python: &Path, entrypoint: &Path) -> Result<()> {
|
||||
/// Run `python <entrypoint>` on the system with `env` added to its environment,
|
||||
/// waiting for it to finish.
|
||||
fn run(python: &Path, entrypoint: &Path, env: &[(String, String)]) -> Result<()> {
|
||||
let status = Command::new(python)
|
||||
.arg(entrypoint)
|
||||
.envs(env.iter().map(|(key, value)| (key, value)))
|
||||
.status()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
@@ -29,10 +31,17 @@ pub fn launch(python: &Path, entrypoint: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn launch_server(entry: &LauncherEntry) -> Result<()> {
|
||||
launch(&entry.python, &entry.server_file)
|
||||
/// Run `python <entrypoint>` on the system, waiting for it to finish.
|
||||
pub fn launch(python: &Path, entrypoint: &Path) -> Result<()> {
|
||||
run(python, entrypoint, &[])
|
||||
}
|
||||
|
||||
/// Launch the server entry point with extra `env` variables added to its
|
||||
/// environment.
|
||||
pub fn launch_server(entry: &LauncherEntry, env: &[(String, String)]) -> Result<()> {
|
||||
run(&entry.python, &entry.server_file, env)
|
||||
}
|
||||
|
||||
pub fn launch_gui(entry: &LauncherEntry) -> Result<()> {
|
||||
launch(&entry.python, &entry.gui_file)
|
||||
run(&entry.python, &entry.gui_file, &[])
|
||||
}
|
||||
|
||||
+14
-3
@@ -1,5 +1,6 @@
|
||||
mod config;
|
||||
mod detect;
|
||||
mod env_file;
|
||||
mod launcher;
|
||||
mod tui;
|
||||
|
||||
@@ -14,6 +15,8 @@ use detect::detect_beamline;
|
||||
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")]
|
||||
@@ -49,16 +52,21 @@ enum Command {
|
||||
/// Launch the AareGUI with default settings
|
||||
#[command(alias = "g")]
|
||||
Gui {
|
||||
/// Config entry (beamline) to launch. Defaults to the detected beamline.
|
||||
/// Config entry (deployment) to launch. Defaults to the detected beamline.
|
||||
#[arg(long)]
|
||||
entry: Option<String>,
|
||||
},
|
||||
/// Launch the AareDAQ server with default settings
|
||||
#[command(alias = "s")]
|
||||
Server {
|
||||
/// Config entry (beamline) to launch. Defaults to the detected beamline.
|
||||
/// Config entry (deployment) to launch. Defaults to the detected beamline.
|
||||
#[arg(long)]
|
||||
entry: Option<String>,
|
||||
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -80,7 +88,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
Command::Gui { entry } => launcher::launch_gui(lookup_entry(&config, entry)?)?,
|
||||
Command::Server { entry } => launcher::launch_server(lookup_entry(&config, entry)?)?,
|
||||
Command::Server { entry, env_file } => {
|
||||
let env = env_file::load_env_file(&env_file);
|
||||
launcher::launch_server(lookup_entry(&config, entry)?, &env)?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user