← Commit history

v0.9.189: new kicad_plugin_diagnose - one read-only call that answers 'why is the reverse bridge not loading' with a verdict (deployed? imported by KiCad? what did the loader log? is the embedded stdlib complete?)

John Lauer ·5de2ca1001 ·1mo ago ·parent 7949b90
6 files changed +167−7
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.188+0.9.189
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.188",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.188/adom-bridge-kicad-v0.9.188.zip",-  "sha256": "551063bec787383debea7324f659ee9274195fd530f9aae840a452271815547f",-  "size": 455691,+  "version": "0.9.189",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.189/adom-bridge-kicad-v0.9.189.zip",+  "sha256": "7f13596197384490e009ecea5f1a5cd8732d435a5f5b59e149be3a7ffbcaf451",+  "size": 458771,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-19T20:37:29Z",+  "released_at": "2026-08-19T20:51:11Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.189.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.188",+  "version": "0.9.189",   "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/plugin_diagnose.pyadded+152
@@ -0,0 +1,152 @@+"""plugin_diagnose.py — kicad_plugin_diagnose: why is the reverse bridge not loading?++Born out of wiki issue #34 (Colby Knox, 2026-08-19). The plugin binds on both of+this maintainer's boxes and not on his, and answering that took several rounds of+"please send me this file, now that one". Every artifact needed to tell the four+possible causes apart is readable from the box itself, so this verb collects them+in one call:++  1. Was the loader DEPLOYED?            -> the files, with sizes and ages+  2. Did KiCad IMPORT it?                -> a __pycache__ entry next to it is proof+  3. Did the loader RUN and what did it say? -> its own log + the process breadcrumbs+  4. Could the payload import at all?     -> KiCad's embedded stdlib, which has been+                                             found incomplete on a real machine+                                             (a missing Lib/urllib made every import+                                             of the payload fail; no bridge code can+                                             work around that)++Read-only. No windows, no side effects.+"""+import json+import os+import time+from pathlib import Path+++def _stat(p: Path) -> dict:+    try:+        st = p.stat()+        return {"exists": True, "bytes": st.st_size,+                "ageMin": int((time.time() - st.st_mtime) / 60)}+    except Exception:+        return {"exists": False}+++def _dir_listing(d: Path, pattern: str = "*") -> list:+    out = []+    try:+        for p in sorted(d.glob(pattern)):+            e = _stat(p)+            e["name"] = p.name+            out.append(e)+    except Exception:+        pass+    return out+++def _tail(p: Path, lines: int = 25) -> list:+    try:+        return p.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:]+    except Exception:+        return []+++def handle_plugin_diagnose(kicad_info: dict, args: dict) -> dict:+    """Collect every artifact that explains whether the reverse-bridge plugin+    loaded, and why not. Read-only; safe to run any time."""+    appdata = os.environ.get("APPDATA") or ""+    userprofile = os.environ.get("USERPROFILE") or str(Path.home())+    temp = Path(os.environ.get("TEMP") or "/tmp")+    version = str(kicad_info.get("version") or "10.0")++    report = {"success": True, "kicadVersion": version}++    # 1 + 2: the plugin-directory loader, and whether KiCad imported it+    pdirs = []+    base = Path(appdata) / "kicad" if appdata else None+    if base:+        for vdir in sorted([d for d in base.glob("*") if d.is_dir()]):+            pdir = vdir / "scripting" / "plugins"+            if not pdir.exists():+                continue+            loader = pdir / "adom_bridge_plugin.py"+            pyc = _dir_listing(pdir / "__pycache__", "adom_bridge_plugin*.pyc")+            pdirs.append({+                "dir": str(pdir),+                "loader": {**_stat(loader), "name": "adom_bridge_plugin.py"},+                "importedByKiCad": bool(pyc),      # a .pyc is proof of import+                "pycache": pyc,+                "otherFiles": [f["name"] for f in _dir_listing(pdir, "*.py")],+            })+    report["pluginDirs"] = pdirs++    # the user-site payload (the other deploy target)+    site = Path(userprofile) / "Documents" / "KiCad" / version / "3rdparty" / "Python311" / "site-packages"+    report["userSite"] = {"path": str(site), "files": _dir_listing(site, "adom*")}+    std_site = Path(appdata) / "Python" / "Python311" / "site-packages" if appdata else None+    if std_site:+        report["stdUserSite"] = {"path": str(std_site), "files": _dir_listing(std_site, "adom*")}++    # 3: what the loader itself said, and which exes ever ran the bootstrap+    report["loaderLog"] = _tail(temp / "adom-kicad-plugin-loader.log")+    report["breadcrumbs"] = [+        {"name": p.name, **_stat(p)} for p in sorted(temp.glob("adom-kicad-process-*.json"))+    ]+    report["loaderErrors"] = {p.name: _tail(p, 12)+                              for p in sorted(temp.glob("adom-kicad-loader-*.error.log"))}++    # 4: is KiCad's embedded stdlib complete? (a real machine was missing urllib,+    # which made every import of the payload fail with ModuleNotFoundError)+    stdlib = {}+    bin_dir = Path(kicad_info.get("kicad_exe") or "").parent if kicad_info.get("kicad_exe") else None+    if bin_dir and bin_dir.exists():+        lib = bin_dir / "Lib"+        stdlib["libDir"] = str(lib)+        for mod in ("urllib", "email", "http", "json", "encodings", "socket.py", "threading.py"):+            stdlib[mod] = (lib / mod).exists()+        stdlib["complete"] = all(v for k, v in stdlib.items()+                                 if isinstance(v, bool))+    report["embeddedStdlib"] = stdlib++    # live instances, for completeness+    try:+        from handlers.bridge_client import handle_bridge_status+        st = handle_bridge_status(kicad_info, {})+        report["live"] = st.get("summary") or {}+        report["instances"] = [{k: p.get(k) for k in ("alive", "exeName", "pid", "port")}+                               for p in (st.get("plugins") or [])]+    except Exception as e:+        report["live"] = {"error": str(e)[:120]}++    # the verdict, so a human does not have to correlate five sections by hand+    deployed = any(d["loader"].get("exists") for d in pdirs)+    imported = any(d["importedByKiCad"] for d in pdirs)+    ran = bool(report["loaderLog"])+    alive = (report.get("live") or {}).get("alive") or 0+    if alive:+        verdict = "WORKING: the plugin is bound and answering."+    elif not deployed:+        verdict = ("NOT DEPLOYED: adom_bridge_plugin.py is missing from KiCad's "+                   "scripting/plugins. Run kicad_install_plugin.")+    elif not imported and not ran:+        verdict = ("DEPLOYED BUT NEVER IMPORTED: no __pycache__ entry and no loader "+                   "log, so KiCad has not scanned its plugin directory in this "+                   "install. That scan happens when a BOARD EDITOR opens - if one "+                   "has been opened and this still says never imported, this KiCad "+                   "build does not scan that directory and the hook has to change.")+    elif ran and not alive:+        verdict = ("IMPORTED BUT DID NOT START: read loaderLog below - it names the "+                   "exact failure. 'No module named X' means KiCad's embedded stdlib "+                   "is incomplete (see embeddedStdlib); a repair install fixes it.")+    else:+        verdict = ("IMPORTED, no log yet: the loader arms a background thread and "+                   "retries the payload import for ~60s. Re-run shortly.")+    report["verdict"] = verdict+    report["_hint"] = (+        "One-call answer to 'why is the reverse bridge not loading'. importedByKiCad "+        "is the key field: a __pycache__ entry beside the loader is proof KiCad "+        "imported it. If deployed but never imported, the plugin-directory scan is "+        "not happening in this build. If imported but not alive, loaderLog names the "+        "cause. Wiki issue #34."+    )+    return report
server.py+8
@@ -69,6 +69,7 @@ from handlers.export_molecule import handle_export_molecule from handlers.demo import handle_demo from handlers.make_part_project import handle_make_part_project from handlers.export_part import handle_export_part+from handlers.plugin_diagnose import handle_plugin_diagnose from handlers.settings import handle_get_settings, handle_set_settings # v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer # (NOT winget, which lags the real release). check_for_updates is read-only;@@ -350,6 +351,12 @@ _VERB_CATALOG = {     "readiness": {"summary": "Read-only host-app detection — is KiCad installed/ready?", "long": False,         "hint": "Zero side effects; returns {detected, ready, versionCount, defaultVersion}. Poll before driving.",         "related": ["kicad_list_versions", "kicad_upgrade", "kicad_describe"], "pitfalls": ["there is NO prewarm — KiCad is a host app you install via kicad_upgrade, not a download Bridge warms"]},+    "plugin_diagnose": {"summary": "Why is the reverse-bridge plugin not loading? One call, every artifact.",+                        "long": False,+                        "hint": "Read-only. Reports whether the loader is deployed, whether KiCad IMPORTED it "+                                "(a __pycache__ entry is proof), what the loader logged, and whether KiCad's "+                                "embedded stdlib is complete - plus a plain-language verdict. Wiki issue #34.",+                        "related": ["kicad_install_plugin", "kicad_bridge_status"], "pitfalls": []},     "describe": {"summary": "This catalog — every verb the bridge exposes.", "long": False,         "hint": "Read-only full verb catalog; the completeness escape hatch. Per-call _hints carry you in practice.",         "related": ["kicad_readiness"], "pitfalls": []},@@ -579,6 +586,7 @@ COMMAND_HANDLERS = {     "state": handle_state,     "uninstall": handle_uninstall,     "install_plugin": plugin_install.handle_install_plugin,+    "plugin_diagnose": handle_plugin_diagnose,     # v1.7.12+: headless lint pre-checks via kicad-cli     "run_erc": handle_run_erc,     "lint_board": handle_lint_board,