Compare commits

...

4 Commits

Author SHA1 Message Date
Dorian
cbf30e2e29 release(v1.7.8-alpha): fix apply ETXTBSY — use mv instead of install
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
apply_update's binary swap called `sudo install -m 0755 src
/usr/local/bin/archipelago`. install opens the destination for write
with O_TRUNC; the kernel returns ETXTBSY (exit 1) when the path is a
currently-running executable, which it always is during apply because
apply_update is called by the archipelago RPC handler — running as
archipelago itself. Every previous "Failed to apply update" was this
one root cause; the manual sideload path only worked because we
stopped the service first.

rename() doesn't modify the file it replaces — it repoints the path
at a new inode while the old inode stays alive for any process that
has it mapped. `mv` uses rename(). Switched to `sudo mv` (with prior
chmod+chown on the staging file) so the swap is atomic and tolerant
of the running binary.

Frontend tarball byte-identical to v1.7.7-alpha; only the binary
version string changes.

Artefacts:
  archipelago                                      2753daec…48094d  40377648
  archipelago-frontend-1.7.8-alpha.tar.gz          4fb79664…0172e9  76984615 (reused)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:04:09 -04:00
Dorian
23cc78f0db release(v1.7.7-alpha): clean OTA test bump on top of robust apply
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Pure version bump. No code changes. First release shipped with the
reinforced apply_update (timestamped staging + all-mv) and frontend
with 95% progress cap — this OTA should walk through cleanly from
.116/.198/.253 without any sideload intervention.

Artefacts:
  archipelago                                      e3f1740d…006025  40373392
  archipelago-frontend-1.7.7-alpha.tar.gz          4fb79664…0172e9  76984615 (reused)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:44:19 -04:00
Dorian
9c6251c784 release(v1.7.6-alpha): robust apply_update + manifest-override env var
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 9m36s
apply_update frontend swap
  Transient EROFS on .198 (filesystem hiccup — root FS mounts with
  errors=remount-ro so a fleeting glitch can bounce /opt to RO for a
  moment) caught the pre-cleanup `rm -rf web-ui.new web-ui.bak` mid-
  stride and aborted the apply. Rewrote the swap to use a timestamped
  staging dir (web-ui.new.<ms>) and a timestamped old-copy path so
  nothing needs to be rm'd before the extract. After the new tree is
  mv'd into place, the previous rollback copy is rotated aside with a
  .<ms> suffix (best-effort) and this apply's old copy becomes the new
  web-ui.bak. If the final mv fails, the staged old is restored so
  nginx keeps serving.

handle_update_check manifest override
  handle_update_check takes the git path whenever ~/archy/.git exists.
  On the dev box (.116) that meant the Pull & Rebuild button was
  always the only option even though the manifest-path OTA was
  already wired via ARCHIPELAGO_UPDATE_URL. Now: if that env var is
  set, we skip the git detection entirely and use the manifest path.
  The regular fleet (no env var, no repo) hits the manifest branch
  naturally; beta dev nodes (repo + no env var) still get Pull &
  Rebuild; dev nodes with the env var explicitly set can finally test
  the manifest OTA end-to-end.

Artefacts:
  archipelago                                      356e78cc…91a6dd  40372288
  archipelago-frontend-1.7.6-alpha.tar.gz          4fb79664…0172e9  76984615 (reused)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:33:10 -04:00
Dorian
12f48a21c1 release(v1.7.5-alpha): OTA end-to-end test bump
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 9m55s
Trivial version-only bump. No code changes; binary differs only in its
embedded CARGO_PKG_VERSION string. Frontend tarball is byte-identical
to v1.7.4-alpha's (same sha), copied under the new filename to satisfy
the manifest component naming.

This exists so the fleet nodes (.116/.198/.253), all now running
v1.7.4-alpha with the fixed apply_update tar flow, can exercise the
full OTA pipeline from the UI: Check → Download (30-min timeout) →
Install (sudo install binary + sudo tar to web-ui.new + atomic swap) →
auto-restart (systemctl --no-block) → sidebar updates → state sync.

Artefacts:
  archipelago                                      7422a695…a1a2a6  40362432
  archipelago-frontend-1.7.5-alpha.tar.gz          4fb79664…0172e9  76984615 (reused)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:10:50 -04:00
13 changed files with 94 additions and 55 deletions

2
core/Cargo.lock generated
View File

@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.7.4-alpha"
version = "1.7.8-alpha"
dependencies = [
"anyhow",
"archipelago-container",

View File

@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.4-alpha"
version = "1.7.8-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]

View File

@@ -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<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(
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);
}

View File

@@ -277,27 +277,31 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
match name.as_str() {
"archipelago" => {
// /usr/local/bin is root-owned; archipelago user can't
// fs::copy into it directly. Use sudo install which handles
// the copy, mode, and ownership atomically.
// We're running FROM /usr/local/bin/archipelago right now,
// so we can't rewrite it in place — `install` / `cp` would
// 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")
.args([
"install",
"-m",
"0755",
"-o",
"root",
"-g",
"root",
&src.to_string_lossy(),
"/usr/local/bin/archipelago",
])
.args(["mv", &staged, "/usr/local/bin/archipelago"])
.status()
.await
.with_context(|| format!("Failed to spawn install for {}", name))?;
.with_context(|| format!("Failed to spawn mv for {}", name))?;
if !status.success() {
anyhow::bail!(
"sudo install failed for {} (exit {:?})",
"sudo mv failed for {} (exit {:?})",
name,
status.code()
);
@@ -305,45 +309,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 +358,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");
}
_ => {

View File

@@ -1,24 +1,23 @@
{
"version": "1.7.4-alpha",
"version": "1.7.8-alpha",
"release_date": "2026-04-20",
"changelog": [
"Install Update actually installs now. Before, the final step extracted the new UI into the wrong folder and bailed with 'Failed to apply update' — your node ended up backing up cleanly but never swapping in the new files. Fixed.",
"Download progress no longer overshoots 100%. You'll see the bar climb smoothly to 95% and then jump to 100% when the download actually finishes."
"Install Update finally works end-to-end over the air. The installer was trying to overwrite the running backend binary with a tool that fails on in-use files (ETXTBSY) — swapped it for an atomic rename, which the kernel allows on a live executable. Every previous 'Failed to apply update' attempt was this one root cause."
],
"components": [
{
"name": "archipelago",
"current_version": "1.7.3-alpha",
"new_version": "1.7.4-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.4-alpha/archipelago",
"sha256": "a14ad7e4dbcb8f74377d44a4bd5e600b285481df3b30c08f8bea2cd17e2a2be3",
"size_bytes": 40361984
"current_version": "1.7.7-alpha",
"new_version": "1.7.8-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.8-alpha/archipelago",
"sha256": "2753daec113bb4fbbc2a68148ef1579524a26707733eea410faf400b9948094d",
"size_bytes": 40377648
},
{
"name": "archipelago-frontend-1.7.4-alpha.tar.gz",
"current_version": "1.7.3-alpha",
"new_version": "1.7.4-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.4-alpha/archipelago-frontend-1.7.4-alpha.tar.gz",
"name": "archipelago-frontend-1.7.8-alpha.tar.gz",
"current_version": "1.7.7-alpha",
"new_version": "1.7.8-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.8-alpha/archipelago-frontend-1.7.8-alpha.tar.gz",
"sha256": "4fb796643cc9dc8469078ca3392f7cc5541071f6849979922b3259e5f20172e9",
"size_bytes": 76984615
}

BIN
releases/v1.7.5-alpha/archipelago Executable file

Binary file not shown.

BIN
releases/v1.7.6-alpha/archipelago Executable file

Binary file not shown.

BIN
releases/v1.7.7-alpha/archipelago Executable file

Binary file not shown.

BIN
releases/v1.7.8-alpha/archipelago Executable file

Binary file not shown.