← Commit history

1.9.150: #49 workspace race + progress-keyed hang ceiling

John Lauer ·dc4e73542f ·1mo ago ·parent a233da9
7 files changed +114−27
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.149\ No newline at end of file+1.9.150\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.149",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.149/adom-bridge-fusion-v1.9.149.zip",-  "sha256": "c4a1afea4fda8299a2cbf402a0f614b50e50e4b07ee600f25abfcafea005f462",-  "size": 480834,+  "version": "1.9.150",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.150/adom-bridge-fusion-v1.9.150.zip",+  "sha256": "bd0dc44eb9bb3cff7b8116b12e0ad6cc65bb2bf1133ac4af17160628d5a7817d",+  "size": 482346,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.149",+  "version": "1.9.150",   "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/fusion-bridge",   "author": "Adom Inc.",
fusion_detect.py+44
@@ -830,6 +830,50 @@ def _fusion_main_hwnd_quick():         return None  +class _FILETIME(ctypes.Structure):+    _fields_ = [("dwLowDateTime", ctypes.wintypes.DWORD),+                ("dwHighDateTime", ctypes.wintypes.DWORD)]+++def fusion_using_cpu(sample_s: float = 0.7) -> bool:+    """True if a Fusion-family process is ACTIVELY consuming CPU right now (#49 part 10, John+    2026-08-23: a heavy first-ever cloud mint can exceed a fixed wall-clock ceiling while still+    WORKING, and the hang gate then kills the very run the long-op registry exists to protect).++    A GIL-starved-but-progressing mint (STEP import, geometry, upload) burns CPU; a genuinely+    wedged process sits at ~0%. Sample the family's summed kernel+user CPU time twice and see if+    it advanced. Best-effort: any failure (non-Windows, no handle) returns False so the caller+    falls back to its wall-clock rule - never a reason to SKIP a kill on error."""+    try:+        k32 = ctypes.windll.kernel32+        k32.OpenProcess.restype = ctypes.wintypes.HANDLE+        PQLI = 0x1000  # PROCESS_QUERY_LIMITED_INFORMATION++        def _ticks():+            total = 0+            for pid in (_fusion_family_pids() or set()):+                h = k32.OpenProcess(PQLI, False, int(pid))+                if not h:+                    continue+                try:+                    ct, et, kt, ut = _FILETIME(), _FILETIME(), _FILETIME(), _FILETIME()+                    if k32.GetProcessTimes(h, ctypes.byref(ct), ctypes.byref(et),+                                           ctypes.byref(kt), ctypes.byref(ut)):+                        total += (kt.dwLowDateTime | (kt.dwHighDateTime << 32))+                        total += (ut.dwLowDateTime | (ut.dwHighDateTime << 32))+                finally:+                    k32.CloseHandle(h)+            return total++        t0 = _ticks()+        time.sleep(sample_s)+        t1 = _ticks()+        # CPU time is in 100ns ticks; >5ms of CPU burned in the window == actively working.+        return (t1 - t0) > 50000+    except Exception:+        return False++ def begin_background_envelope(seconds: float, minimize: bool = False):     """Guard the next `seconds` against Fusion-family focus theft. Fire-and-forget."""     global _ENVELOPE_COUNT
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.172",+  "version": "1.9.173",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+10−7
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.172",+  "version": "1.9.173",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,9 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }+  ] }\ No newline at end of file
server.py+53−13
@@ -2752,10 +2752,25 @@ def _confirmed_hung() -> dict:     import time as _t     # A bridge-dispatched long op (create_library mint/bind) legitimately wedges the main     # thread for minutes - never declare hung while one is in flight and under its ceiling.-    if _LONG_OP["since"] and (_t.time() - _LONG_OP["since"]) < 900:-        return {"hung": False, "phase": "long-op-in-flight",-                "longOp": _LONG_OP["name"],-                "longOpElapsedSec": round(_t.time() - _LONG_OP["since"], 1)}+    # PROGRESS-KEYED, not a fixed wall-clock (#49 part 10, John 2026-08-23: a 94-connect part's+    # first-ever cloud mint ran ~1600s and the old 900s wall turned the gate back into the kill+    # the registry exists to prevent). Under the (raised) ceiling: always protected. PAST it:+    # still protected as long as Fusion is actively burning CPU (a slow-but-working mint) - only+    # a long-op that is BOTH past the ceiling AND idle (genuinely wedged) falls through to the+    # kill-grade rules below.+    if _LONG_OP["since"]:+        _lo_el = _t.time() - _LONG_OP["since"]+        _protected = _lo_el < 1800+        if not _protected:+            try:+                from fusion_detect import fusion_using_cpu+                _protected = fusion_using_cpu()+            except Exception:+                _protected = False+        if _protected:+            return {"hung": False, "phase": "long-op-in-flight",+                    "longOp": _LONG_OP["name"], "longOpElapsedSec": round(_lo_el, 1),+                    "keptBy": ("under-ceiling" if _lo_el < 1800 else "fusion-cpu-active")}     try:         from handlers.fusion_ui import is_fusion_hung         _h = is_fusion_hung()@@ -3293,20 +3308,32 @@ def _ensure_library_active(lbr_path: str, timeout_s: int = 60) -> dict:     import time as _t     stem = lbr_path.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0].lower()     deadline = _t.time() + timeout_s-    tried_activate = False+    last_ws = None     while _t.time() < deadline:         try:             st = _proxy_to_addin("get_app_state", {}, timeout=6)-            active = str((st.get("data") or {}).get("activeDocument") or "")-            if st.get("success") and stem in active.lower():-                return {"ok": True, "activeDocument": active}-            if st.get("success") and not tried_activate:-                tried_activate = True+            _d = st.get("data") or {}+            active = str(_d.get("activeDocument") or "")+            # THE WORKSPACE, not just the document (#49 parts 6/7, John 2026-08-23): the 3D+            # bind leaves the .lbr the ACTIVE DOCUMENT but the WORKSPACE on "Design" (the 3D+            # view), and EDIT then fails "Not in Electronics editor (current: Design)". So the+            # library is only truly ready when the doc matches AND the workspace is an+            # ELECTRONICS EDITOR (Library / Schematic / Board), never Design / the container.+            ws = str(_d.get("activeWorkspace") or "")+            last_ws = ws+            _wl = ws.lower()+            ws_ok = ("librar" in _wl or "schematic" in _wl or "board" in _wl+                     or "pcb" in _wl) and "electronics design" not in _wl+            if st.get("success") and stem in active.lower() and ws_ok:+                return {"ok": True, "activeDocument": active, "activeWorkspace": ws}+            # Re-activate EVERY iteration until the workspace actually switches - a single+            # activate can lose the race with the 3D step's workspace change (was tried once).+            if st.get("success"):                 _proxy_to_addin("activate_document", {"name": stem}, timeout=10)         except Exception:             pass-        _t.sleep(2)-    return {"ok": False, "activeDocument": None}+        _t.sleep(1.5)+    return {"ok": False, "activeDocument": None, "activeWorkspace": last_ws}   def _create_3d_package(lbr_path: str, timeout: int = 60) -> dict:@@ -7436,7 +7463,20 @@ def _show_library_view(name, suffix, lbr_path=None):     for _busy_try in range(7):         edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix)}, timeout=15)         _etxt = str(edit.get("error") or "")-        if edit.get("success") or ("BUSY" not in _etxt and "busy" not in _etxt):+        if edit.get("success"):+            break+        # NOT-IN-ELECTRONICS-EDITOR RETRY (#49 parts 6/7, 2026-08-23): the 3D step can leave+        # Fusion on the Design workspace, and even after _ensure_library_active a stray EDIT+        # can land a beat before the switch completes. Re-ensure the library editor and retry+        # rather than losing the frame - the same "transient, wait it out" law as BUSY.+        if lbr_path and ("not in electronics editor" in _etxt.lower()+                         or "current: design" in _etxt.lower()):+            print("[Fusion Bridge] EDIT %s.%s hit Design workspace (attempt %d) - re-entering "+                  "the library editor and retrying" % (name, suffix, _busy_try + 1))+            _ensure_library_active(lbr_path, timeout_s=20)+            time.sleep(1.0)+            continue+        if "BUSY" not in _etxt and "busy" not in _etxt:             break         print("[Fusion Bridge] EDIT %s.%s deferred - main thread busy (attempt %d), waiting 15s"               % (name, suffix, _busy_try + 1))