app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
2.11.2: serve never kills itself, never mistakes a free port for a stale instance
6 files changed
+60−35
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.11.1"+version = "2.11.2" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.11.1"+version = "2.11.2" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time."
VERSION+1−1@@ -1 +1 @@-2.11.1+2.11.2
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.11.1",+ "version": "2.11.2", "type": "app", "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.", "dependencies": {},
page.json+9−12@@ -1,8 +1,8 @@ { "slug": "adom-lbr",- "version": "2.11.1",+ "version": "2.11.2", "type": "app",- "description": "adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.",+ "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.", "dependencies": {}, "tags": [ "eda",@@ -52,24 +52,21 @@ ], "files": [ "bin/**",- "assets/**", "SKILL.md",- "skills/**", "README.md", "install.sh", "uninstall.sh",- "screenshots/**",- "dockbar.json"+ "screenshots/**" ],- "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools — KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion — through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",+ "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools \u2014 KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion \u2014 through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.", "sample_prompts": [ { "prompt": "Convert this KiCad symbol and footprint to an Altium SchLib and PcbLib",- "label": "KiCad → Altium"+ "label": "KiCad \u2192 Altium" }, { "prompt": "Import this Altium footprint into the adom-lbr JSON",- "label": "Altium → JSON"+ "label": "Altium \u2192 JSON" }, { "prompt": "Bundle this part into a single Altium IntLib",@@ -77,15 +74,15 @@ }, { "prompt": "Convert this Altium SchLib and PcbLib to a KiCad symbol and footprint",- "label": "Altium → KiCad"+ "label": "Altium \u2192 KiCad" }, { "prompt": "Export this adom-lbr JSON to a KiCad .kicad_sym and .kicad_mod",- "label": "JSON → KiCad"+ "label": "JSON \u2192 KiCad" }, { "prompt": "Open the adom-lbr Manager to browse wiki components and view the layer stackup", "label": "Browse parts" } ]-}+}
src/dock.rs+47−19@@ -523,23 +523,40 @@ pub fn running_version(port: u16) -> Option<String> { None } -/// Our own serve processes, matched by EXACT argv shape (binary named adom-lbr,-/// then `dashboard serve` or `manage`), never by substring: a substring match-/// also hits the shell whose command text mentions this program.-fn serve_pids() -> Vec<i32> {+/// Our own SERVER processes, matched by EXACT argv shape: a binary named+/// adom-lbr running `dashboard serve ... --foreground` (the backgrounded+/// child) or `manage` (the pre-2.11 Manager). A launcher (`dashboard serve+/// --print-url` without --foreground) is never a victim, and neither are this+/// process or its parent: on ConfRoomROG (2026-08-23) the second launch+/// SIGTERMed itself and died with exit 143 and no output.+fn serve_pids(port: u16) -> Vec<i32> { let me = std::process::id() as i32;+ let parent = std::fs::read_to_string("/proc/self/stat").ok()+ .and_then(|st| st.rsplit(')').next().map(|s| s.to_string()))+ .and_then(|tail| tail.split_whitespace().nth(1).and_then(|p| p.parse::<i32>().ok()))+ .unwrap_or(-1); let mut out = vec![]; let Ok(rd) = std::fs::read_dir("/proc") else { return out }; for ent in rd.flatten() { let Ok(pid) = ent.file_name().to_string_lossy().parse::<i32>() else { continue };- if pid == me { continue; }+ if pid == me || pid == parent { continue; } let Ok(raw) = std::fs::read(ent.path().join("cmdline")) else { continue }; let argv: Vec<&[u8]> = raw.split(|b| *b == 0).filter(|s| !s.is_empty()).collect(); if argv.len() < 2 { continue; } let base = argv[0].rsplit(|b| *b == b'/').next().unwrap_or(argv[0]); if base != b"adom-lbr" { continue; }- let is_serve = argv[1] == b"manage" || (argv.len() >= 3 && argv[1] == b"dashboard" && argv[2] == b"serve");- if is_serve { out.push(pid); }+ let is_manage = argv[1] == b"manage";+ let is_server = argv.len() >= 3 && argv[1] == b"dashboard" && argv[2] == b"serve"+ && argv.iter().any(|a| *a == b"--foreground");+ if !(is_manage || is_server) { continue; }+ // Scoped to THIS port: a foreign owner on one port must never get our+ // healthy servers on other ports killed (caught in testing, 2026-08-23).+ let their_port: u16 = argv.iter().position(|a| *a == b"--port")+ .and_then(|i| argv.get(i + 1))+ .and_then(|p| std::str::from_utf8(p).ok())+ .and_then(|p| p.parse().ok())+ .unwrap_or(8785);+ if their_port == port { out.push(pid); } } out }@@ -552,8 +569,15 @@ fn kill(pid: i32, sig: &str) { /// reason to skip serving: version-blind reuse pinned users to a stale server /// no reinstall could fix. Returns true when the port is free afterwards. fn retire_stale(port: u16, theirs: &str) -> bool {- eprintln!("adom-lbr dashboard {theirs} is serving on {port} but this is {VERSION}; retiring the stale instance");- let victims = serve_pids();+ let victims = serve_pids(port);+ if victims.is_empty() {+ // Something answers on the port but no server process of ours exists:+ // it is not ours to kill, and waiting would not change that.+ eprintln!("adom-lbr dashboard: port {port} answers ({theirs}) but no adom-lbr server process exists; something else owns it");+ return false;+ }+ eprintln!("adom-lbr dashboard {theirs} is serving on {port} but this is {VERSION}; retiring the stale instance (pid {})",+ victims.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", ")); for pid in &victims { kill(*pid, "-TERM"); } for _ in 0..40 { if !port_in_use(port) { return true; }@@ -564,15 +588,14 @@ fn retire_stale(port: u16, theirs: &str) -> bool { !port_in_use(port) } -/// Does anything accept a TCP connection on the port? curl exit 7 is-/// "could not connect"; any other outcome means someone is listening.+/// Does anything ACCEPT a TCP connection on the port? Only a completed+/// connect counts. Refused means free, and so does a timeout: under WSL2+/// mirrored networking a connect to a free loopback port can hang instead of+/// refusing (ConfRoomROG, 2026-08-23), which the old curl-exit-code check read+/// as "in use" and then spent 40 s failing to retire nothing. fn port_in_use(port: u16) -> bool {- Command::new("curl")- .args(["-s", "-o", "/dev/null", "--max-time", "1", &format!("http://127.0.0.1:{port}/")])- .stdout(Stdio::null()).stderr(Stdio::null())- .status()- .map(|s| s.code() != Some(7))- .unwrap_or(false)+ let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));+ std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(500)).is_ok() } /// Take ownership of the dock's pre-opened webview tab. The dock sets@@ -616,8 +639,13 @@ fn ask_server_to_claim(port: u16) { pub fn serve_entry(port: u16, print_url: bool, foreground: bool) -> anyhow::Result<bool> { let mut theirs = running_version(port); // A pre-2.11 Manager has no /api/status, so it cannot identify itself; but- // it is still one of our serve processes by argv, and it must make way.- if theirs.is_none() && port_in_use(port) && !serve_pids().is_empty() {+ // it is still one of our server processes by argv, and it must make way.+ // Only a port that actually ACCEPTS a connection can hold one; refused or+ // timed out means free, and we simply bind.+ if theirs.is_none() && port_in_use(port) {+ if serve_pids(port).is_empty() {+ anyhow::bail!("port {port} is taken by something that is not adom-lbr; pick another with --port");+ } if !retire_stale(port, "pre-2.11") { anyhow::bail!("an older adom-lbr Manager owns port {port} and would not exit; kill it and relaunch"); }