Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61bfdac7cc | ||
|
|
1e648800ad | ||
|
|
8a2cabdba9 | ||
|
|
4d1bd063e8 | ||
|
|
b8397b5ecb | ||
|
|
1e7df417a4 |
Generated
+1
-1
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.3-alpha"
|
version = "1.7.9-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.3-alpha"
|
version = "1.7.9-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
authors = ["Archipelago Team"]
|
authors = ["Archipelago Team"]
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ impl RpcHandler {
|
|||||||
/// Check for available system updates.
|
/// Check for available system updates.
|
||||||
/// Tries git-based check first (if repo exists), falls back to manifest-based.
|
/// Tries git-based check first (if repo exists), falls back to manifest-based.
|
||||||
pub(super) async fn handle_update_check(&self) -> Result<serde_json::Value> {
|
pub(super) async fn handle_update_check(&self) -> Result<serde_json::Value> {
|
||||||
// Try git-based check first (preferred for beta nodes)
|
// Manifest override: when ARCHIPELAGO_UPDATE_URL is explicitly set,
|
||||||
|
// the operator wants OTA via manifest — typically a dev box where
|
||||||
|
// ~/archy/.git exists but isn't the intended update surface.
|
||||||
|
// Without this short-circuit the dev box always advertises "Pull
|
||||||
|
// & Rebuild" and can never exercise the manifest OTA path.
|
||||||
|
let manifest_override = std::env::var("ARCHIPELAGO_UPDATE_URL").is_ok();
|
||||||
|
|
||||||
let repo_dir = std::path::PathBuf::from(
|
let repo_dir = std::path::PathBuf::from(
|
||||||
std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()),
|
std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()),
|
||||||
)
|
)
|
||||||
.join("archy");
|
.join("archy");
|
||||||
if repo_dir.join(".git").exists() {
|
if !manifest_override && repo_dir.join(".git").exists() {
|
||||||
if let Ok(git_status) = self.git_check_update(&repo_dir).await {
|
if let Ok(git_status) = self.git_check_update(&repo_dir).await {
|
||||||
return Ok(git_status);
|
return Ok(git_status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,27 +277,31 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
|||||||
|
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
"archipelago" => {
|
"archipelago" => {
|
||||||
// /usr/local/bin is root-owned; archipelago user can't
|
// We're running FROM /usr/local/bin/archipelago right now,
|
||||||
// fs::copy into it directly. Use sudo install which handles
|
// so we can't rewrite it in place — `install` / `cp` would
|
||||||
// the copy, mode, and ownership atomically.
|
// hit ETXTBSY on the busy executable. Use `mv` instead:
|
||||||
|
// rename() is atomic and doesn't modify the existing file,
|
||||||
|
// it just re-points the path at a new inode. The currently
|
||||||
|
// running process keeps executing off the old inode; new
|
||||||
|
// invocations (i.e. after the post-apply systemctl
|
||||||
|
// restart) pick up the new binary.
|
||||||
|
let staged = src.to_string_lossy().to_string();
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["chmod", "0755", &staged])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["chown", "root:root", &staged])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
let status = tokio::process::Command::new("sudo")
|
let status = tokio::process::Command::new("sudo")
|
||||||
.args([
|
.args(["mv", &staged, "/usr/local/bin/archipelago"])
|
||||||
"install",
|
|
||||||
"-m",
|
|
||||||
"0755",
|
|
||||||
"-o",
|
|
||||||
"root",
|
|
||||||
"-g",
|
|
||||||
"root",
|
|
||||||
&src.to_string_lossy(),
|
|
||||||
"/usr/local/bin/archipelago",
|
|
||||||
])
|
|
||||||
.status()
|
.status()
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("Failed to spawn install for {}", name))?;
|
.with_context(|| format!("Failed to spawn mv for {}", name))?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"sudo install failed for {} (exit {:?})",
|
"sudo mv failed for {} (exit {:?})",
|
||||||
name,
|
name,
|
||||||
status.code()
|
status.code()
|
||||||
);
|
);
|
||||||
@@ -305,42 +309,90 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
|||||||
info!(name = %name, "Backend binary applied");
|
info!(name = %name, "Backend binary applied");
|
||||||
}
|
}
|
||||||
_ if name.contains("frontend") && name.ends_with(".tar.gz") => {
|
_ if name.contains("frontend") && name.ends_with(".tar.gz") => {
|
||||||
let web_ui_dir = Path::new("/opt/archipelago/web-ui");
|
// Tarball contents are the *inside* of web-ui/ (root entries
|
||||||
// Back up current frontend. /opt/archipelago is root-owned;
|
// `./test-aiui.html`, `./assets/`, ...). Extract into a
|
||||||
// the backup goes under our data_dir where we can write.
|
// uniquely-named staging dir, then mv into place. No `rm
|
||||||
let frontend_backup = backup_dir.join("web-ui-backup.tar.gz");
|
// -rf` pre-cleanup — that's what hit transient EROFS on
|
||||||
if web_ui_dir.exists() {
|
// .198 and aborted the apply mid-flight.
|
||||||
let status = tokio::process::Command::new("sudo")
|
let ts = chrono::Utc::now().timestamp_millis();
|
||||||
.args([
|
let staging_new = format!("/opt/archipelago/web-ui.new.{}", ts);
|
||||||
"tar",
|
let staging_old = format!("/opt/archipelago/web-ui.old.{}", ts);
|
||||||
"-czf",
|
let web_ui = "/opt/archipelago/web-ui";
|
||||||
&frontend_backup.to_string_lossy(),
|
let backup_path = "/opt/archipelago/web-ui.bak";
|
||||||
"-C",
|
|
||||||
"/opt/archipelago",
|
let mk = tokio::process::Command::new("sudo")
|
||||||
"web-ui",
|
.args(["mkdir", "-p", &staging_new])
|
||||||
])
|
.status()
|
||||||
.status()
|
.await
|
||||||
.await
|
.context("Failed to create frontend staging dir")?;
|
||||||
.context("Failed to backup frontend")?;
|
if !mk.success() {
|
||||||
if status.success() {
|
anyhow::bail!("mkdir {} failed", staging_new);
|
||||||
info!("Frontend backed up");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Extract new frontend into /opt/archipelago (root-owned dir).
|
let extract = tokio::process::Command::new("sudo")
|
||||||
let status = tokio::process::Command::new("sudo")
|
.args(["tar", "-xzf", &src.to_string_lossy(), "-C", &staging_new])
|
||||||
.args(["tar", "-xzf", &src.to_string_lossy(), "-C", "/opt/archipelago"])
|
|
||||||
.status()
|
.status()
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("Failed to extract {}", name))?;
|
.with_context(|| format!("Failed to extract {}", name))?;
|
||||||
if !status.success() {
|
if !extract.success() {
|
||||||
|
// Best-effort cleanup of the partial extraction.
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["rm", "-rf", &staging_new])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
anyhow::bail!("tar extraction failed for {}", name);
|
anyhow::bail!("tar extraction failed for {}", name);
|
||||||
}
|
}
|
||||||
// nginx serves this tree; keep ownership consistent with
|
|
||||||
// what first-boot + the ISO layout expect.
|
|
||||||
let _ = tokio::process::Command::new("sudo")
|
let _ = tokio::process::Command::new("sudo")
|
||||||
.args(["chown", "-R", "archipelago:archipelago", "/opt/archipelago/web-ui"])
|
.args(["chown", "-R", "archipelago:archipelago", &staging_new])
|
||||||
.status()
|
.status()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
// Swap: mv current web-ui aside, then mv new into place.
|
||||||
|
if Path::new(web_ui).exists() {
|
||||||
|
let mv_old = tokio::process::Command::new("sudo")
|
||||||
|
.args(["mv", web_ui, &staging_old])
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.context("Failed to rotate old web-ui")?;
|
||||||
|
if !mv_old.success() {
|
||||||
|
anyhow::bail!("failed to move old web-ui aside");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mv_new = tokio::process::Command::new("sudo")
|
||||||
|
.args(["mv", &staging_new, web_ui])
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.context("Failed to swap new web-ui into place")?;
|
||||||
|
if !mv_new.success() {
|
||||||
|
// Roll back the rename so nginx keeps serving.
|
||||||
|
if Path::new(&staging_old).exists() {
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["mv", &staging_old, web_ui])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
anyhow::bail!("failed to move new web-ui into place");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate previous rollback aside (best-effort) and install
|
||||||
|
// this apply's old copy as the new rollback.
|
||||||
|
if Path::new(&staging_old).exists() {
|
||||||
|
if Path::new(backup_path).exists() {
|
||||||
|
// Tag the previous backup with its own ts so it
|
||||||
|
// doesn't collide; best-effort cleanup.
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args([
|
||||||
|
"mv",
|
||||||
|
backup_path,
|
||||||
|
&format!("{}.{}", backup_path, ts),
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["mv", &staging_old, backup_path])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui");
|
info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -345,10 +345,12 @@ async function downloadUpdate() {
|
|||||||
downloadPercent.value = 0
|
downloadPercent.value = 0
|
||||||
statusMessage.value = ''
|
statusMessage.value = ''
|
||||||
|
|
||||||
// Simulate incremental progress while waiting for the RPC
|
// Simulate incremental progress while waiting for the RPC. Capped at
|
||||||
|
// 95% so the bar never shows >100% before the real completion jumps it
|
||||||
|
// to 100 — previously the random increment could overshoot.
|
||||||
const progressInterval = setInterval(() => {
|
const progressInterval = setInterval(() => {
|
||||||
if (downloadPercent.value < 90) {
|
if (downloadPercent.value < 95) {
|
||||||
downloadPercent.value += Math.random() * 15
|
downloadPercent.value = Math.min(95, downloadPercent.value + Math.random() * 3)
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user