use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use log::{info, warn};
use crate::{
config::{DEFAULT_INSTALL_PATH, DEFAULT_SYSTEMD_UNIT_NAME, DEFAULT_SYSTEMD_UNIT_PATH},
install, privilege,
};
#[derive(Parser, Debug)]
pub struct UpgradeArgs {
#[arg(long, default_value = DEFAULT_SYSTEMD_UNIT_NAME)]
unit: String,
#[arg(long, default_value = DEFAULT_SYSTEMD_UNIT_PATH)]
unit_file: PathBuf,
#[arg(long, default_value = DEFAULT_INSTALL_PATH)]
install_path: PathBuf,
#[arg(long)]
source: Option<PathBuf>,
#[arg(long)]
no_install: bool,
#[arg(long)]
dry_run: bool,
}
pub fn run(args: UpgradeArgs) -> Result<()> {
if !args.dry_run {
privilege::require_root("linprov upgrade")?;
}
let mut changed = true;
if !args.no_install {
let source = resolve_source(args.source.as_deref(), &args.install_path)?;
if args.dry_run {
println!(
"would copy {} -> {}",
source.display(),
args.install_path.display()
);
} else {
install::refuse_distro_owned(&args.install_path)?;
match install::install_to(&source, &args.install_path)? {
install::Outcome::Installed => info!(
"refreshed `{}` from `{}`",
args.install_path.display(),
source.display(),
),
install::Outcome::AlreadyCurrent => {
info!(
"`{}` already matches `{}` — nothing to do",
args.install_path.display(),
source.display(),
);
changed = false;
}
}
}
}
check_exec_start_matches(&args.unit_file, &args.install_path)?;
if !changed {
info!("skipping systemctl restart (no new bytes installed)");
return Ok(());
}
systemctl(&args.unit, args.dry_run)
}
fn resolve_source(explicit: Option<&Path>, install_path: &Path) -> Result<PathBuf> {
if let Some(p) = explicit {
return Ok(p.to_path_buf());
}
if let Some(p) = install::cargo_install_source() {
info!(
"found a freshly-installed binary at `{}`; using it as the upgrade source",
p.display()
);
return Ok(p);
}
let current = install::current_exe()?;
if same_path(¤t, install_path) {
return Err(anyhow!(
"no upgrade source: the running binary IS `{}`, and we couldn't \
auto-detect a `~/.cargo/bin/linprov` anywhere on this host. \
Either run `cargo install --force linprov` as a normal user, \
or point at the new binary explicitly:\n\n \
sudo linprov upgrade --source /path/to/new/linprov\n",
install_path.display()
));
}
Ok(current)
}
fn same_path(a: &Path, b: &Path) -> bool {
fs::canonicalize(a)
.and_then(|ac| fs::canonicalize(b).map(|bc| ac == bc))
.unwrap_or(false)
}
fn check_exec_start_matches(unit_file: &Path, install_path: &Path) -> Result<()> {
let unit = match fs::read_to_string(unit_file) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
warn!(
"systemd unit `{}` not found — `linprov setup` hasn't run \
on this host, or it wrote a unit somewhere else. Skipping \
the ExecStart drift check.",
unit_file.display()
);
return Ok(());
}
Err(e) => {
return Err(e).with_context(|| format!("reading `{}`", unit_file.display()));
}
};
let exec_start = unit
.lines()
.find_map(|l| l.strip_prefix("ExecStart="))
.map(str::trim);
let Some(exec_line) = exec_start else {
warn!(
"unit `{}` has no ExecStart= line; can't verify the binary path matches.",
unit_file.display()
);
return Ok(());
};
let unit_binary = exec_line.split_whitespace().next().unwrap_or("");
if Path::new(unit_binary) != install_path {
warn!(
"unit ExecStart points at `{unit_binary}` but the install \
path is `{}`. The unit will keep running the old binary \
until you re-run `linprov setup --force` to rewrite it.",
install_path.display(),
);
} else {
info!(
"ExecStart matches the install path ({})",
install_path.display()
);
}
Ok(())
}
fn systemctl(unit: &str, dry_run: bool) -> Result<()> {
let cmds: &[&[&str]] = &[
&["systemctl", "daemon-reload"],
&["systemctl", "restart", unit],
];
for cmd in cmds {
if dry_run {
println!("would run: {}", cmd.join(" "));
continue;
}
info!("running: {}", cmd.join(" "));
let status = Command::new(cmd[0])
.args(&cmd[1..])
.status()
.with_context(|| format!("invoking `{}`", cmd.join(" ")))?;
if !status.success() {
return Err(anyhow!("`{}` exited {:?}", cmd.join(" "), status.code()));
}
}
if !dry_run {
info!("linprov restarted. Tail with: journalctl -u {unit} -f");
}
Ok(())
}