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.4: close the mid-update race that left a stale build serving (serve hands off when it is not the installed build), retire by port ownership rather than name, strip the (deleted) suffix before any path use; reproduced via /proc/pid/exe and verified fixed
4 files changed
+102−3
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-chip-fetcher"-version = "1.4.3"+version = "1.4.4" dependencies = [ "anyhow", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-chip-fetcher"-version = "1.4.3"+version = "1.4.4" 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−1@@ -108,6 +108,72 @@ pub fn running_version(port: u16) -> Option<String> { } } +/// The PID listening on `port`, found through the kernel rather than through+/// process names: parse /proc/net/tcp{,6} for the LISTEN socket on that port,+/// then find the process holding that socket inode. Names and argv can lie or+/// drift (a rename, an alias, a wrapper); the socket cannot.+pub fn pid_owning_port(port: u16) -> Option<u32> {+ let mut inodes: Vec<String> = Vec::new();+ for f in ["/proc/net/tcp", "/proc/net/tcp6"] {+ let Ok(text) = std::fs::read_to_string(f) else { continue };+ for line in text.lines().skip(1) {+ let cols: Vec<&str> = line.split_whitespace().collect();+ if cols.len() < 10 {+ continue;+ }+ // local_address is "HEXADDR:HEXPORT"; state 0A is LISTEN.+ let Some((_, p)) = cols[1].split_once(':') else { continue };+ if u16::from_str_radix(p, 16).ok() != Some(port) || cols[3] != "0A" {+ continue;+ }+ inodes.push(cols[9].to_string());+ }+ }+ if inodes.is_empty() {+ return None;+ }+ let rd = std::fs::read_dir("/proc").ok()?;+ for e in rd.flatten() {+ let name = e.file_name().to_string_lossy().to_string();+ let Ok(pid) = name.parse::<u32>() else { continue };+ let Ok(fds) = std::fs::read_dir(format!("/proc/{pid}/fd")) else { continue };+ for fd in fds.flatten() {+ if let Ok(target) = std::fs::read_link(fd.path()) {+ let t = target.to_string_lossy();+ if let Some(ino) = t.strip_prefix("socket:[").and_then(|x| x.strip_suffix(']')) {+ if inodes.iter().any(|i| i == ino) {+ return Some(pid);+ }+ }+ }+ }+ }+ None+}++/// Is this PID one of OUR builds? Checks the executable behind the process,+/// stripping Linux's "(deleted)" suffix, which appears the instant a package+/// update unlinks the old binary and writes a new one at the same path. Any+/// path comparison that skips this stops matching exactly when it matters.+fn pid_is_ours(pid: u32) -> bool {+ let exe = std::fs::read_link(format!("/proc/{pid}/exe"))+ .map(|p| p.to_string_lossy().to_string())+ .unwrap_or_default();+ let exe = exe.strip_suffix(" (deleted)").unwrap_or(&exe).to_string();+ let base = exe.rsplit('/').next().unwrap_or("").to_string();+ if base == "adom-chip-fetcher" || base == "chip-fetcher" {+ return true;+ }+ // Fall back to argv, which is frozen at exec and never gains "(deleted)".+ let cmd = std::fs::read(format!("/proc/{pid}/cmdline")).unwrap_or_default();+ let argv: Vec<&[u8]> = cmd.split(|b| *b == 0).collect();+ if argv.len() < 2 || argv[1] != b"serve" {+ return false;+ }+ let bin = argv[0].rsplit(|b| *b == b'/').next().unwrap_or(argv[0]);+ bin == b"adom-chip-fetcher" || bin == b"chip-fetcher"+}+ /// Retire a stale instance of a different version. Kills only processes /// whose argv is EXACTLY `[<...>/adom-chip-fetcher, serve, ...]` (or the /// `chip-fetcher` alias), never a shell whose command text merely mentions@@ -115,7 +181,15 @@ pub fn running_version(port: u16) -> Option<String> { pub fn retire_stale(port: u16, their: &str) -> bool { eprintln!("{APP} {their} is serving on {port} but this is {VERSION}; retiring the stale instance"); let me = std::process::id();+ let parent = std::os::unix::process::parent_id(); let mut victims: Vec<u32> = Vec::new();+ // Preferred: whoever actually owns the port, confirmed to be one of ours.+ // Never ourselves, never our parent.+ if let Some(owner) = pid_owning_port(port) {+ if owner != me && owner != parent && pid_is_ours(owner) {+ victims.push(owner);+ }+ } if let Ok(rd) = std::fs::read_dir("/proc") { for e in rd.flatten() { let name = e.file_name().to_string_lossy().to_string();@@ -123,7 +197,7 @@ pub fn retire_stale(port: u16, their: &str) -> bool { Ok(p) => p, Err(_) => continue, };- if pid == me {+ if pid == me || pid == parent || victims.contains(&pid) { continue; } let cmd = match std::fs::read(format!("/proc/{pid}/cmdline")) {
src/serve.rs+25@@ -31,6 +31,31 @@ fn now_epoch() -> u64 { pub fn run(port: u16, print_url: bool, detached_child: bool) -> Result<()> { use crate::dock; if !detached_child {+ // THE RACE that left a stale build serving on 2026-08-24: the dock's+ // Hard Refresh updates the package while the launch steps run, so+ // `serve` can be THIS process running the OLD binary. It then sees a+ // matching version on the port, reuses it, and the just-installed+ // build never takes over. So before anything else: if the binary on+ // disk is not us, hand the whole command to it, once (guarded so a+ // version-reporting oddity can never loop).+ if std::env::var("CF_SERVE_HANDED_OFF").is_err() {+ if let Some(disk) = dock::installed_version() {+ if disk != dock::VERSION {+ if let Some(exe) = dock::exe_path() {+ eprintln!("{} v{} is installed but this process is v{}; re-running serve from the installed build", dock::APP, disk, dock::VERSION);+ let mut cmd = std::process::Command::new(exe);+ cmd.arg("serve").arg("--port").arg(port.to_string());+ if print_url {+ cmd.arg("--print-url");+ }+ cmd.env("CF_SERVE_HANDED_OFF", "1");+ if let Ok(st) = cmd.status() {+ std::process::exit(st.code().unwrap_or(0));+ }+ }+ }+ }+ } // Idempotent + version-aware: an instance of OURS makes relaunch // instant; a different version is the thing to replace. if let Some(their) = dock::running_version(port) {