use std::{
collections::hash_map::DefaultHasher,
hash::{Hash as _, Hasher as _},
sync::{Arc, Mutex},
time::Duration,
};
use futures::future::BoxFuture;
use miette::Result;
use serde_json::json;
use tracing::{debug, info, warn};
use bestool_alertd::{BackgroundTask, TaskContext, TaskEndpoint, TaskEndpointResponse};
use super::{UpdateOutcome, perform_update, perform_update_from_file};
use crate::download::{fetch_latest_version, remote_is_newer};
const CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
const STAGGER_WINDOW: Duration = Duration::from_secs(60 * 60);
pub(crate) struct SelfUpdateTask {
failed_version: Arc<Mutex<Option<String>>>,
}
impl SelfUpdateTask {
pub(crate) fn new() -> Self {
Self {
failed_version: Arc::new(Mutex::new(None)),
}
}
async fn check_and_update(&self, ctx: &TaskContext) {
let current = env!("CARGO_PKG_VERSION");
let latest = match fetch_latest_version().await {
Ok(latest) => latest,
Err(err) => {
warn!("self-update: could not check latest version: {err}");
return;
}
};
if !remote_is_newer(current, &latest) {
debug!(current, %latest, "self-update: already current");
return;
}
if self.failed_version.lock().unwrap().as_deref() == Some(latest.as_str()) {
warn!(%latest, "self-update: skipping version that previously failed to install");
return;
}
info!(current, %latest, "self-update: newer version available, installing");
match perform_update(&latest, None, None, false).await {
Ok(UpdateOutcome::Updated { from, to }) => {
info!(%from, %to, "self-update installed; requesting daemon restart");
request_restart(ctx).await;
}
Ok(UpdateOutcome::AlreadyCurrent { .. }) => {}
Err(err) => {
warn!(%latest, "self-update failed; not retrying this version: {err}");
*self.failed_version.lock().unwrap() = Some(latest);
}
}
}
}
fn stagger_offset() -> Duration {
let host = std::env::var("COMPUTERNAME").unwrap_or_default();
let mut hasher = DefaultHasher::new();
host.hash(&mut hasher);
Duration::from_secs(hasher.finish() % STAGGER_WINDOW.as_secs())
}
async fn request_restart(ctx: &TaskContext) {
if let Some(restart) = &ctx.restart {
restart.request_restart().await;
} else {
warn!("self-update: no restart handle; the new binary takes effect on next restart");
}
}
impl BackgroundTask for SelfUpdateTask {
fn name(&self) -> &'static str {
"self-update"
}
fn interval(&self) -> Duration {
CHECK_INTERVAL
}
fn run<'a>(&'a self, ctx: &'a TaskContext) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
let offset = stagger_offset();
info!(?offset, "self-update: first check staggered");
tokio::time::sleep(offset).await;
let mut tick = tokio::time::interval(CHECK_INTERVAL);
loop {
tick.tick().await;
self.check_and_update(ctx).await;
}
})
}
fn http_endpoints(&self) -> Vec<TaskEndpoint> {
let failed_version = self.failed_version.clone();
vec![TaskEndpoint {
name: "update",
handler: Arc::new(move |ctx: TaskContext| {
Box::pin(on_demand_update(ctx, failed_version.clone()))
}),
}]
}
}
async fn on_demand_update(
ctx: TaskContext,
failed_version: Arc<Mutex<Option<String>>>,
) -> TaskEndpointResponse {
let current = env!("CARGO_PKG_VERSION");
if let Some(path) = ctx.query.get("from_file") {
let path = std::path::PathBuf::from(path);
let to = format!("file:{}", path.display());
let restart = ctx.restart.clone();
tokio::spawn(async move {
match perform_update_from_file(&path).await {
Ok(_) => {
info!(from = %path.display(), "on-demand self-update from file installed; restarting");
tokio::time::sleep(Duration::from_secs(1)).await;
if let Some(restart) = restart {
restart.request_restart().await;
}
}
Err(err) => {
warn!(from = %path.display(), "on-demand self-update from file failed: {err}");
*failed_version.lock().unwrap() = Some(format!("file:{}", path.display()));
}
}
});
return TaskEndpointResponse::Json(json!({
"updating": true,
"from": current,
"to": to,
}));
}
let requested = ctx
.query
.get("version")
.map(String::as_str)
.unwrap_or("latest");
let force = ctx.query.get("force").map(|v| v == "true").unwrap_or(false);
let resolved = if requested == "latest" {
match fetch_latest_version().await {
Ok(latest) => {
if !force && !remote_is_newer(current, &latest) {
return TaskEndpointResponse::Json(json!({
"updating": false,
"current": current,
"latest": latest,
}));
}
latest
}
Err(err) => {
return TaskEndpointResponse::Error {
status: 502,
message: format!("could not check latest version: {err}"),
};
}
}
} else {
requested.to_string()
};
let restart = ctx.restart.clone();
let to = resolved.clone();
tokio::spawn(async move {
match perform_update(&resolved, None, None, true).await {
Ok(_) => {
info!(version = %resolved, "on-demand self-update installed; restarting");
tokio::time::sleep(Duration::from_secs(1)).await;
if let Some(restart) = restart {
restart.request_restart().await;
}
}
Err(err) => {
warn!(version = %resolved, "on-demand self-update failed: {err}");
*failed_version.lock().unwrap() = Some(resolved);
}
}
});
TaskEndpointResponse::Json(json!({
"updating": true,
"from": current,
"to": to,
}))
}