use std::fmt;
use std::path::PathBuf;
use engenho_unit::layout::{HostRoot, Layout};
use engenho_unit::{Overrides, UnitRunError, plan};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Args {
pub unit: PathBuf,
pub user: Option<String>,
pub group: Option<String>,
pub check: bool,
pub root: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgsError {
MissingUnit,
MissingValue {
flag: String,
},
Unknown {
argument: String,
},
}
impl fmt::Display for ArgsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingUnit => {
f.write_str("unit-run needs --unit <path-to-a-rendered-.service-file>")
}
Self::MissingValue { flag } => write!(f, "{flag} needs a value"),
Self::Unknown { argument } => write!(
f,
"unit-run does not take {argument:?} (it takes --unit <path> \
[--user <name>] [--group <name>] [--root <dir>] [--check])"
),
}
}
}
impl std::error::Error for ArgsError {}
impl Args {
pub fn parse(args: impl IntoIterator<Item = String>) -> Result<Self, ArgsError> {
let mut unit = None;
let mut user = None;
let mut group = None;
let mut root = None;
let mut check = false;
let mut args = args.into_iter();
while let Some(argument) = args.next() {
let mut value = |flag: &str| {
args.next().ok_or_else(|| ArgsError::MissingValue {
flag: flag.to_string(),
})
};
match argument.as_str() {
"--unit" => unit = Some(PathBuf::from(value("--unit")?)),
"--user" => user = Some(value("--user")?),
"--group" => group = Some(value("--group")?),
"--root" => root = Some(PathBuf::from(value("--root")?)),
"--check" => check = true,
_ => return Err(ArgsError::Unknown { argument }),
}
}
Ok(Self {
unit: unit.ok_or(ArgsError::MissingUnit)?,
user,
group,
check,
root,
})
}
#[must_use]
pub fn overrides(&self) -> Overrides {
Overrides {
user: self.user.clone(),
group: self.group.clone(),
}
}
#[must_use]
pub fn layout(&self) -> Layout {
Layout::new(match &self.root {
Some(root) => HostRoot::at(root.clone()),
None => HostRoot::system(),
})
}
}
#[must_use]
pub fn run(args: Vec<String>) -> i32 {
let args = match Args::parse(args) {
Ok(args) => args,
Err(e) => {
eprintln!("engenho unit-run: {e}");
return 64;
}
};
init_tracing();
let planned = match plan(&args.unit, &args.overrides(), args.layout()) {
Ok(planned) => planned,
Err(e) => return report(&e),
};
if args.check {
print!("{}", planned.report());
return 0;
}
report(&planned.run())
}
fn report(error: &UnitRunError) -> i32 {
tracing::error!(error = %error, "unit-run failed");
eprintln!("engenho unit-run: {error}");
error.exit_code()
}
fn init_tracing() {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
let _ = tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
.try_init();
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(argv: &[&str]) -> Result<Args, ArgsError> {
Args::parse(argv.iter().map(|a| (*a).to_string()))
}
#[test]
fn the_contract_the_nix_side_calls() {
let args = parse(&[
"--unit",
"/nix/store/x-unit-zwave-js.service/zwave-js.service",
])
.unwrap();
assert_eq!(
args.unit,
PathBuf::from("/nix/store/x-unit-zwave-js.service/zwave-js.service")
);
assert_eq!(args.overrides(), Overrides::default());
assert!(!args.check);
}
#[test]
fn the_overrides_a_dynamic_user_unit_needs() {
let args = parse(&[
"--unit",
"/x/zwave-js.service",
"--user",
"zwave-js",
"--group",
"zwave-js",
])
.unwrap();
assert_eq!(args.user.as_deref(), Some("zwave-js"));
assert_eq!(args.group.as_deref(), Some("zwave-js"));
assert_eq!(
args.overrides(),
Overrides {
user: Some("zwave-js".into()),
group: Some("zwave-js".into())
}
);
}
#[test]
fn a_missing_unit_a_dangling_flag_and_a_stray_argument_are_typed_errors() {
assert_eq!(parse(&["--user", "x"]), Err(ArgsError::MissingUnit));
assert_eq!(
parse(&["--unit"]),
Err(ArgsError::MissingValue {
flag: "--unit".into()
})
);
assert_eq!(
parse(&["--unit", "/x/a.service", "--sandbox"]),
Err(ArgsError::Unknown {
argument: "--sandbox".into()
})
);
}
#[test]
fn a_root_makes_the_layout_relative_to_it() {
let args = parse(&["--unit", "/x/a.service", "--root", "/tmp/r", "--check"]).unwrap();
assert!(args.check);
assert_eq!(args.layout().root().path(), std::path::Path::new("/tmp/r"));
let default = parse(&["--unit", "/x/a.service"]).unwrap();
assert_eq!(default.layout().root().path(), std::path::Path::new("/"));
}
}