app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
v0.9.165 HOTFIX: boot maintenance ran BEFORE the port bind and SetWindowLongPtr blocks forever against a hung KiCad, so the bridge never came up and every verb timed out. Window work now runs on a daemon thread after the server is listening, and style changes probe the window first
6 files changed
+57−47
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.164+0.9.165
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.164",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.164/adom-bridge-kicad-v0.9.164.zip",- "sha256": "74b29b5d9c35f5095634890c710a6ca2b476ad64292d2513b21bd1488ba1b236",- "size": 443664,+ "version": "0.9.165",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.165/adom-bridge-kicad-v0.9.165.zip",+ "sha256": "2cb2c8ea5e2f51b818b91a90083c14ed4fd51f26aa181137719f8a70dab06da8",+ "size": 443871, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-18T11:21:42Z",+ "released_at": "2026-08-18T12:25:13Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.165.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.164",+ "version": "0.9.165", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",
handlers/win_focus.py+20@@ -174,6 +174,24 @@ except Exception: pass +def _window_responsive(hwnd, ms: int = 150) -> bool:+ """Is this window's thread pumping messages? SetWindowLongPtr SENDS+ WM_STYLECHANGING/WM_STYLECHANGED cross-process, so touching the style of a+ HUNG window blocks the caller forever (0.9.165: that deadlock, on the boot+ path, took the whole bridge down). Probe first, with a timeout."""+ if not IS_WINDOWS:+ return False+ try:+ res = ctypes.c_ulong(0)+ SMTO_ABORTIFHUNG = 0x0002+ WM_NULL = 0x0000+ ok = _user32.SendMessageTimeoutW(hwnd, WM_NULL, 0, 0, SMTO_ABORTIFHUNG,+ ms, ctypes.byref(res))+ return bool(ok)+ except Exception:+ return False++ def _get_exstyle(hwnd): fn = getattr(_user32, "GetWindowLongPtrW", None) or _user32.GetWindowLongW return fn(hwnd, GWL_EXSTYLE)@@ -675,6 +693,8 @@ def heal_window_usability() -> int: if busy and h in _parked: continue # mid-verb: this window is legitimately suppressed try:+ if not _window_responsive(h):+ continue # hung window: touching its style would block us too cur = _get_exstyle(h) want = (cur & ~WS_EX_NOACTIVATE) | WS_EX_APPWINDOW if want != cur:
server.py+30−40@@ -1095,47 +1095,37 @@ def main(): except Exception: pass - # 0.9.155: revive the ownership ledger FIRST (which windows/pids are OURS —- # never the user's own KiCad), then re-stamp badges on the survivors only.- try:- win_focus.load_owned_ledger()- except Exception:- pass- # 0.9.156: heal any window stranded at parking coordinates (a crash or a- # close-while-parked poisons KiCad's saved geometry, and the next NORMAL- # launch opens invisibly off-screen — John's catch).- try:- win_focus.heal_offscreen_windows()- except Exception:- pass- # 0.9.164: a window suppressed by an EARLIER bridge process is unknown to- # this one, so nothing would ever clear it - heal by inspection at boot.- try:- win_focus.heal_window_usability()- except Exception:- pass- # Bridge shutdown must never strand parked windows either.- try:- import atexit- atexit.register(win_focus.unpark_all)- except Exception:- pass- # 0.9.157: scrub bridge projects from KiCad's restore-on-launch list (only- # sticks while no KiCad runs; the sweep retries until that window opens).- # 0.9.158: START the sweep at boot — it used to arm only on the first GUI- # verb, so on a quiet boot the 60s sanitize retry (and parked-window- # defense) never ran at all.- try:- win_focus.sanitize_kicad_session()- except Exception:- pass- try:- win_focus._start_park_sweep()- except Exception:- pass+ # 0.9.165 - THE BRIDGE MUST ALWAYS COME UP. Every one of these touches other+ # processes' windows, and SetWindowLongPtr sends WM_STYLECHANGING/CHANGED+ # cross-process: against a HUNG KiCad that SendMessage never returns. Run on+ # the boot path (as 0.9.155-0.9.164 did) it deadlocked before the port was+ # even bound, so the whole bridge went silent and every verb timed out -+ # John's laptop, live. Desktop-window work now happens on a daemon thread,+ # after the server is already listening.+ def _boot_maintenance():+ import time as _t+ _t.sleep(2) # let the port bind and the first verbs answer first+ for fn in ("load_owned_ledger", "heal_offscreen_windows",+ "heal_window_usability", "sanitize_kicad_session",+ "_start_park_sweep"):+ try:+ getattr(win_focus, fn)()+ except Exception as e:+ print(f"[boot] {fn} failed: {e}", file=sys.stderr, flush=True)+ try:+ from handlers.win_taskbar import badge_existing_on_boot+ badge_existing_on_boot()+ except Exception:+ pass+ try:+ import atexit+ atexit.register(win_focus.unpark_all)+ except Exception:+ pass+ try:- from handlers.win_taskbar import badge_existing_on_boot- badge_existing_on_boot()+ import threading as _th+ _th.Thread(target=_boot_maintenance, daemon=True, name="kicad-boot-maint").start() except Exception: pass