fix: release v1.7.50-alpha OTA runtime repair
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
//! best-effort — failures are logged but never abort the backend.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
@@ -31,6 +31,7 @@ const DOCTOR_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-doctor.servic
|
||||
const DOCTOR_TIMER_PATH: &str = "/etc/systemd/system/archipelago-doctor.timer";
|
||||
|
||||
const NGINX_CONF_PATH: &str = "/etc/nginx/sites-available/archipelago";
|
||||
const RUNTIME_ASSETS_DIR: &str = "/opt/archipelago/web-ui/archipelago-runtime";
|
||||
|
||||
/// Inserted into every server block of the nginx config that lacks the
|
||||
/// `/api/app-catalog` proxy. Kept in sync with the canonical block in
|
||||
@@ -40,6 +41,11 @@ const NGINX_APP_CATALOG_BLOCK: &str = "\n # App Store catalog proxy — backe
|
||||
/// Entry point called from main startup. Never returns an error to the caller —
|
||||
/// failing to bootstrap host artifacts must not prevent the backend from serving.
|
||||
pub async fn ensure_doctor_installed() {
|
||||
match run_runtime_assets().await {
|
||||
Ok(changed) if changed => info!("Runtime assets synchronized from OTA payload"),
|
||||
Ok(_) => debug!("No OTA runtime payload to synchronize"),
|
||||
Err(e) => warn!("Runtime asset bootstrap failed (non-fatal): {:#}", e),
|
||||
}
|
||||
match run().await {
|
||||
Ok(changed) if changed => info!("Doctor artifacts synchronized with binary"),
|
||||
Ok(_) => debug!("Doctor artifacts already in sync"),
|
||||
@@ -52,6 +58,117 @@ pub async fn ensure_doctor_installed() {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_runtime_assets() -> Result<bool> {
|
||||
// The v1.7.50 OTA bridge puts scripts/apps/docker assets inside the
|
||||
// frontend tarball because older binaries only know how to apply the
|
||||
// backend binary and frontend archive. Once the new backend starts, it
|
||||
// promotes that payload into /opt so app installs use the matching specs.
|
||||
let runtime_dir = Path::new(RUNTIME_ASSETS_DIR);
|
||||
if !runtime_dir.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
for (relative, dest) in [
|
||||
("apps", "/opt/archipelago/apps"),
|
||||
("scripts", "/opt/archipelago/scripts"),
|
||||
("docker", "/opt/archipelago/docker"),
|
||||
] {
|
||||
let src = runtime_dir.join(relative);
|
||||
if src.exists() {
|
||||
replace_dir_from_runtime(&src, dest).await?;
|
||||
if relative == "scripts" {
|
||||
let _ = host_sudo(&[
|
||||
"find", dest, "-type", "f", "-name", "*.sh", "-exec", "chmod", "755", "{}", "+",
|
||||
])
|
||||
.await;
|
||||
let image_versions = format!("{}/image-versions.sh", dest);
|
||||
if Path::new(&image_versions).exists() {
|
||||
let _ =
|
||||
host_sudo(&["cp", &image_versions, "/opt/archipelago/image-versions.sh"])
|
||||
.await;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
let configs = runtime_dir.join("image-recipe/configs");
|
||||
for unit in ["archipelago-doctor.service", "archipelago-doctor.timer"] {
|
||||
let src = configs.join(unit);
|
||||
if src.exists() {
|
||||
let src_s = src.to_string_lossy().to_string();
|
||||
let dest = format!("/etc/systemd/system/{}", unit);
|
||||
let status = host_sudo(&["install", "-m", "644", &src_s, &dest])
|
||||
.await
|
||||
.with_context(|| format!("install {}", unit))?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("install {} exited with {}", unit, status);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
let _ = host_sudo(&["systemctl", "daemon-reload"]).await;
|
||||
let _ = host_sudo(&["systemctl", "enable", "--now", "archipelago-doctor.timer"]).await;
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
async fn replace_dir_from_runtime(src: &Path, dest: &str) -> Result<()> {
|
||||
let tmp = format!("{}.new.{}", dest, chrono::Utc::now().timestamp_millis());
|
||||
let src_dot = path_dot(src);
|
||||
let mkdir = host_sudo(&["mkdir", "-p", &tmp])
|
||||
.await
|
||||
.with_context(|| format!("mkdir {}", tmp))?;
|
||||
if !mkdir.success() {
|
||||
anyhow::bail!("mkdir {} exited with {}", tmp, mkdir);
|
||||
}
|
||||
let copy = host_sudo(&["cp", "-a", &src_dot, &tmp])
|
||||
.await
|
||||
.with_context(|| format!("copy runtime {} -> {}", src.display(), tmp))?;
|
||||
if !copy.success() {
|
||||
let _ = host_sudo(&["rm", "-rf", &tmp]).await;
|
||||
anyhow::bail!("copy runtime {} exited with {}", src.display(), copy);
|
||||
}
|
||||
let _ = host_sudo(&["mkdir", "-p", dest]).await;
|
||||
let cleanup = host_sudo(&[
|
||||
"find",
|
||||
dest,
|
||||
"-mindepth",
|
||||
"1",
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-exec",
|
||||
"rm",
|
||||
"-rf",
|
||||
"{}",
|
||||
"+",
|
||||
])
|
||||
.await
|
||||
.with_context(|| format!("clean {}", dest))?;
|
||||
if !cleanup.success() {
|
||||
let _ = host_sudo(&["rm", "-rf", &tmp]).await;
|
||||
anyhow::bail!("clean {} exited with {}", dest, cleanup);
|
||||
}
|
||||
let tmp_dot = format!("{}/.", tmp);
|
||||
let promote = host_sudo(&["cp", "-a", &tmp_dot, dest])
|
||||
.await
|
||||
.with_context(|| format!("promote {} -> {}", tmp, dest))?;
|
||||
let _ = host_sudo(&["rm", "-rf", &tmp]).await;
|
||||
if !promote.success() {
|
||||
anyhow::bail!("promote {} exited with {}", dest, promote);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_dot(path: &Path) -> String {
|
||||
let mut p = PathBuf::from(path);
|
||||
p.push(".");
|
||||
p.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
async fn run() -> Result<bool> {
|
||||
// Dev-box guard: on contributors' laptops `/home/archipelago/archy` is
|
||||
// typically a symlink into the git checkout, and writing through it
|
||||
|
||||
Reference in New Issue
Block a user