diff --git a/core/Cargo.lock b/core/Cargo.lock index a6699b34..2df3e3c1 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.5-alpha" +version = "1.7.6-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 30f2a017..0616123d 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.5-alpha" +version = "1.7.6-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/api/rpc/update.rs b/core/archipelago/src/api/rpc/update.rs index 0db8b0fd..277f27b6 100644 --- a/core/archipelago/src/api/rpc/update.rs +++ b/core/archipelago/src/api/rpc/update.rs @@ -6,12 +6,18 @@ impl RpcHandler { /// Check for available system updates. /// Tries git-based check first (if repo exists), falls back to manifest-based. pub(super) async fn handle_update_check(&self) -> Result { - // 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( std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()), ) .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 { return Ok(git_status); } diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index c3e3cc2d..3cef79a0 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -305,45 +305,47 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> { info!(name = %name, "Backend binary applied"); } _ if name.contains("frontend") && name.ends_with(".tar.gz") => { - // The tarball contents are the *inside* of web-ui/ — root - // entries are `./test-aiui.html`, `./assets/`, etc. Extract - // into a sibling staging dir, then swap atomically so - // nginx never sees a half-written tree. - let new_dir = "/opt/archipelago/web-ui.new"; + // Tarball contents are the *inside* of web-ui/ (root entries + // `./test-aiui.html`, `./assets/`, ...). Extract into a + // uniquely-named staging dir, then mv into place. No `rm + // -rf` pre-cleanup — that's what hit transient EROFS on + // .198 and aborted the apply mid-flight. + let ts = chrono::Utc::now().timestamp_millis(); + let staging_new = format!("/opt/archipelago/web-ui.new.{}", ts); + let staging_old = format!("/opt/archipelago/web-ui.old.{}", ts); + let web_ui = "/opt/archipelago/web-ui"; let backup_path = "/opt/archipelago/web-ui.bak"; - // Wipe any previous attempt's staging / backup dirs. - let _ = tokio::process::Command::new("sudo") - .args(["rm", "-rf", new_dir, backup_path]) - .status() - .await; + let mk = tokio::process::Command::new("sudo") - .args(["mkdir", "-p", new_dir]) + .args(["mkdir", "-p", &staging_new]) .status() .await .context("Failed to create frontend staging dir")?; if !mk.success() { - anyhow::bail!("mkdir {} failed", new_dir); + anyhow::bail!("mkdir {} failed", staging_new); } - // Extract INTO the staging dir — tar's ./ entries land at - // the right place (web-ui.new/assets/... etc.). - let status = tokio::process::Command::new("sudo") - .args(["tar", "-xzf", &src.to_string_lossy(), "-C", new_dir]) + let extract = tokio::process::Command::new("sudo") + .args(["tar", "-xzf", &src.to_string_lossy(), "-C", &staging_new]) .status() .await .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); } - // Ownership: match what first-boot + the ISO expect. let _ = tokio::process::Command::new("sudo") - .args(["chown", "-R", "archipelago:archipelago", new_dir]) + .args(["chown", "-R", "archipelago:archipelago", &staging_new]) .status() .await; - // Atomic-ish swap: move old aside, new into place. - let web_ui = "/opt/archipelago/web-ui"; + + // 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, backup_path]) + .args(["mv", web_ui, &staging_old]) .status() .await .context("Failed to rotate old web-ui")?; @@ -352,13 +354,41 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> { } } let mv_new = tokio::process::Command::new("sudo") - .args(["mv", new_dir, web_ui]) + .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"); } _ => {