Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eadec6936 | ||
|
|
6b0a84e710 | ||
|
|
fd361fb35e | ||
|
|
8bb61a51e2 | ||
|
|
17d225190a | ||
|
|
d08c0d29c7 | ||
|
|
a93bd70c5a | ||
|
|
25162ee846 | ||
|
|
837cfdfd1f |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.104-alpha (2026-07-19)
|
||||
|
||||
- Software updates are now much safer to receive: the node will never install an update that isn't completely downloaded and verified byte-for-byte, closing a rare bug where an interrupted or cancelled download could leave a node unable to start.
|
||||
- If a freshly installed update does fail to start, the node now notices and automatically restores the previous working version by itself — no manual rescue needed.
|
||||
- The Electrum server now works with whichever Bitcoin you run: it finds Bitcoin Knots or Bitcoin Core automatically instead of assuming Knots.
|
||||
- While the Electrum server is first building its index, its waiting screen now shows the ElectrumX app icon and live progress.
|
||||
|
||||
## v1.7.103-alpha (2026-07-18)
|
||||
|
||||
- Connecting from the phone app no longer replays the intro cinematic on a loop: signing in after scanning the pairing QR could accidentally trigger "Replay Intro" instead of logging you in. The companion app now lands you straight on your dashboard, and the Android app waits for the login screen to be ready before it types your password.
|
||||
|
||||
@@ -10,9 +10,16 @@ app:
|
||||
network: archy-net
|
||||
data_uid: "1000:1000"
|
||||
entrypoint: ["sh", "-lc"]
|
||||
# The bitcoin backend container is bitcoin-knots OR bitcoin-core depending
|
||||
# on which version the node runs (multi-version switch) — probe which name
|
||||
# resolves on archy-net instead of hardcoding knots, which left electrumx
|
||||
# permanently disconnected (block index 0) on core nodes.
|
||||
custom_args:
|
||||
- >-
|
||||
export DAEMON_URL="http://archipelago:$(printenv BITCOIN_RPC_PASS)@bitcoin-knots:8332/";
|
||||
for h in bitcoin-knots bitcoin-core; do
|
||||
if getent hosts "$h" >/dev/null 2>&1; then BTC_HOST="$h"; break; fi;
|
||||
done;
|
||||
export DAEMON_URL="http://archipelago:$(printenv BITCOIN_RPC_PASS)@${BTC_HOST:-bitcoin-knots}:8332/";
|
||||
exec electrumx_server
|
||||
secret_env:
|
||||
- key: BITCOIN_RPC_PASS
|
||||
|
||||
Generated
+1
-1
@@ -95,7 +95,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.103-alpha"
|
||||
version = "1.7.104-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.103-alpha"
|
||||
version = "1.7.104-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
+226
-38
@@ -24,6 +24,16 @@ pub static DOWNLOAD_CANCEL: AtomicBool = AtomicBool::new(false);
|
||||
/// confidence than "looks stuck at 0%".
|
||||
pub static DOWNLOAD_PROGRESS_AT: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Serializes the mutating update operations (download, apply, and the
|
||||
/// staging wipe in cancel). The .198 v1.7.103 bricking (2026-07-18) was
|
||||
/// exactly this race: two concurrent `update.download` RPCs shared one
|
||||
/// staging file, a cancel wiped staging mid-flight, a third download began
|
||||
/// re-filling it, and `apply_update` mv'd the 3-second-old 17MB partial of
|
||||
/// a 49MB binary into /usr/local/bin → SEGV boot loop. Writers take this
|
||||
/// via `try_lock` so a concurrent caller gets an explicit "already running"
|
||||
/// error instead of silently interleaving.
|
||||
static UPDATE_OP_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
||||
|
||||
fn now_ms() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now()
|
||||
@@ -976,6 +986,9 @@ pub async fn dismiss_update(data_dir: &Path) -> Result<()> {
|
||||
/// verified over the complete file at the end of each component, so a
|
||||
/// partially-corrupt resume still fails cleanly.
|
||||
pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
|
||||
let _op = UPDATE_OP_LOCK.try_lock().map_err(|_| {
|
||||
anyhow::anyhow!("another update operation (download or apply) is already running")
|
||||
})?;
|
||||
let mut state = load_state(data_dir).await?;
|
||||
if state.available_update.is_none() {
|
||||
state = check_for_updates(data_dir).await?;
|
||||
@@ -1133,7 +1146,6 @@ async fn download_component_resumable(
|
||||
dest: &Path,
|
||||
prior_total: u64,
|
||||
) -> Result<()> {
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
const MAX_ATTEMPTS: u32 = 6;
|
||||
const BACKOFFS: [u64; 5] = [5, 15, 30, 60, 120];
|
||||
@@ -1145,8 +1157,19 @@ async fn download_component_resumable(
|
||||
Err(_) => 0,
|
||||
};
|
||||
if existing_len >= component.size_bytes {
|
||||
// File is already complete — break out and go verify.
|
||||
break;
|
||||
// File is already complete (a resumed run finished it, or a
|
||||
// leftover from an earlier attempt) — verify it instead of
|
||||
// trusting it. The old code `break`d here, which skipped
|
||||
// verification entirely AND landed on the error return below
|
||||
// ("download failed without a captured error").
|
||||
match verify_component_on_disk(component, dest).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
let _ = tokio::fs::remove_file(dest).await;
|
||||
last_err = Some(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if attempt > 1 {
|
||||
let delay = BACKOFFS[(attempt as usize - 2).min(BACKOFFS.len() - 1)];
|
||||
@@ -1294,44 +1317,86 @@ async fn download_component_resumable(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Full file — verify hash.
|
||||
let bytes = tokio::fs::read(dest)
|
||||
.await
|
||||
.context("read staging file for hash check")?;
|
||||
let hash = hex::encode(Sha256::digest(&bytes));
|
||||
if hash == component.sha256 {
|
||||
// DHT Phase 1: if the manifest also pins a BLAKE3 digest, it must
|
||||
// match too. SHA-256 stays the mandatory gate during migration;
|
||||
// BLAKE3 is the hash the iroh swarm will fetch/verify by, so a
|
||||
// present-but-wrong BLAKE3 means the bytes aren't swarm-consistent
|
||||
// — treat it like a SHA mismatch and re-download.
|
||||
if let Some(b3) = component.blake3.as_deref() {
|
||||
let expected = b3.trim().strip_prefix("blake3:").unwrap_or(b3.trim());
|
||||
let actual = crate::content_hash::blake3_hex(&bytes);
|
||||
if !actual.eq_ignore_ascii_case(expected) {
|
||||
let _ = tokio::fs::remove_file(dest).await;
|
||||
last_err = Some(anyhow::anyhow!(
|
||||
"BLAKE3 mismatch for {}: expected {}, got {}",
|
||||
component.name,
|
||||
expected,
|
||||
actual
|
||||
));
|
||||
continue;
|
||||
}
|
||||
// Full file — verify hashes. On mismatch the file on disk is
|
||||
// garbage: nuke it and start over from scratch on the next attempt.
|
||||
match verify_component_on_disk(component, dest).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
let _ = tokio::fs::remove_file(dest).await;
|
||||
last_err = Some(e);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// SHA mismatch — the file on disk is garbage. Nuke it and
|
||||
// start over from scratch on the next attempt.
|
||||
let _ = tokio::fs::remove_file(dest).await;
|
||||
last_err = Some(anyhow::anyhow!(
|
||||
}
|
||||
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("download failed without a captured error")))
|
||||
}
|
||||
|
||||
/// Verify a fully-downloaded component file on disk: SHA-256 is the
|
||||
/// mandatory gate; when the manifest also pins a BLAKE3 digest it must
|
||||
/// match too (BLAKE3 is the hash the iroh swarm fetches/verifies by, so
|
||||
/// a present-but-wrong BLAKE3 means the bytes aren't swarm-consistent —
|
||||
/// treated exactly like a SHA mismatch). Err = mismatch; the caller
|
||||
/// decides whether to remove the file and retry.
|
||||
async fn verify_component_on_disk(component: &ComponentUpdate, dest: &Path) -> Result<()> {
|
||||
use sha2::{Digest, Sha256};
|
||||
let bytes = tokio::fs::read(dest)
|
||||
.await
|
||||
.context("read staging file for hash check")?;
|
||||
let hash = hex::encode(Sha256::digest(&bytes));
|
||||
if hash != component.sha256 {
|
||||
anyhow::bail!(
|
||||
"SHA256 mismatch for {}: expected {}, got {}",
|
||||
component.name,
|
||||
component.sha256,
|
||||
hash
|
||||
));
|
||||
);
|
||||
}
|
||||
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("download failed without a captured error")))
|
||||
if let Some(b3) = component.blake3.as_deref() {
|
||||
let expected = b3.trim().strip_prefix("blake3:").unwrap_or(b3.trim());
|
||||
let actual = crate::content_hash::blake3_hex(&bytes);
|
||||
if !actual.eq_ignore_ascii_case(expected) {
|
||||
anyhow::bail!(
|
||||
"BLAKE3 mismatch for {}: expected {}, got {}",
|
||||
component.name,
|
||||
expected,
|
||||
actual
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-verify every manifest component against the bytes actually sitting
|
||||
/// in staging, immediately before install. The download path verifies as
|
||||
/// it goes, but staging can change between download and apply — on .198
|
||||
/// (v1.7.103, 2026-07-18) a concurrent download was re-filling a wiped
|
||||
/// staging dir when apply ran, and a 17MB partial of the 49MB binary got
|
||||
/// installed. This apply-time gate is the one that must never be skipped.
|
||||
async fn verify_staged_components(staging_dir: &Path, manifest: &UpdateManifest) -> Result<()> {
|
||||
for component in &manifest.components {
|
||||
let dest = staging_dir.join(&component.name);
|
||||
let len = tokio::fs::metadata(&dest)
|
||||
.await
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
if len != component.size_bytes {
|
||||
anyhow::bail!(
|
||||
"staged component {} is {} bytes but the manifest says {} — \
|
||||
refusing to apply (incomplete or concurrently-rewritten download)",
|
||||
component.name,
|
||||
len,
|
||||
component.size_bytes
|
||||
);
|
||||
}
|
||||
verify_component_on_disk(component, &dest)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"staged component {} failed verification — refusing to apply",
|
||||
component.name
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel an in-flight download. Sets the cancellation flag so the
|
||||
@@ -1343,11 +1408,21 @@ pub async fn cancel_download(data_dir: &Path) -> Result<()> {
|
||||
DOWNLOAD_CANCEL.store(true, Ordering::Relaxed);
|
||||
DOWNLOAD_BYTES.store(0, Ordering::Relaxed);
|
||||
DOWNLOAD_TOTAL.store(0, Ordering::Relaxed);
|
||||
// Only wipe staging when no download/apply holds the op lock. Wiping
|
||||
// under a live operation is how .198 ended up applying a re-filling
|
||||
// staging dir; with the lock held elsewhere we just set the cancel
|
||||
// flag and let the in-flight loop bail at its next chunk boundary
|
||||
// (partials are size+hash revalidated on the next resume anyway).
|
||||
let staging = data_dir.join("update-staging");
|
||||
let wiped = if staging.exists() {
|
||||
tokio::fs::remove_dir_all(&staging).await.is_ok()
|
||||
} else {
|
||||
false
|
||||
let wiped = match UPDATE_OP_LOCK.try_lock() {
|
||||
Ok(_op) => {
|
||||
if staging.exists() {
|
||||
tokio::fs::remove_dir_all(&staging).await.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
// Clear the "downloaded, ready to apply" marker too — a canceled
|
||||
// download is not a staged update.
|
||||
@@ -1398,11 +1473,34 @@ pub(crate) async fn host_sudo(args: &[&str]) -> Result<std::process::ExitStatus>
|
||||
|
||||
/// Apply a downloaded update. Backs up current binaries, replaces with staged versions.
|
||||
pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
||||
let _op = UPDATE_OP_LOCK.try_lock().map_err(|_| {
|
||||
anyhow::anyhow!("another update operation (download or apply) is already running")
|
||||
})?;
|
||||
let staging_dir = data_dir.join("update-staging");
|
||||
if !staging_dir.exists() {
|
||||
anyhow::bail!("No staged update found. Download first.");
|
||||
}
|
||||
|
||||
// Gate 1: the completion marker is written only after EVERY component
|
||||
// downloaded and hash-verified. A staging dir without it is a partial
|
||||
// or in-flight download — exactly what got installed on .198.
|
||||
if !has_staged_update(data_dir).await {
|
||||
anyhow::bail!(
|
||||
"Staged update is incomplete (no completion marker) — download the update again before applying"
|
||||
);
|
||||
}
|
||||
|
||||
// Gate 2: re-verify the actual staged bytes against the manifest.
|
||||
let manifest = load_state(data_dir)
|
||||
.await?
|
||||
.available_update
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"no update manifest in state to verify staged files against — re-download the update"
|
||||
)
|
||||
})?;
|
||||
verify_staged_components(&staging_dir, &manifest).await?;
|
||||
|
||||
let backup_dir = data_dir.join("update-backup");
|
||||
fs::create_dir_all(&backup_dir)
|
||||
.await
|
||||
@@ -1690,6 +1788,30 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
||||
.await;
|
||||
}
|
||||
|
||||
// Install the OTA crash-loop guard as a drop-in on existing
|
||||
// nodes (fresh ISOs carry it in the unit file itself). The
|
||||
// guard restores the update-backup binary when a freshly
|
||||
// applied binary SEGVs before it can run its own post-OTA
|
||||
// verification — the .198 v1.7.103 truncated-binary loop.
|
||||
// Best-effort: `+-` in the drop-in means a missing script can
|
||||
// never block the service, and a failed install here must not
|
||||
// abort the apply.
|
||||
if Path::new("/opt/archipelago/scripts/ota-crash-guard.sh").exists() {
|
||||
let dropin_dir = "/etc/systemd/system/archipelago.service.d";
|
||||
let _ = host_sudo(&["mkdir", "-p", dropin_dir]).await;
|
||||
let _ = host_sudo(&[
|
||||
"bash",
|
||||
"-c",
|
||||
&format!(
|
||||
"printf '%s\\n' '[Service]' \
|
||||
'ExecStartPre=+-/opt/archipelago/scripts/ota-crash-guard.sh' \
|
||||
> {}/ota-crash-guard.conf",
|
||||
dropin_dir
|
||||
),
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
let _ = host_sudo(&["systemctl", "daemon-reload"]).await;
|
||||
let _ =
|
||||
host_sudo(&["systemctl", "enable", "--now", "archipelago-doctor.timer"]).await;
|
||||
@@ -2443,6 +2565,72 @@ mod tests {
|
||||
assert!(!persisted.update_in_progress);
|
||||
}
|
||||
|
||||
/// apply_update takes the global single-flight UPDATE_OP_LOCK, so tests
|
||||
/// that call it must not run concurrently — one would see the other's
|
||||
/// lock and fail with "another update operation is already running".
|
||||
static APPLY_TEST_SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_apply_refuses_unmarked_staging() {
|
||||
let _serial = APPLY_TEST_SERIAL.lock().await;
|
||||
// Regression: .198 v1.7.103 bricking — apply ran against a staging
|
||||
// dir that a concurrent download was still filling. Without the
|
||||
// .download-complete marker, apply must refuse before touching
|
||||
// anything.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let staging = dir.path().join("update-staging");
|
||||
tokio::fs::create_dir_all(&staging).await.unwrap();
|
||||
tokio::fs::write(staging.join("archipelago"), b"partial")
|
||||
.await
|
||||
.unwrap();
|
||||
let err = apply_update(dir.path()).await.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("completion marker"),
|
||||
"got: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_apply_refuses_staged_bytes_that_mismatch_manifest() {
|
||||
let _serial = APPLY_TEST_SERIAL.lock().await;
|
||||
// Marker present (a complete download once existed) but the staged
|
||||
// bytes no longer match the manifest — apply must re-verify and
|
||||
// refuse rather than install whatever is on disk.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let staging = dir.path().join("update-staging");
|
||||
tokio::fs::create_dir_all(&staging).await.unwrap();
|
||||
tokio::fs::write(staging.join(STAGED_COMPLETE_MARKER), b"1")
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::fs::write(staging.join("archipelago"), b"truncated-garbage")
|
||||
.await
|
||||
.unwrap();
|
||||
let state = UpdateState {
|
||||
available_update: Some(UpdateManifest {
|
||||
version: "999.0.0".to_string(),
|
||||
release_date: "2026-07-18".to_string(),
|
||||
changelog: vec![],
|
||||
components: vec![ComponentUpdate {
|
||||
name: "archipelago".to_string(),
|
||||
current_version: "1.0.0".to_string(),
|
||||
new_version: "999.0.0".to_string(),
|
||||
download_url: "http://example.invalid/archipelago".to_string(),
|
||||
sha256: "0".repeat(64),
|
||||
size_bytes: 49_949_048,
|
||||
blake3: None,
|
||||
}],
|
||||
}),
|
||||
update_in_progress: true,
|
||||
..UpdateState::default()
|
||||
};
|
||||
save_state(dir.path(), &state).await.unwrap();
|
||||
let err = apply_update(dir.path()).await.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("refusing to apply"),
|
||||
"got: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dismiss_update_clears_available() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -25,6 +25,11 @@ ExecStartPre=+/bin/bash -c 'mkdir -p /run/user/1000 /var/lib/containers && chown
|
||||
# once a VPN/bridge interface exists (netbird's wg tunnel sorted first and
|
||||
# poisoned every host_ip consumer). Falls back to hostname -I when routeless.
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /var/lib/archipelago && chown archipelago:archipelago /var/lib/archipelago && IP=$(ip -4 route show default 2>/dev/null | sed -n "s/.* src \([0-9.]*\).*/\1/p" | head -1); [ -n "$$IP" ] || IP=$(hostname -I 2>/dev/null | awk "{print $$1}"); echo "ARCHIPELAGO_HOST_IP=$$IP" > /var/lib/archipelago/host-ip.env && chown archipelago:archipelago /var/lib/archipelago/host-ip.env'
|
||||
# OTA crash-loop guard: if a just-applied binary can't start (SEGV loop), the
|
||||
# in-binary post-OTA probe never runs — this restores the update-backup binary
|
||||
# after 5 failed start attempts while the pending-verify marker exists.
|
||||
# "-" so a missing/failed guard can never block the service itself.
|
||||
ExecStartPre=+-/opt/archipelago/scripts/ota-crash-guard.sh
|
||||
ExecStart=/usr/local/bin/archipelago
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.103-alpha",
|
||||
"version": "1.7.104-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.103-alpha",
|
||||
"version": "1.7.104-alpha",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.103-alpha",
|
||||
"version": "1.7.104-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
let the app's own UI load instead of a loader stuck on top (B7). -->
|
||||
<div v-if="electrsSync && !electrsSync.stale" class="absolute inset-0 z-10 flex flex-col items-center justify-center">
|
||||
<div class="text-center px-8 w-full max-w-md">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-orange-300 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 7v10a2 2 0 002 2h12a2 2 0 002-2V7M4 7l8 5 8-5M4 7l8-4 8 4" />
|
||||
</svg>
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center overflow-hidden animate-pulse">
|
||||
<img :src="appIcon" :alt="appTitle" class="w-full h-full object-cover" @error="handleImageError" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white mb-2">{{ appTitle }} is syncing</h3>
|
||||
<p class="text-white/50 text-sm mb-5">
|
||||
@@ -119,6 +117,7 @@
|
||||
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import type { ElectrsSyncStatus } from '@/composables/useElectrsSync'
|
||||
import AppLoadingScreen from '@/components/AppLoadingScreen.vue'
|
||||
import { handleImageError } from '@/views/apps/appsConfig'
|
||||
|
||||
const props = defineProps<{
|
||||
appUrl: string
|
||||
|
||||
@@ -362,6 +362,19 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.7.104-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.104-alpha</span>
|
||||
<span class="text-xs text-white/40">July 19, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Software updates are now much safer to receive: the node will never install an update that isn't completely downloaded and verified byte-for-byte, closing a rare bug where an interrupted or cancelled download could leave a node unable to start.</p>
|
||||
<p>If a freshly installed update does fail to start, the node now notices and automatically restores the previous working version by itself — no manual rescue needed.</p>
|
||||
<p>The Electrum server now works with whichever Bitcoin you run: it finds Bitcoin Knots or Bitcoin Core automatically instead of assuming Knots.</p>
|
||||
<p>While the Electrum server is first building its index, its waiting screen now shows the ElectrumX app icon and live progress.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.103-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
|
||||
+18
-17
@@ -1,29 +1,30 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Connecting from the phone app no longer replays the intro cinematic on a loop: signing in after scanning the pairing QR could accidentally trigger \"Replay Intro\" instead of logging you in. The companion app now lands you straight on your dashboard, and the Android app waits for the login screen to be ready before it types your password.",
|
||||
"Pressing Enter in any password box now does what you expect — it signs you in or moves to the next field, and can no longer \"click\" a nearby button by mistake when using a controller or the companion app.",
|
||||
"The public demo no longer interrupts you with an \"Update Available\" popup that reset the site back to the intro — demo visitors simply get the newest version on their next visit."
|
||||
"Software updates are now much safer to receive: the node will never install an update that isn't completely downloaded and verified byte-for-byte, closing a rare bug where an interrupted or cancelled download could leave a node unable to start.",
|
||||
"If a freshly installed update does fail to start, the node now notices and automatically restores the previous working version by itself — no manual rescue needed.",
|
||||
"The Electrum server now works with whichever Bitcoin you run: it finds Bitcoin Knots or Bitcoin Core automatically instead of assuming Knots.",
|
||||
"While the Electrum server is first building its index, its waiting screen now shows the ElectrumX app icon and live progress."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.103-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.103-alpha/archipelago",
|
||||
"current_version": "1.7.104-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.104-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.103-alpha",
|
||||
"sha256": "b1a30287c1a694186e092d1746ed7cd647c4d2615edc59132cdc323ea5958ac4",
|
||||
"size_bytes": 49949048
|
||||
"new_version": "1.7.104-alpha",
|
||||
"sha256": "6f290654d3f6c784dd9518df673a14783b50f8832937306943bf50e62a33f1bb",
|
||||
"size_bytes": 49976648
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.103-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.103-alpha/archipelago-frontend-1.7.103-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.103-alpha.tar.gz",
|
||||
"new_version": "1.7.103-alpha",
|
||||
"sha256": "0544897a1d365fda8f7113eb80d76c5edcf50d539588af2886764546f02f52bf",
|
||||
"size_bytes": 174592877
|
||||
"current_version": "1.7.104-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.104-alpha/archipelago-frontend-1.7.104-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.104-alpha.tar.gz",
|
||||
"new_version": "1.7.104-alpha",
|
||||
"sha256": "8413af30dadbcade9ca40984beeac17add1b0477de7b1f7e4274f1482658d554",
|
||||
"size_bytes": 174592628
|
||||
}
|
||||
],
|
||||
"release_date": "2026-07-18",
|
||||
"signature": "39d3f161437a03d44a71cf87c0622d6c00cec2ec587e9e1533128f279c9b0e09c34c51465f2ee79983a3f76f080e909b836a0a74e9d16009cdcf528a7dc0830c",
|
||||
"release_date": "2026-07-19",
|
||||
"signature": "c1e2f9312d44c6109a77ff4500ce511cfa66b0c879a35271c7295d5673172053942de911db5e64306c4bef78b4bfc259cac0bf0fe1fc8dadfe77b14d4a5c0d08",
|
||||
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
|
||||
"version": "1.7.103-alpha"
|
||||
"version": "1.7.104-alpha"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"updated": "2026-07-10",
|
||||
"updated": "2026-07-18",
|
||||
"apps": {
|
||||
"adguardhome": {
|
||||
"version": "v0.107.55",
|
||||
@@ -333,6 +333,78 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"barkd": {
|
||||
"version": "0.3.0",
|
||||
"manifest": {
|
||||
"app": {
|
||||
"id": "barkd",
|
||||
"name": "Ark Wallet",
|
||||
"version": "0.3.0",
|
||||
"description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.",
|
||||
"container": {
|
||||
"image": "146.59.87.168:3000/lfg2025/barkd:0.3.0",
|
||||
"pull_policy": "if-not-present",
|
||||
"network": "archy-net",
|
||||
"generated_secrets": [
|
||||
{
|
||||
"name": "barkd-secret",
|
||||
"kind": "hex32"
|
||||
}
|
||||
],
|
||||
"secret_env": [
|
||||
{
|
||||
"key": "BARKD_SECRET",
|
||||
"secret_file": "barkd-secret"
|
||||
}
|
||||
],
|
||||
"data_uid": "1000:1000"
|
||||
},
|
||||
"dependencies": [
|
||||
{
|
||||
"storage": "1Gi"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"cpu_limit": 1,
|
||||
"memory_limit": "512Mi",
|
||||
"disk_limit": "1Gi"
|
||||
},
|
||||
"security": {
|
||||
"readonly_root": true,
|
||||
"network_policy": "bridge"
|
||||
},
|
||||
"ports": [
|
||||
{
|
||||
"host": 3535,
|
||||
"container": 3535,
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/lib/archipelago/barkd",
|
||||
"target": "/data",
|
||||
"options": [
|
||||
"rw"
|
||||
]
|
||||
}
|
||||
],
|
||||
"environment": [
|
||||
"BARKD_DATADIR=/data",
|
||||
"BARKD_BIND_HOST=0.0.0.0",
|
||||
"BARKD_BIND_PORT=3535"
|
||||
],
|
||||
"health_check": {
|
||||
"type": "tcp",
|
||||
"endpoint": "localhost:3535",
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bitcoin-core": {
|
||||
"version": "latest",
|
||||
"manifest": {
|
||||
@@ -1085,7 +1157,7 @@
|
||||
"-lc"
|
||||
],
|
||||
"custom_args": [
|
||||
"export DAEMON_URL=\"http://archipelago:$(printenv BITCOIN_RPC_PASS)@bitcoin-knots:8332/\"; exec electrumx_server"
|
||||
"for h in bitcoin-knots bitcoin-core; do if getent hosts \"$h\" >/dev/null 2>&1; then BTC_HOST=\"$h\"; break; fi; done; export DAEMON_URL=\"http://archipelago:$(printenv BITCOIN_RPC_PASS)@${BTC_HOST:-bitcoin-knots}:8332/\"; exec electrumx_server"
|
||||
],
|
||||
"secret_env": [
|
||||
{
|
||||
|
||||
+18
-17
@@ -1,29 +1,30 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Connecting from the phone app no longer replays the intro cinematic on a loop: signing in after scanning the pairing QR could accidentally trigger \"Replay Intro\" instead of logging you in. The companion app now lands you straight on your dashboard, and the Android app waits for the login screen to be ready before it types your password.",
|
||||
"Pressing Enter in any password box now does what you expect — it signs you in or moves to the next field, and can no longer \"click\" a nearby button by mistake when using a controller or the companion app.",
|
||||
"The public demo no longer interrupts you with an \"Update Available\" popup that reset the site back to the intro — demo visitors simply get the newest version on their next visit."
|
||||
"Software updates are now much safer to receive: the node will never install an update that isn't completely downloaded and verified byte-for-byte, closing a rare bug where an interrupted or cancelled download could leave a node unable to start.",
|
||||
"If a freshly installed update does fail to start, the node now notices and automatically restores the previous working version by itself — no manual rescue needed.",
|
||||
"The Electrum server now works with whichever Bitcoin you run: it finds Bitcoin Knots or Bitcoin Core automatically instead of assuming Knots.",
|
||||
"While the Electrum server is first building its index, its waiting screen now shows the ElectrumX app icon and live progress."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.103-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.103-alpha/archipelago",
|
||||
"current_version": "1.7.104-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.104-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.103-alpha",
|
||||
"sha256": "b1a30287c1a694186e092d1746ed7cd647c4d2615edc59132cdc323ea5958ac4",
|
||||
"size_bytes": 49949048
|
||||
"new_version": "1.7.104-alpha",
|
||||
"sha256": "6f290654d3f6c784dd9518df673a14783b50f8832937306943bf50e62a33f1bb",
|
||||
"size_bytes": 49976648
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.103-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.103-alpha/archipelago-frontend-1.7.103-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.103-alpha.tar.gz",
|
||||
"new_version": "1.7.103-alpha",
|
||||
"sha256": "0544897a1d365fda8f7113eb80d76c5edcf50d539588af2886764546f02f52bf",
|
||||
"size_bytes": 174592877
|
||||
"current_version": "1.7.104-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.104-alpha/archipelago-frontend-1.7.104-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.104-alpha.tar.gz",
|
||||
"new_version": "1.7.104-alpha",
|
||||
"sha256": "8413af30dadbcade9ca40984beeac17add1b0477de7b1f7e4274f1482658d554",
|
||||
"size_bytes": 174592628
|
||||
}
|
||||
],
|
||||
"release_date": "2026-07-18",
|
||||
"signature": "39d3f161437a03d44a71cf87c0622d6c00cec2ec587e9e1533128f279c9b0e09c34c51465f2ee79983a3f76f080e909b836a0a74e9d16009cdcf528a7dc0830c",
|
||||
"release_date": "2026-07-19",
|
||||
"signature": "c1e2f9312d44c6109a77ff4500ce511cfa66b0c879a35271c7295d5673172053942de911db5e64306c4bef78b4bfc259cac0bf0fe1fc8dadfe77b14d4a5c0d08",
|
||||
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
|
||||
"version": "1.7.103-alpha"
|
||||
"version": "1.7.104-alpha"
|
||||
}
|
||||
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# OTA crash-loop guard — runs as root from ExecStartPre=+- on archipelago.service.
|
||||
#
|
||||
# Covers the failure mode verify_pending_update() cannot: a freshly-applied
|
||||
# binary that can't even start (SEGV/ENOEXEC — e.g. the truncated 17MB binary
|
||||
# .198 installed on the v1.7.103 OTA, which crash-looped 236 times with a
|
||||
# perfectly good backup sitting in update-backup/). The in-binary probe never
|
||||
# runs because the binary never runs, so this guard counts start attempts from
|
||||
# outside and restores the backup binary once the new one has clearly failed.
|
||||
#
|
||||
# Scope is deliberately narrow: it acts ONLY while the post-OTA pending-verify
|
||||
# marker exists (written by apply_update just before the restart, deleted by
|
||||
# the new binary once it boots and passes its probes). A crash loop with no
|
||||
# marker is not an OTA gone wrong, and this script stays out of it.
|
||||
#
|
||||
# Always exits 0 — a guard must never be the reason the service can't start.
|
||||
|
||||
set -u
|
||||
|
||||
DATA_DIR=/var/lib/archipelago
|
||||
MARKER="$DATA_DIR/update-pending-verify.json"
|
||||
COUNT_FILE="$DATA_DIR/ota-crash-guard.count"
|
||||
BACKUP="$DATA_DIR/update-backup/archipelago"
|
||||
BINARY=/usr/local/bin/archipelago
|
||||
MAX_ATTEMPTS=5
|
||||
|
||||
log() {
|
||||
echo "$*" | systemd-cat -t ota-crash-guard -p warning 2>/dev/null || true
|
||||
}
|
||||
|
||||
# No pending OTA verification -> nothing to guard; clear any stale counter.
|
||||
if [ ! -f "$MARKER" ]; then
|
||||
rm -f "$COUNT_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count this start attempt. The counter only accumulates while the marker
|
||||
# exists; a healthy new binary deletes the marker on its first successful
|
||||
# boot, and the next start clears the counter above.
|
||||
count=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
|
||||
case "$count" in ''|*[!0-9]*) count=0 ;; esac
|
||||
count=$((count + 1))
|
||||
echo "$count" > "$COUNT_FILE" 2>/dev/null || true
|
||||
|
||||
if [ "$count" -lt "$MAX_ATTEMPTS" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$BACKUP" ]; then
|
||||
log "OTA crash guard: $count failed start attempts but no backup binary at $BACKUP — cannot roll back"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Already restored (or the OTA never replaced the binary)? Don't loop.
|
||||
if cmp -s "$BACKUP" "$BINARY"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Restore via copy-to-temp + atomic rename; never truncate the live path.
|
||||
tmp="$BINARY.rollback.$$"
|
||||
if cp "$BACKUP" "$tmp" && chown root:root "$tmp" && chmod 755 "$tmp" && mv "$tmp" "$BINARY"; then
|
||||
# Leave a tombstone for the UI/logs instead of the marker so the restored
|
||||
# binary doesn't run the post-OTA probe against the rolled-back version.
|
||||
mv "$MARKER" "$DATA_DIR/update-rolled-back.json" 2>/dev/null || rm -f "$MARKER"
|
||||
rm -f "$COUNT_FILE"
|
||||
log "OTA crash guard: restored previous binary after $count failed start attempts of the updated one"
|
||||
else
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
log "OTA crash guard: failed to restore backup binary (cp/mv error)"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user