← Commit history

1.4.2: self-handoff when a package update replaces the binary (a stale build could outlive pkg update because Hard Refresh reopens the tab without re-running serve; /proc/self/exe resolves to '<path> (deleted)' after an unlink-write, which made the naive check blind), /health reports installed_version + stale, Dashboard LED shows the version pair, and the favicon is a line-drawing chip with a download arrow per adom-ui-design 1.10.0 icons

John Lauer ·8ca77f3acf ·1mo ago ·parent cf61de8
4 files changed +83−4
Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-chip-fetcher"-version = "1.4.1"+version = "1.4.2" dependencies = [  "anyhow",  "clap",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-chip-fetcher"-version = "1.4.1"+version = "1.4.2" edition = "2021" description = "Crawls the web on your behalf to gather everything about the chips you care about (datasheet, symbol, footprint, 3D, stock) into one library, by driving Chrome via pup." 
src/dock.rs+75−2
@@ -26,6 +26,7 @@ use serde_json::{json, Value}; use std::collections::VecDeque; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream};+use std::path::PathBuf; use std::sync::Mutex; use std::time::{Duration, Instant}; @@ -450,7 +451,15 @@ pub fn build_facets_forced(force: bool) -> Facets {     // 1. the dashboard itself: answering means ok.     leds.insert(         "dashboard".into(),-        led("Dashboard", "ok", format!("v{VERSION} serving"), "The dashboard answers /api/status; the dock polls it every 15s."),+        match installed_version() {+            Some(d) if d != VERSION => led(+                "Dashboard",+                "warn",+                format!("v{VERSION} serving, v{d} installed"),+                "A newer build is on disk. This server hands the port over within a minute by itself; `adom-chip-fetcher serve --print-url` does it now.",+            ),+            _ => led("Dashboard", "ok", format!("v{VERSION} serving"), "The dashboard answers /api/status; the dock polls it every 15s."),+        },     );      // 2. the AI skills on this container: the main skill (+playbooks), the@@ -579,6 +588,70 @@ pub fn status_json(force: bool) -> String {     body } +/// The version of the binary currently AT our own path on disk. After+/// `adom-wiki pkg update` replaces the file, this differs from the running+/// VERSION, and that gap is how a stale build kept serving after a dock Hard+/// Refresh (the dock reopens the tab against the running server and never+/// re-runs the launch steps, so nothing asks the old process to stand down).+/// Our own executable path, with Linux's "(deleted)" suffix stripped. A+/// package update unlinks the old file and writes a new one at the same path,+/// so a running process resolves /proc/self/exe to "<path> (deleted)"; reading+/// the SUFFIXED path fails and the upgrade watch goes blind. That is the exact+/// trap that let a stale build keep serving.+pub fn exe_path() -> Option<PathBuf> {+    let p = std::fs::read_link("/proc/self/exe").ok().or_else(|| std::env::current_exe().ok())?;+    let s = p.to_string_lossy().to_string();+    Some(PathBuf::from(s.strip_suffix(" (deleted)").unwrap_or(&s)))+}++pub fn installed_version() -> Option<String> {+    let exe = exe_path()?;+    if !exe.is_file() {+        return None;+    }+    let out = std::process::Command::new("timeout").arg("5").arg(&exe).arg("--version").output().ok()?;+    let text = String::from_utf8_lossy(&out.stdout);+    let v = text.split_whitespace().last()?.trim().to_string();+    // Only trust a clean semver; never act on a parse accident.+    if v.split('.').count() == 3 && v.chars().all(|c| c.is_ascii_digit() || c == '.') {+        Some(v)+    } else {+        None+    }+}++/// Watch our own binary and hand off when a package update replaces it.+/// Verified handoff: the replacement must answer `--version` cleanly before we+/// give up the port, and the successor is spawned detached with a short gap so+/// it can bind. Without this, `pkg update` + a tab reopen leaves the OLD build+/// serving, which is exactly how a parked-crash build stayed live.+pub fn watch_for_upgrade(port: u16) {+    std::thread::spawn(move || loop {+        std::thread::sleep(Duration::from_secs(if std::env::var("CF_UPGRADE_WATCH_FAST").is_ok() { 3 } else { 60 }));+        let Some(disk) = installed_version() else { continue };+        if disk == VERSION {+            continue;+        }+        let Some(exe) = exe_path() else { continue };+        console_push("info", &format!("binary on disk is v{disk} but this process is v{VERSION}; handing the port over"), "serve");+        eprintln!("{APP}: v{disk} is installed, this process is v{VERSION}; handing over port {port}");+        let cmd = format!("sleep 1; exec {:?} serve --port {port} --detached-child", exe);+        let spawned = std::process::Command::new("sh")+            .arg("-c")+            .arg(&cmd)+            .stdout(std::process::Stdio::null())+            .stderr(std::process::Stdio::null())+            .spawn();+        if spawned.is_ok() {+            std::thread::sleep(Duration::from_millis(200));+            std::process::exit(0);+        }+        eprintln!("{APP}: could not spawn the successor; staying up on v{VERSION}");+    });+}+ pub fn health_json() -> String {-    json!({"ok": true, "app": APP, "version": VERSION}).to_string()+    let disk = installed_version();+    let stale = disk.as_deref().map(|d| d != VERSION).unwrap_or(false);+    json!({"ok": true, "app": APP, "version": VERSION, "installed_version": disk, "stale": stale}).to_string() }
src/serve.rs+6
@@ -77,6 +77,7 @@ pub fn run(port: u16, print_url: bool, detached_child: bool) -> Result<()> {         println!("(open in pup via `adom-chip-fetcher app`, or visit in any browser)");     }     dock::claim_dock_webview(port);+    dock::watch_for_upgrade(port);     LIBRARY_VERSION.lock().map(|mut v| *v = library_signature()).ok();     // First run: an empty library teaches nothing. Seed the sample bundles     // from the Adom Wiki in the background (no scraping, no desktop) so the@@ -1691,6 +1692,11 @@ fn say_send(phrase: &str) -> serde_json::Value {     }     let staged = control_post(         "/ai-threads/new",+        // The tag is `dock:` + the WIKI PAGE SLUG (adom-ui-design 1.9.3 ruling,+        // 2026-08-24), never the dock card id: the page slug identifies the+        // package, its issues and its dockbar manifest, and is unique; card ids+        // are a dock-internal ordering key. Ours is adom-chip-fetcher, not+        // chip-fetcher. Do not "correct" this.         &serde_json::json!({"name": "Chip Fetcher - say it", "tags": ["dock:adom-chip-fetcher"]}),     );     let sid = staged