use std::path::{Path, PathBuf};
use std::process::Command;
use crate::cli::UpdateCommand;
use crate::{service, update};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
UpToDate {
current: String,
latest: String,
},
Available {
current: String,
latest: String,
},
Unknown {
current: String,
latest: String,
},
}
pub fn decide(current: &str, latest: &str) -> Decision {
let (current, latest) = (current.to_string(), latest.to_string());
if !update::is_semver(¤t) || !update::is_semver(&latest) {
return Decision::Unknown { current, latest };
}
if update::is_newer(¤t, &latest) {
return Decision::Available { current, latest };
}
Decision::UpToDate { current, latest }
}
pub fn describe(decision: &Decision) -> String {
match decision {
Decision::UpToDate { current, latest } => {
format!("mobux {current} is up to date (latest published: {latest})")
}
Decision::Available { current, latest } => {
format!("mobux {current} — an update is available: {latest}")
}
Decision::Unknown { current, latest } => {
format!("mobux {current} — cannot compare against the published version {latest:?}")
}
}
}
pub fn has_prebuilt_asset(os: &str, arch: &str) -> bool {
update::asset_target(os, arch).is_some()
}
pub async fn run(command: UpdateCommand) -> i32 {
let settings = match crate::resolve_config(&crate::cli::RunOptions::default()) {
Ok(settings) => settings,
Err(e) => {
eprintln!("mobux: could not read the config: {e}");
return 1;
}
};
let current = update::UpdateState::current_version();
let latest = match update::fetch_latest_version_from(&settings.update.check_url).await {
Ok(latest) => latest,
Err(e) => {
eprintln!("mobux: could not check for updates: {e}");
eprintln!(
" point update.check_url (env: MOBUX_UPDATE_CHECK_URL) at a reachable \
mirror of the crates.io index, or install by hand: cargo install mobux --locked"
);
return 1;
}
};
let decision = decide(current, &latest);
println!("{}", describe(&decision));
if command == UpdateCommand::Check {
return 0;
}
match decision {
Decision::UpToDate { .. } => 0,
Decision::Unknown { latest, .. } => {
eprintln!(
"mobux: refusing to install {latest:?} — it is not a version this binary can \
compare against. Install the version you want by hand: \
cargo install mobux --locked --version <VERSION>"
);
1
}
Decision::Available { latest, .. } => apply(&settings, &latest),
}
}
fn apply(settings: &crate::config::Config, version: &str) -> i32 {
match install(settings, version) {
Ok(bin) => restart_or_report(&settings.app.service_name, version, &bin),
Err(message) => {
eprintln!("mobux: {message}");
1
}
}
}
fn install(settings: &crate::config::Config, version: &str) -> Result<PathBuf, String> {
if std::env::var_os("MOBUX_UPDATE_DISABLE_RUN").is_some() {
return Err(
"self-update is disabled on this host (MOBUX_UPDATE_DISABLE_RUN); update mobux \
the way this host manages it"
.to_string(),
);
}
let bin = std::env::current_exe()
.map_err(|e| format!("could not resolve this binary's own path: {e}"))?;
check_replaceable(&bin)?;
let (os, arch) = (std::env::consts::OS, std::env::consts::ARCH);
let asset = update::asset_name(os, arch);
if !has_prebuilt_asset(os, arch) {
println!(
"no prebuilt release asset for {os}/{arch} — installing from source with cargo \
install, which takes several minutes"
);
}
let dir =
crate::resolve_data_dir(settings).map_err(|e| format!("resolving the data dir: {e}"))?;
std::fs::create_dir_all(&dir).map_err(|e| format!("creating {}: {e}", dir.display()))?;
let script = update::write_updater_script(&dir).map_err(|e| e.to_string())?;
println!("installing mobux {version} over {}", bin.display());
let mut cmd = Command::new("bash");
cmd.arg(&script)
.arg("--install-only")
.env("MOBUX_UPDATE_VERSION", version)
.env("MOBUX_UPDATE_BIN", &bin)
.env("MOBUX_UPDATE_ROOT", update::cargo_root(&bin))
.env("MOBUX_UPDATE_DATA_DIR", &dir);
if let Some(asset) = &asset {
cmd.env("MOBUX_UPDATE_ASSET", asset);
}
let status = cmd
.status()
.map_err(|e| format!("running the updater ({}): {e}", script.display()))?;
match status.code() {
Some(0) => Ok(bin),
Some(4) => Err(
"another update already holds the updater lock; wait for it to finish and try again"
.to_string(),
),
Some(code) => Err(format!(
"the updater failed (exit {code}) and {} is unchanged. When no release asset exists \
for this platform the fallback needs cargo: cargo install mobux --locked --version \
{version}",
bin.display()
)),
None => Err(format!(
"the updater was killed by a signal; {} may be half-replaced — reinstall with \
cargo install mobux --locked --version {version}",
bin.display()
)),
}
}
fn check_replaceable(bin: &Path) -> Result<(), String> {
let dir = bin
.parent()
.ok_or_else(|| format!("{} has no parent directory", bin.display()))?;
let probe = dir.join(format!(".mobux-update-probe-{}", std::process::id()));
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&probe)
.map_err(|e| {
format!(
"cannot replace {}: {} is not writable ({e}). Run the update as the user that \
owns it, or install your own copy: cargo install mobux --locked",
bin.display(),
dir.display()
)
})?;
let _ = std::fs::remove_file(&probe);
Ok(())
}
fn restart_or_report(service_name: &str, version: &str, bin: &Path) -> i32 {
let unit = service::unit_name(Some(service_name.to_string()));
let installed = service::config_dir()
.map(|dir| service::unit_path(&dir, &unit).exists())
.unwrap_or(false);
if !installed {
println!("mobux {version} installed at {}", bin.display());
println!("no systemd --user unit {unit:?} here — restart any running mobux by hand");
return 0;
}
match systemctl_restart(&unit) {
Ok(()) => {
println!("mobux {version} installed and unit {unit:?} restarted");
0
}
Err(message) => {
eprintln!("mobux: {message}");
eprintln!(
" mobux {version} IS installed at {} — only the restart failed. The old \
version keeps serving until you run: systemctl --user restart {unit}",
bin.display()
);
1
}
}
}
fn systemctl_restart(unit: &str) -> Result<(), String> {
let output = Command::new("systemctl")
.args(["--user", "restart", unit])
.output()
.map_err(|e| format!("running systemctl --user restart {unit}: {e}"))?;
if output.status.success() {
return Ok(());
}
Err(format!(
"systemctl --user restart {unit} failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_newer_published_version_is_an_update() {
assert_eq!(
decide("0.1.5", "0.1.6"),
Decision::Available {
current: "0.1.5".to_string(),
latest: "0.1.6".to_string(),
}
);
assert!(matches!(
decide("0.1.9", "0.1.20"),
Decision::Available { .. }
));
}
#[test]
fn the_same_or_an_older_published_version_is_up_to_date() {
assert_eq!(
decide("0.1.5", "0.1.5"),
Decision::UpToDate {
current: "0.1.5".to_string(),
latest: "0.1.5".to_string(),
}
);
assert!(matches!(
decide("0.2.0", "0.1.5"),
Decision::UpToDate { .. }
));
}
#[test]
fn an_unparseable_version_is_not_silently_up_to_date() {
assert!(matches!(
decide("0.1.5", "nightly"),
Decision::Unknown { .. }
));
assert!(matches!(decide("dev", "0.1.5"), Decision::Unknown { .. }));
assert!(matches!(decide("0.1", "0.1.5"), Decision::Unknown { .. }));
}
#[test]
fn every_outcome_prints_both_versions() {
for decision in [
decide("0.1.5", "0.1.6"),
decide("0.1.5", "0.1.5"),
decide("0.1.5", "nightly"),
] {
let line = describe(&decision);
assert!(line.contains("0.1.5"), "{line}");
}
assert!(describe(&decide("0.1.5", "0.1.6")).contains("update is available"));
assert!(describe(&decide("0.1.5", "0.1.5")).contains("up to date"));
}
#[test]
fn both_linux_architectures_have_a_prebuilt_asset() {
assert!(has_prebuilt_asset("linux", "x86_64"));
assert!(has_prebuilt_asset("linux", "aarch64"));
assert!(has_prebuilt_asset("linux", "arm64"));
assert!(!has_prebuilt_asset("linux", "armv7"));
assert!(!has_prebuilt_asset("linux", "armv7l"));
assert!(!has_prebuilt_asset("macos", "x86_64"));
assert!(!has_prebuilt_asset("macos", "aarch64"));
}
#[test]
fn the_asset_name_follows_the_running_arch() {
assert_eq!(
update::asset_name("linux", "aarch64").as_deref(),
Some("mobux-aarch64-unknown-linux-gnu.tar.gz")
);
assert_eq!(
update::asset_name("linux", "x86_64").as_deref(),
Some("mobux-x86_64-unknown-linux-gnu.tar.gz")
);
assert_eq!(
update::asset_name("linux", "arm64").as_deref(),
Some("mobux-aarch64-unknown-linux-gnu.tar.gz")
);
assert_eq!(update::asset_name("linux", "armv7l"), None);
assert_eq!(update::asset_name("macos", "aarch64"), None);
}
#[tokio::test]
async fn the_check_decides_from_what_the_check_url_serves() {
let url = update::serve_once(
"200 OK",
"{\"name\":\"mobux\",\"vers\":\"999.0.0\",\"yanked\":false}\n",
)
.await;
let latest = update::fetch_latest_version_from(&url).await.unwrap();
assert!(matches!(
decide(update::UpdateState::current_version(), &latest),
Decision::Available { .. }
));
}
}