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>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MachineSource {
Env,
File,
Generated,
Hostname,
}
impl MachineSource {
pub fn label(self) -> &'static str {
match self {
MachineSource::Env => "MOADIM_MACHINE env",
MachineSource::File => "machine.local.toml",
MachineSource::Generated => "auto-generated (first run)",
MachineSource::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_file() -> Option<String> {
let text = std::fs::read_to_string(machine_config_path()).ok()?;
toml::from_str::<MachineToml>(&text).ok()?.name
}
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 path = machine_config_path();
std::fs::create_dir_all(path.parent().expect("machine config path has a parent dir"))?;
let toml = MachineToml {
name: Some(name.to_string()),
};
let text = toml::to_string_pretty(&toml)
.expect("MachineToml serialization cannot fail for a struct with an Option<String> field");
atomic_write(&path, text.as_bytes())
}
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(|name| name == me)
}
pub fn run(args: &[String]) -> i32 {
match args.first().map(String::as_str) {
None | Some("show") => cmd_show(),
Some("set") => match args.get(1) {
Some(name) => cmd_set(name),
None => {
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;