app
Adom Desktop - 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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
"""adom-desktop loader for KiCad embedded Python.
This file is a tiny shim that fires at every KiCad-bundled Python init
(see PLAN.md Phase 4 v4 architectural notes for the discovery story).
It does TWO things:
1. Writes a process-discovery breadcrumb to %TEMP%\\adom-kicad-process-<exe>.json
so adom-desktop can confirm which KiCad executables exposed Python.
This is the same diagnostic file the original Phase 4 probe wrote;
adom-desktop's kicad-detect code uses it for the `embeddedPython`
field of `kicad_list_versions`.
2. Calls `adom_bridge.start()` to spin up the per-process WebSocket
server (Phase 2 reverse bridge). If `adom_bridge` import fails
(e.g. plugin not deployed yet, or websockets lib unavailable in an
older KiCad), we just log the error and the discovery breadcrumb
still gets written so the orchestrator sees this process at all.
Both steps are wrapped in try/except so a bug here never crashes KiCad.
Honors ADOM_KICAD_BRIDGE_DISABLE=1 (skip the bridge but still write
discovery) and ADOM_KICAD_PROBE_DISABLE=1 (skip everything).
"""
try:
import json
import os
import sys
import traceback
from datetime import datetime
from pathlib import Path
if os.environ.get("ADOM_KICAD_PROBE_DISABLE") == "1":
raise SystemExit
_OUT_DIR = Path(os.environ.get("TEMP") or os.environ.get("TMP") or "/tmp")
_EXE_NAME = (Path(sys.executable).stem or "unknown").lower()
_PROCESS_PATH = _OUT_DIR / f"adom-kicad-process-{_EXE_NAME}.json"
_ERR_PATH = _OUT_DIR / f"adom-kicad-loader-{_EXE_NAME}.error.log"
# -- Step 1: discovery breadcrumb (lightweight, always written) --
try:
_PROCESS_PATH.write_text(json.dumps({
"stage": "usercustomize-fired",
"firedAt": datetime.utcnow().isoformat() + "Z",
"exeName": _EXE_NAME,
"executable": sys.executable,
"argv": sys.argv,
"cwd": os.getcwd(),
"pythonVersion": sys.version,
"pid": os.getpid(),
}, indent=2, default=str), encoding="utf-8")
except OSError:
pass
# -- Step 2: start the reverse bridge (Phase 2 MVP) --
try:
import adom_bridge # type: ignore[import-not-found]
port = adom_bridge.start()
# Stash the port back into the discovery file so consumers can
# find it without parsing both files.
if port is not None:
try:
payload = json.loads(_PROCESS_PATH.read_text(encoding="utf-8"))
payload["bridgePort"] = port
payload["bridgeVersion"] = adom_bridge.__version__
_PROCESS_PATH.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
except (OSError, ValueError):
pass
except Exception:
# adom_bridge missing or its start() raised. Log but don't fail.
try:
_ERR_PATH.write_text(traceback.format_exc(), encoding="utf-8")
except OSError:
pass
except SystemExit:
pass
except Exception:
# Last-resort safety net.
try:
import os as _os
import traceback as _tb
_tmp = _os.environ.get("TEMP") or _os.environ.get("TMP") or "/tmp"
with open(_os.path.join(_tmp, "adom-kicad-usercustomize-fatal.log"), "w", encoding="utf-8") as _f:
_f.write(_tb.format_exc())
except Exception:
pass