← 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

John Lauer ·abb9f663af ·1mo ago ·parent cdd33c4
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