← Commit history

v1.6.64: ad_client DISCOVERS AD's loopback direct-API when ADOM_DIRECT_API_URL is not injected (AD 1.9.103 VM had the API up on 127.0.0.1 but no env var, so available() short-circuited False and everything fell back to slow CLI relays). Fixes seat-resolve + notify + ownedPopupCount at the ROOT; honest seat click-counting.

John Lauer ·0c2b46874a ·3mo ago ·parent 9dc5138
5 files changed +78−10
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.63\ No newline at end of file+1.6.64\ No newline at end of file
ad_client.py+70−2
@@ -33,9 +33,77 @@ _caps_cache = None _self_name_cache = None  +_discovered_base = None  # None = not tried, "" = tried+failed, str = discovered base URL+++def _raw_get(url, timeout=4):+    """Low-level GET that does NOT go through _base_url (used by discovery so it can't recurse).+    Returns parsed JSON, or None."""+    try:+        req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET")+        with urllib.request.urlopen(req, timeout=timeout) as resp:+            return json.loads(resp.read().decode("utf-8", "replace"))+    except Exception:+        return None+++def _looks_like_ad_api(data) -> bool:+    """Does a /commands response look like AD's verb catalog (so we don't bind a random service)?"""+    if isinstance(data, dict):+        if isinstance(data.get("commands"), list):+            return True+        # AD's /commands also carries a _hint mentioning POST /command+        if "command" in (data.get("_hint") or "").lower():+            return True+    return isinstance(data, list)+++def _discover_direct_api():+    """Find AD's loopback direct-API base by port-probing, for the case where AD did NOT inject+    ADOM_DIRECT_API_URL into our spawn env (seen live on an AD 1.9.103 Hyper-V VM: the direct API+    WAS up on 127.0.0.1 but the env var was absent, so available() short-circuited to False and the+    bridge fell back to slow CLI relays - John caught it 2026-07-14). We enumerate the loopback TCP+    ports adom-desktop.exe is LISTENING on and probe each `/commands`; the one that returns AD's verb+    catalog is the direct API. Cached for the process lifetime. Windows-only + best-effort; never raises."""+    global _discovered_base+    if _discovered_base is not None:+        return _discovered_base or None+    _discovered_base = ""  # default: tried and failed (so we don't re-probe every call)+    try:+        import subprocess as _sp, re as _re+        cnw = getattr(_sp, "CREATE_NO_WINDOW", 0)+        tl = _sp.run(["tasklist", "/FI", "IMAGENAME eq adom-desktop.exe", "/FO", "CSV", "/NH"],+                     capture_output=True, text=True, timeout=8, creationflags=cnw).stdout+        pids = set(_re.findall(r'"adom-desktop\.exe","(\d+)"', tl))+        if not pids:+            return None+        ns = _sp.run(["netstat", "-ano", "-p", "TCP"], capture_output=True, text=True,+                     timeout=8, creationflags=cnw).stdout+        seen = []+        for line in ns.splitlines():+            m = _re.search(r'\s(?:127\.0\.0\.1|\[::1\]):(\d+)\s+\S+\s+LISTENING\s+(\d+)', line)+            if m and m.group(2) in pids:+                p = int(m.group(1))+                if p not in seen:+                    seen.append(p)+        for p in seen:+            base = f"http://127.0.0.1:{p}"+            if _looks_like_ad_api(_raw_get(base + "/commands")):+                _discovered_base = base+                return base+    except Exception:+        pass+    return None++ def _base_url():-    """The loopback direct-API base AD injects into our spawn env, else None."""-    return (os.environ.get("ADOM_DIRECT_API_URL") or "").rstrip("/") or None+    """The loopback direct-API base: the URL AD injects into our spawn env, else one we DISCOVER by+    probing adom-desktop's loopback ports (AD sometimes fails to inject the env var even though the+    API is up - see _discover_direct_api). None only if neither yields a reachable API."""+    env = (os.environ.get("ADOM_DIRECT_API_URL") or "").rstrip("/")+    if env:+        return env+    return _discover_direct_api()   def _token():
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.63",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.63/adom-bridge-fusion-v1.6.63.zip",-  "sha256": "8db07d4fb1be353eb055cc37681e14700b4c4da3e47cd2eecbb46c14cad91a17",-  "size": 357907,+  "version": "1.6.64",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.64/adom-bridge-fusion-v1.6.64.zip",+  "sha256": "98761caabf5b6a40e68b9cfd11b61ab08ed741a22d148505b45c07f4a711c539",+  "size": 359076,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.63",+  "version": "1.6.64",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop-fusion-bridge",   "author": "Adom Inc.",
server.py+2−2
@@ -185,8 +185,8 @@ def _resolve_seat_dialog(max_clicks: int = 8) -> dict:             break         saw = True         parent = dlg.get("owner")-        _resolve_seat_via_uia(dlg)-        clicks += 1+        if _resolve_seat_via_uia(dlg):     # count only clicks that actually landed, so+            clicks += 1                    # seatDialogAutoResolved isn't a lie when ad_client is down         _t.sleep(1.5)         # Ground-truth confirm via parent/child screenshot (John's insisted-on check).         cnt = _owned_popup_count(parent)