use std::path::PathBuf;
use clap::Parser;
use binstalk_downloader::download::{Download, PkgFmt};
use detect_targets::{TARGET, get_desired_targets};
use miette::{IntoDiagnostic, Result, miette};
use tracing::info;
use crate::{
args::Args,
download::{DownloadSource, client},
};
use super::Context;
#[cfg(unix)]
fn check_exe_writable() -> Result<()> {
let exe_path = std::env::current_exe().into_diagnostic()?;
let exe_dir = exe_path
.parent()
.ok_or_else(|| miette!("current exe is not in a directory"))?;
let test_file = exe_dir.join(".bestool_write_test");
if std::fs::write(&test_file, b"test").is_err() {
return Err(miette!(
"Cannot write to executable directory: {}\n\
Please run with sudo: sudo bestool self-update",
exe_dir.display()
));
}
let _ = std::fs::remove_file(test_file);
Ok(())
}
#[cfg(unix)]
pub(crate) fn is_package_manager_install() -> bool {
std::path::Path::new("/usr/share/doc/bestool/copyright").exists()
}
#[cfg(not(unix))]
pub(crate) fn is_package_manager_install() -> bool {
false
}
#[cfg(unix)]
fn check_package_manager_install(force: bool) -> Result<()> {
if is_package_manager_install() && !force {
return Err(miette!(
"bestool appears to be installed via a package manager.\n\
Please use your package manager to update bestool (e.g., 'apt update && apt upgrade bestool').\n\
If you want to override this and self-update anyway, use: bestool self-update --force"
));
}
Ok(())
}
#[derive(Debug, Clone, Parser)]
pub struct SelfUpdateArgs {
#[arg(long, default_value = "latest")]
pub version: String,
#[arg(long)]
pub target: Option<String>,
#[arg(long)]
pub temp_dir: Option<PathBuf>,
#[cfg(windows)]
#[arg(short = 'P', long)]
pub add_to_path: bool,
#[arg(long)]
pub force: bool,
}
pub async fn run(ctx: Context<Args, SelfUpdateArgs>) -> Result<()> {
#[cfg(unix)]
{
check_exe_writable()?;
check_package_manager_install(ctx.args_sub.force)?;
}
let SelfUpdateArgs {
version,
target,
temp_dir,
#[cfg(windows)]
add_to_path,
..
} = ctx.args_sub;
let client = client().await?;
let detected_targets = get_desired_targets(target.map(|t| vec![t]));
let detected_targets = detected_targets.get().await;
let dir = temp_dir.unwrap_or_else(std::env::temp_dir);
let filename = format!(
"bestool{ext}",
ext = if cfg!(windows) { ".exe" } else { "" }
);
let dest = dir.join(&filename);
let _ = tokio::fs::remove_file(&dest).await;
let host = DownloadSource::Tools.host();
let url = host
.join(&format!(
"/bestool/{version}/{target}/{filename}",
target = detected_targets
.first()
.cloned()
.unwrap_or_else(|| TARGET.into()),
))
.into_diagnostic()?;
info!(url = %url, "downloading");
Download::new(client, url)
.and_extract(PkgFmt::Bin, &dest)
.await
.into_diagnostic()?;
#[cfg(windows)]
if add_to_path && let Err(err) = add_self_to_path() {
tracing::error!("{err:?}");
}
info!(?dest, "downloaded, self-upgrading");
upgrade::run_upgrade(&dest, true, vec!["--version"])
.map_err(|err| miette!("upgrade: {err:?}"))?;
#[cfg(windows)]
if is_alertd_service_running().await {
if let Err(err) = schedule_service_restart() {
tracing::warn!("failed to schedule service restart: {err:?}");
}
}
Ok(())
}
#[cfg(windows)]
fn add_self_to_path() -> Result<()> {
let self_path = std::env::current_exe().into_diagnostic()?;
let self_dir = self_path
.parent()
.ok_or_else(|| miette!("current exe is not in a dir?"))?;
let self_dir = self_dir
.to_str()
.ok_or_else(|| miette!("current exe path is not utf-8"))?;
windows_env::prepend("PATH", self_dir).into_diagnostic()?;
Ok(())
}
#[cfg(windows)]
async fn is_alertd_service_running() -> bool {
match reqwest::get("http://127.0.0.1:8271/status").await {
Ok(response) => response.status().is_success(),
Err(_) => false,
}
}
#[cfg(windows)]
fn schedule_service_restart() -> Result<()> {
use std::process::Command;
let ps_command = "Start-Sleep -Seconds 60; Restart-Service -Name bestool-alertd -Force";
let output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
&format!("Start-Process powershell -ArgumentList '-NoProfile', '-Command', '{}' -WindowStyle Hidden", ps_command),
])
.output()
.into_diagnostic()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(miette!("failed to schedule service restart: {}", stderr));
}
info!("scheduled service restart for 1 minute later");
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use std::fs;
use tempfile::TempDir;
#[test]
fn test_check_exe_writable_with_writable_dir() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join(".bestool_write_test");
assert!(fs::write(&test_file, b"test").is_ok());
assert!(fs::remove_file(test_file).is_ok());
}
#[test]
fn test_check_exe_writable_with_readonly_dir() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
let mut perms = fs::metadata(temp_path).unwrap().permissions();
perms.set_mode(0o555);
fs::set_permissions(temp_path, perms).unwrap();
let test_file = temp_path.join(".bestool_write_test");
assert!(fs::write(&test_file, b"test").is_err());
let mut perms = fs::metadata(temp_path).unwrap().permissions();
perms.set_mode(0o755);
let _ = fs::set_permissions(temp_path, perms);
}
}