ci / rust (push) Successful in 1m35s
New `maestro-installer` crate exposing the `maestro-install` binary. Bootstrap flow: `cargo install --git ... maestro-installer && maestro-install` runs `cargo install` for `maestro-daemon` and `maestro-cli`, optionally drives `maestrod configure`, and on systemd hosts writes a user unit at `~/.config/systemd/user/maestrod.service` plus reload/enable/start. Pure builders (cargo install args, systemctl args, unit body) live in the lib and are unit-tested; main.rs is the prompt glue. Refs NAP-225.
103 lines
3.3 KiB
Rust
103 lines
3.3 KiB
Rust
//! systemd detection + user-unit rendering.
|
|
//!
|
|
//! The wizard only ever writes a *user* unit (`~/.config/systemd/user/`), since
|
|
//! `maestrod` runs per-user and uses tmux sockets in the user's runtime dir.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Standard probe path. Present on hosts booted under systemd.
|
|
pub const SYSTEMD_PROBE_PATH: &str = "/run/systemd/system";
|
|
|
|
/// Filename written under `~/.config/systemd/user/`.
|
|
pub const UNIT_FILE_NAME: &str = "maestrod.service";
|
|
|
|
/// `true` when the given path exists. Production callers should pass
|
|
/// [`SYSTEMD_PROBE_PATH`]; tests inject a temp path so they're hermetic.
|
|
pub fn is_systemd_host(probe: &Path) -> bool {
|
|
probe.exists()
|
|
}
|
|
|
|
/// `~/.config/systemd/user/maestrod.service`. `None` if `$HOME` / config dir
|
|
/// is unresolvable.
|
|
pub fn user_unit_path() -> Option<PathBuf> {
|
|
let base = dirs::config_dir()?;
|
|
Some(base.join("systemd").join("user").join(UNIT_FILE_NAME))
|
|
}
|
|
|
|
/// Render the unit file body. `exec_start` is the absolute path to `maestrod`
|
|
/// — we default to `~/.cargo/bin/maestrod` because that's where `cargo install`
|
|
/// lands binaries; systemd doesn't inherit the user's PATH, so we can't rely
|
|
/// on a bare `maestrod`.
|
|
pub fn render_user_unit(exec_start: &Path) -> String {
|
|
format!(
|
|
"[Unit]\n\
|
|
Description=Maestro per-host daemon (maestrod)\n\
|
|
Documentation=https://git.naps.pt/naps62/maestro\n\
|
|
After=network-online.target\n\
|
|
\n\
|
|
[Service]\n\
|
|
Type=simple\n\
|
|
ExecStart={exec}\n\
|
|
Restart=on-failure\n\
|
|
RestartSec=5\n\
|
|
\n\
|
|
[Install]\n\
|
|
WantedBy=default.target\n",
|
|
exec = exec_start.display()
|
|
)
|
|
}
|
|
|
|
/// Default `ExecStart` target: `~/.cargo/bin/maestrod`. `None` if `$HOME` is
|
|
/// unresolvable.
|
|
pub fn default_maestrod_path() -> Option<PathBuf> {
|
|
Some(
|
|
dirs::home_dir()?
|
|
.join(".cargo")
|
|
.join("bin")
|
|
.join("maestrod"),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn is_systemd_host_true_when_probe_exists() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let probe = dir.path().join("system");
|
|
std::fs::create_dir(&probe).unwrap();
|
|
assert!(is_systemd_host(&probe));
|
|
}
|
|
|
|
#[test]
|
|
fn is_systemd_host_false_when_probe_missing() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let probe = dir.path().join("not-here");
|
|
assert!(!is_systemd_host(&probe));
|
|
}
|
|
|
|
#[test]
|
|
fn unit_renders_with_absolute_exec_start() {
|
|
let body = render_user_unit(Path::new("/home/alice/.cargo/bin/maestrod"));
|
|
assert!(body.contains("ExecStart=/home/alice/.cargo/bin/maestrod"));
|
|
assert!(body.contains("[Unit]"));
|
|
assert!(body.contains("[Service]"));
|
|
assert!(body.contains("[Install]"));
|
|
assert!(body.contains("WantedBy=default.target"));
|
|
assert!(body.contains("Restart=on-failure"));
|
|
}
|
|
|
|
#[test]
|
|
fn unit_ends_with_newline() {
|
|
let body = render_user_unit(Path::new("/x"));
|
|
assert!(body.ends_with('\n'), "unit must end with newline");
|
|
}
|
|
|
|
#[test]
|
|
fn user_unit_path_points_under_config_dir() {
|
|
let p = user_unit_path().expect("config dir resolvable in test env");
|
|
assert!(p.ends_with("systemd/user/maestrod.service"), "got {p:?}");
|
|
}
|
|
}
|