← 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

John Lauer ·d0c74830ca ·1mo ago ·parent 09a22b9
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) {