← Commit history

v0.9.160: THE taskbar-icon root cause - WS_EX_NOACTIVATE removes a window from the taskbar by design, so backgrounded windows vanished from it; now paired with WS_EX_APPWINDOW. Plus guard/park watchdogs (a leaked guard stranded a window parked for 5h), non-blocking icon probes, and a reachability check on unpark

John Lauer ·0fb77d2a7c ·1mo ago ·parent e5e69f8
5 files changed +107−15
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.159+0.9.160
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.159",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.159/adom-bridge-kicad-v0.9.159.zip",-  "sha256": "a5a88aa11d4f80ec9ddbe0f3e44ed6d84e19302b8948f18e22c3dfb226407b26",-  "size": 439770,+  "version": "0.9.160",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.160/adom-bridge-kicad-v0.9.160.zip",+  "sha256": "0ac61822de115957eb211d363e120502d4e94be6539133a190e22629ac68da56",+  "size": 441328,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-18T10:24:38Z",+  "released_at": "2026-08-18T10:33:41Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.160.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.159",+  "version": "0.9.160",   "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+100−8
@@ -152,8 +152,18 @@ def is_kicad_window(hwnd) -> bool: # guardian exits / on bring_to_user / on close — so after bridge work ends the # user can click into KiCad normally again. WS_EX_NOACTIVATE = 0x08000000+WS_EX_APPWINDOW = 0x00040000 GWL_EXSTYLE = -20 +# 0.9.160 (John: "why do i see no windows taskbar icons after you claim you're+# launched"): WS_EX_NOACTIVATE is DOCUMENTED to remove a window from the taskbar+# ("A window that has this style does not appear on the taskbar by default. To+# force the window to appear on the taskbar, use WS_EX_APPWINDOW"). Every+# suppression therefore pairs NOACTIVATE with APPWINDOW, so a backgrounded KiCad+# window keeps a normal, clickable taskbar button - background must mean BEHIND,+# never INVISIBLE. APPWINDOW is left in place when NOACTIVATE clears: it only+# guarantees the button, and re-toggling styles makes the shell rebuild it.+ _noactivated: set = set() _guard_count = 0 _guard_lock = None@@ -175,11 +185,13 @@ def _set_exstyle(hwnd, style):   def suppress_activation(hwnd) -> None:-    """Make a KiCad window unable to grab the foreground (WS_EX_NOACTIVATE)."""+    """Make a KiCad window unable to grab the foreground (WS_EX_NOACTIVATE),+    while KEEPING its taskbar button (WS_EX_APPWINDOW - see the note above)."""     try:         cur = _get_exstyle(hwnd)-        if not (cur & WS_EX_NOACTIVATE):-            _set_exstyle(hwnd, cur | WS_EX_NOACTIVATE)+        want = cur | WS_EX_NOACTIVATE | WS_EX_APPWINDOW+        if want != cur:+            _set_exstyle(hwnd, want)         _noactivated.add(hwnd)     except Exception:         pass@@ -382,6 +394,10 @@ _park_sweep_started = False # no spawned pid tree -> _is_owned_window() is False -> every focus/park/badge # path leaves it completely alone. There is deliberately NO "adopt what exists" # seeding: unknown windows default to user-owned.+_PARK_MAX_S = 12.0        # a window may never stay parked longer than this+_GUARD_GRACE_S = 30.0     # how far past its deadline a guard may run before reset+_park_since: dict = {}    # hwnd -> monotonic when it was first parked+_guard_deadline = 0.0     # latest armed guard's deadline (monotonic) _owned_pids: set = set() _OWNED_EXE_PREFIXES = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor",                        "pcb_calculator", "bitmap2component")@@ -706,6 +722,26 @@ def _start_park_sweep() -> None:         quiet_since = None         while True:             try:+                # HARD CAPS (0.9.160). A leaked guard used to keep the sweep+                # active forever, so a launched window stayed parked (and+                # NOACTIVATE'd, hence taskbar-less) for HOURS - John found one+                # 5h in. Two watchdogs make that impossible: a guard that+                # overran its own deadline is reset, and a window parked past+                # _PARK_MAX_S is released no matter what the bridge is doing.+                now_m = _t.monotonic()+                if _guard_count > 0 and _guard_deadline and now_m > _guard_deadline + _GUARD_GRACE_S:+                    print(f"[focus] guard watchdog: {_guard_count} guard(s) overran "+                          f"by {int(now_m - _guard_deadline)}s - resetting", file=sys.stderr)+                    globals()["_guard_count"] = 0+                    unpark_all()+                    allow_activation_all()+                for h, since in list(_park_since.items()):+                    if now_m - since > _PARK_MAX_S:+                        print(f"[focus] park watchdog: releasing {h} after "+                              f"{int(now_m - since)}s parked", file=sys.stderr)+                        _unpark(h)+                        allow_activation(h)+                        _park_since.pop(h, None)                 active = _verb_inflight > 0 or _guard_count > 0                 if active:                     globals()["_sweep_ticks"] = _sweep_ticks + 1@@ -722,6 +758,7 @@ def _start_park_sweep() -> None:                                 _save_ledger()                             suppress_activation(h)                             _park_offscreen(h)+                            _park_since.setdefault(h, _t.monotonic())                             try:                                 from handlers.win_taskbar import apply_adom_badge                                 apply_adom_badge(h)  # "adom is driving this window"@@ -826,8 +863,16 @@ def _ensure_window_icon(hwnd) -> None:         WM_GETICON, WM_SETICON = 0x007F, 0x0080         GCLP_HICON, GCLP_HICONSM = -14, -34         get_cl = getattr(_user32, "GetClassLongPtrW", None) or _user32.GetClassLongW-        has = (_user32.SendMessageW(hwnd, WM_GETICON, 1, 0)-               or _user32.SendMessageW(hwnd, WM_GETICON, 0, 0)+        # SendMessageW BLOCKS until the target pumps messages - a loading KiCad+        # would freeze this thread (and with it the park sweep / focus guard),+        # which is exactly how a window got stranded parked for hours+        # (0.9.160). Always use the timeout form with SMTO_ABORTIFHUNG.+        def _get_icon(which):+            res = ctypes.c_ulong(0)+            _user32.SendMessageTimeoutW(hwnd, WM_GETICON, which, 0, 0x0002, 200,+                                        ctypes.byref(res))+            return res.value+        has = (_get_icon(1) or _get_icon(0)                or get_cl(hwnd, GCLP_HICON) or get_cl(hwnd, GCLP_HICONSM))         if has:             return@@ -843,10 +888,13 @@ def _ensure_window_icon(hwnd) -> None:             icons = (big, small)             _exe_icons[exe] = icons  # keep HICONs alive for process lifetime         big, small = icons+        res = ctypes.c_ulong(0)         if big and big.value:-            _user32.SendMessageW(hwnd, WM_SETICON, 1, big.value)+            _user32.SendMessageTimeoutW(hwnd, WM_SETICON, 1, big.value, 0x0002, 200,+                                        ctypes.byref(res))         if small and small.value:-            _user32.SendMessageW(hwnd, WM_SETICON, 0, small.value)+            _user32.SendMessageTimeoutW(hwnd, WM_SETICON, 0, small.value, 0x0002, 200,+                                        ctypes.byref(res))     except Exception:         pass @@ -904,6 +952,32 @@ def _park_offscreen(hwnd) -> None:         _park_last_error = f"{type(e).__name__}: {e}"  +def _sane_position(hwnd) -> None:+    """0.9.160: guarantee an unparked window lands somewhere the user can reach.+    A window parked before KiCad finished placing itself unparks to a garbage+    'true position' - John got a 406x354 sliver at y=993, mostly under the+    taskbar. Anything off-screen or peeking in by less than 200px is moved to a+    plain cascade slot, size preserved, without activation."""+    try:+        r = ctypes.wintypes.RECT()+        if not _user32.GetWindowRect(hwnd, ctypes.byref(r)):+            return+        sw = _user32.GetSystemMetrics(0)+        sh = _user32.GetSystemMetrics(1)+        w, h = max(r.right - r.left, 640), max(r.bottom - r.top, 480)+        visible_w = min(r.right, sw) - max(r.left, 0)+        visible_h = min(r.bottom, sh) - max(r.top, 0)+        if visible_w >= 200 and visible_h >= 200:+            return  # genuinely reachable already+        x, y = 120, 90+        SWP_NOSIZE, SWP_NOACTIVATE, SWP_NOZORDER = 0x0001, 0x0010, 0x0004+        _user32.SetWindowPos(hwnd, 0, x, y, w, h, SWP_NOACTIVATE | SWP_NOZORDER)+        print(f"[focus] repositioned {hwnd} to ({x},{y}) - it unparked "+              f"{visible_w}x{visible_h} on-screen", file=sys.stderr)+    except Exception:+        pass++ def _unpark(hwnd, to_bottom: bool = True) -> None:     try:         pos = _parked.pop(hwnd, None)@@ -936,10 +1010,25 @@ def _unpark(hwnd, to_bottom: bool = True) -> None:                 HWND_BOTTOM = 1                 _user32.SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0,                                      0x0010 | 0x0001 | 0x0002)  # NOACTIVATE|NOSIZE|NOMOVE+            _sane_position(hwnd)   # never leave it stranded off-screen+        _park_since.pop(hwnd, None)+        _ensure_window_icon(hwnd)  # a fresh button must not read as a blank doc+        try:+            from handlers.win_taskbar import apply_adom_badge+            apply_adom_badge(hwnd)+        except Exception:+            pass     except Exception:         pass  +def _clear_park_clock(hwnd=None) -> None:+    if hwnd is None:+        _park_since.clear()+    else:+        _park_since.pop(hwnd, None)++ def unpark_all() -> int:     """Place every parked window back at its true coordinates, bottom-z,     no activation. Called when the last guard exits."""@@ -947,6 +1036,7 @@ def unpark_all() -> int:     for h in list(_parked.keys()):         _unpark(h, to_bottom=True)         n += 1+    _clear_park_clock()     return n  @@ -1186,10 +1276,12 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float =      def _loop():         global _guard_count+        deadline = _t.monotonic() + seconds         if _guard_lock:             with _guard_lock:                 _guard_count += 1-        deadline = _t.monotonic() + seconds+                if deadline > _guard_deadline:+                    globals()["_guard_deadline"] = deadline         tick = 0         try:             _sweep_suppress_all()  # pre-suppress what's already up before the verb acts