feat: env file
This commit is contained in:
1 parent
34698b0576
commit
acbdc20f38
4 files changed
+178
-8
No files matched your search
@@ -1,9 +1,12 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::detect::detect_venvs;
|
||||||
|
|
||||||
/// A launcher entry as it appears in `defaults.json`, where `gui_file` and
|
/// 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).
|
/// `server_file` may be omitted (falling back to the top-level defaults).
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -44,10 +47,52 @@ pub struct LauncherEntry {
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Base directory containing the source/app directories.
|
/// Base directory containing the source/app directories.
|
||||||
pub deployments_base: PathBuf,
|
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.
|
/// Resolved launcher entries, keyed by beamline.
|
||||||
pub entries: HashMap<String, LauncherEntry>,
|
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
|
/// Join `rel` onto `base`, treating `rel` as relative even if it starts with a
|
||||||
/// leading separator (as the entry points in `defaults.json` could).
|
/// leading separator (as the entry points in `defaults.json` could).
|
||||||
fn join_relative(base: &Path, rel: &Path) -> PathBuf {
|
fn join_relative(base: &Path, rel: &Path) -> PathBuf {
|
||||||
@@ -99,6 +144,8 @@ pub fn load_config(json: &str) -> Result<Config> {
|
|||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
deployments_base: raw.deployments_base,
|
deployments_base: raw.deployments_base,
|
||||||
|
default_gui_file: raw.default_gui_file,
|
||||||
|
default_server_file: raw.default_server_file,
|
||||||
entries,
|
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;
|
use crate::config::LauncherEntry;
|
||||||
|
|
||||||
/// Run `python <entrypoint>` on the system, waiting for it to finish.
|
/// Run `python <entrypoint>` on the system with `env` added to its environment,
|
||||||
pub fn launch(python: &Path, entrypoint: &Path) -> Result<()> {
|
/// waiting for it to finish.
|
||||||
|
fn run(python: &Path, entrypoint: &Path, env: &[(String, String)]) -> Result<()> {
|
||||||
let status = Command::new(python)
|
let status = Command::new(python)
|
||||||
.arg(entrypoint)
|
.arg(entrypoint)
|
||||||
|
.envs(env.iter().map(|(key, value)| (key, value)))
|
||||||
.status()
|
.status()
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -29,10 +31,17 @@ pub fn launch(python: &Path, entrypoint: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch_server(entry: &LauncherEntry) -> Result<()> {
|
/// Run `python <entrypoint>` on the system, waiting for it to finish.
|
||||||
launch(&entry.python, &entry.server_file)
|
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<()> {
|
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 config;
|
||||||
mod detect;
|
mod detect;
|
||||||
|
mod env_file;
|
||||||
mod launcher;
|
mod launcher;
|
||||||
mod tui;
|
mod tui;
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ use detect::detect_beamline;
|
|||||||
const INSTALLED_CONFIG: &str = "/etc/aarel/defaults.json";
|
const INSTALLED_CONFIG: &str = "/etc/aarel/defaults.json";
|
||||||
/// Config file shipped in the source tree, used as a fallback.
|
/// Config file shipped in the source tree, used as a fallback.
|
||||||
const LOCAL_CONFIG: &str = "data/defaults.json";
|
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)]
|
#[derive(Parser)]
|
||||||
#[command(name = "aarel")]
|
#[command(name = "aarel")]
|
||||||
@@ -49,16 +52,21 @@ enum Command {
|
|||||||
/// Launch the AareGUI with default settings
|
/// Launch the AareGUI with default settings
|
||||||
#[command(alias = "g")]
|
#[command(alias = "g")]
|
||||||
Gui {
|
Gui {
|
||||||
/// Config entry (beamline) to launch. Defaults to the detected beamline.
|
/// Config entry (deployment) to launch. Defaults to the detected beamline.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
entry: Option<String>,
|
entry: Option<String>,
|
||||||
},
|
},
|
||||||
/// Launch the AareDAQ server with default settings
|
/// Launch the AareDAQ server with default settings
|
||||||
#[command(alias = "s")]
|
#[command(alias = "s")]
|
||||||
Server {
|
Server {
|
||||||
/// Config entry (beamline) to launch. Defaults to the detected beamline.
|
/// Config entry (deployment) to launch. Defaults to the detected beamline.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
entry: Option<String>,
|
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::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(())
|
Ok(())
|
||||||
|
|||||||
Reference in new issue
Block a user