app
Chip Fetcher (cf)
Public Made by Adomby adom
Crawls the web on your behalf to gather everything about the chips you care about: datasheets, symbols, footprints, 3D models, stock and pricing, straight from the manufacturer first, into your EDA tool. Say "cf" to your AI.
← 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
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