use std::sync::{Mutex, OnceLock};
use serde::{Deserialize, Serialize};
use crate::paths::machine_config_path;
use crate::utils::atomic::atomic_write;
use crate::utils::lock::LockRecover;
#[derive(Debug, Default, Deserialize, Serialize)]
struct MachineToml {
name: Option<String>,
max_concurrent_runs: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MachineSource {
Env,
File,
Generated,
Hostname,
}
impl MachineSource {
pub const fn label(self) -> &'static str {
match self {
Self::Env => "MOADIM_MACHINE env",
Self::File => "machine.local.toml",
Self::Generated => "auto-generated (first run)",
Self::Hostname => "system hostname",
}
}
}
pub fn current_machine() -> String {
resolve().0
}
pub fn resolve() -> (String, MachineSource) {
let env = std::env::var("MOADIM_MACHINE").ok();
let file = read_machine_file();
if let Some(name) = non_empty(env) {
return (name, MachineSource::Env);
}
if let Some(name) = non_empty(file) {
return (name, MachineSource::File);
}
let generated = generate_name();
match set_machine(&generated) {
Ok(()) => {
log::warn!(
"no machine name configured; generated {generated:?} — run `moadim machine set <name>` to choose your own"
);
(generated, MachineSource::Generated)
}
Err(err) => {
log::warn!("failed to save generated machine name: {err}; falling back to hostname");
(hostname(), MachineSource::Hostname)
}
}
}
fn generate_name() -> String {
format!(
"machine-{}",
&uuid::Uuid::new_v4().simple().to_string()[..8]
)
}
#[cfg(test)]
fn resolve_from(
env: Option<String>,
file: Option<String>,
hostname: String,
) -> (String, MachineSource) {
if let Some(name) = non_empty(env) {
return (name, MachineSource::Env);
}
if let Some(name) = non_empty(file) {
return (name, MachineSource::File);
}
(hostname, MachineSource::Hostname)
}
fn non_empty(value: Option<String>) -> Option<String> {
value
.map(|raw| raw.trim().to_string())
.filter(|trimmed| !trimmed.is_empty())
}
fn hostname() -> String {
gethostname::gethostname().to_string_lossy().into_owned()
}
fn read_machine_toml() -> MachineToml {
std::fs::read_to_string(machine_config_path())
.ok()
.and_then(|text| toml::from_str(&text).ok())
.unwrap_or_default()
}
fn read_machine_file() -> Option<String> {
read_machine_toml().name
}
fn machine_toml_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn write_machine_toml(toml: &MachineToml) -> std::io::Result<()> {
let path = machine_config_path();
let parent = crate::utils::fs_perms::parent_or_err(&path, "machine config")?;
crate::utils::fs_perms::create_private_dir_all(parent)?;
let text = toml::to_string_pretty(toml).map_err(std::io::Error::other)?;
atomic_write(&path, text.as_bytes())
}
pub fn set_machine(name: &str) -> std::io::Result<()> {
let name = name.trim();
if name.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"machine name must not be empty",
));
}
let _guard = machine_toml_lock().lock_recover();
let mut toml = read_machine_toml();
toml.name = Some(name.to_string());
write_machine_toml(&toml)
}
pub fn max_concurrent_runs_override() -> Option<usize> {
read_machine_toml().max_concurrent_runs
}
pub fn set_max_concurrent_runs_override(value: Option<usize>) -> std::io::Result<()> {
let _guard = machine_toml_lock().lock_recover();
let mut toml = read_machine_toml();
toml.max_concurrent_runs = value;
write_machine_toml(&toml)
}
pub fn referenced_machines() -> std::collections::BTreeSet<String> {
let mut names = std::collections::BTreeSet::new();
let routines = crate::routine_storage::load_store();
for routine in routines.lock_recover().values() {
names.extend(routine.machines.iter().cloned());
}
names
}
pub fn targets(machines: &[String], me: &str) -> bool {
machines.iter().any(|pattern| {
if pattern.contains('*') {
glob_match(pattern, me)
} else {
pattern == me
}
})
}
fn glob_match(pattern: &str, name: &str) -> bool {
let pattern: Vec<char> = pattern.chars().collect();
let name: Vec<char> = name.chars().collect();
let (mut pi, mut ni) = (0, 0);
let mut star: Option<usize> = None;
let mut star_match = 0;
while ni < name.len() {
if pi < pattern.len() && pattern[pi] == name[ni] {
pi += 1;
ni += 1;
} else if pi < pattern.len() && pattern[pi] == '*' {
star = Some(pi);
star_match = ni;
pi += 1;
} else if let Some(star_pi) = star {
pi = star_pi + 1;
star_match += 1;
ni = star_match;
} else {
return false;
}
}
while pattern.get(pi) == Some(&'*') {
pi += 1;
}
pi == pattern.len()
}
pub fn run(args: &[String]) -> i32 {
match args.first().map(String::as_str) {
None | Some("show") => cmd_show(),
Some("set") => {
if let Some(name) = args.get(1) {
cmd_set(name)
} else {
eprintln!("usage: moadim machine set <name>");
2
}
}
Some("list") => cmd_list(),
Some(other) => {
eprintln!("unknown machine subcommand {other:?}; expected show, set, or list");
2
}
}
}
fn cmd_show() -> i32 {
let (name, source) = resolve();
println!("{name} (from {})", source.label());
0
}
fn cmd_set(name: &str) -> i32 {
match set_machine(name) {
Ok(()) => {
println!("machine name set to {:?}", name.trim());
0
}
Err(err) => {
eprintln!("error: failed to set machine name: {err}");
1
}
}
}
fn cmd_list() -> i32 {
let names = referenced_machines();
if names.is_empty() {
println!("no machines referenced by any routine");
} else {
for name in &names {
println!("{name}");
}
}
0
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod machine_tests;