app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
← Commit history
v1.6.38: deterministic process-based seat/licensing dialog detection
6 files changed
+146−20
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.37+1.6.38
CHANGELOG.md+11@@ -3,6 +3,17 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge ยท GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.38 - 2026-07-06++- **Deterministic, process-based detection of the seat/licensing dialog** (John: track state in CODE,+ not via the AI following skills). The "Active Sessions Exceeded" dialog's window TITLE is literally+ just "Fusion360" (the text is CEF body content), so the title-based classifier never caught it - it+ silently blocked Fusion's main thread for a whole session. New `family_windows()` enumerates visible+ top-level windows by OWNING PROCESS (Fusion360 / FusionLauncher / AdskIdentityManager), and+ `find_licensing_dialog()` finds the licensing modal by process + dialog size. `fusion_start` now+ auto-reclaims on it deterministically, and `fusion_readiness` reports `licensingDialog:true` with a+ clear hint instead of a vague "add-in not responding" hang.+ ## 1.6.37 - 2026-07-06 - **`fusion_readiness` no longer lies `ready:true` for a stuck/dead Fusion** (John caught it). It only
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.37",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.37/adom-bridge-fusion-v1.6.37.zip",- "sha256": "3274f36e47192bb2a6ca48cb8262f6aba386e3dd9e9f4c031b73ed29339f4ca3",- "size": 337727,+ "version": "1.6.38",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.38/adom-bridge-fusion-v1.6.38.zip",+ "sha256": "9d224d1ce06beffb8962bcb69df6a6389e60b16f2b2bbf9e73c1a357311a0840",+ "size": 339225, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-07-06T11:44:57Z"+ "released_at": "2026-07-06T12:09:09Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.37",+ "version": "1.6.38", "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.",
fusion_detect.py+96@@ -328,6 +328,102 @@ def _fusion_family_pids() -> set: return pids +def _family_pid_images() -> dict:+ """{pid: image_name} for every running Autodesk-launch-family process."""+ try:+ cnw = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0+ out = subprocess.check_output(+ ["tasklist", "/FO", "CSV", "/NH"],+ stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=cnw,+ ).decode("utf-8", "replace")+ except Exception:+ return {}+ m = {}+ for mo in re.finditer(r'"([^"]+\.exe)","(\d+)"', out):+ img = mo.group(1).lower()+ if img in _FUSION_FAMILY_IMAGES:+ m[int(mo.group(2))] = img+ return m+++def family_windows() -> list:+ """Visible top-level windows owned by the Autodesk launch family (Fusion360 /+ FusionLauncher / AdskIdentityManager). Each dict: {hwnd, title, pid, image, width,+ height, left, top}.++ This is how the launch state machine detects the SEAT/licensing dialog+ DETERMINISTICALLY - by OWNING PROCESS + size, NOT the "Fusion360" window TITLE that+ fooled the title-based classifier for a whole day (the "Active Sessions Exceeded"+ dialog's title is literally just "Fusion360"; the real text is CEF body content).+ Never raises."""+ results = []+ try:+ user32 = ctypes.windll.user32+ HWND = ctypes.wintypes.HWND+ user32.GetWindowThreadProcessId.argtypes = [HWND, ctypes.POINTER(ctypes.wintypes.DWORD)]+ user32.IsWindowVisible.argtypes = [HWND]+ fam = _fusion_family_pids()+ if not fam:+ return results+ pid_img = _family_pid_images()+ except Exception:+ return results+ WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)++ def cb(hwnd, _l):+ try:+ if not user32.IsWindowVisible(hwnd):+ return True+ wpid = ctypes.wintypes.DWORD(0)+ user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid))+ if wpid.value not in fam:+ return True+ n = user32.GetWindowTextLengthW(hwnd)+ title = ""+ if n:+ buf = ctypes.create_unicode_buffer(n + 1)+ user32.GetWindowTextW(hwnd, buf, n + 1)+ title = buf.value+ rect = ctypes.wintypes.RECT()+ user32.GetWindowRect(hwnd, ctypes.byref(rect))+ w = rect.right - rect.left+ h = rect.bottom - rect.top+ if w <= 0 or h <= 0:+ return True+ results.append({+ "hwnd": int(hwnd), "title": title, "pid": int(wpid.value),+ "image": pid_img.get(int(wpid.value), ""),+ "width": w, "height": h, "left": rect.left, "top": rect.top,+ })+ except Exception:+ pass+ return True++ cb_c = WNDENUMPROC(cb)+ _callbacks.append(cb_c)+ try:+ user32.EnumWindows(cb_c, 0)+ except Exception:+ pass+ try:+ _callbacks.remove(cb_c)+ except ValueError:+ pass+ return results+++def find_licensing_dialog():+ """The seat/licensing modal ("Active Sessions Exceeded", or a sign-in prompt): a+ family window that is DIALOG-SIZED (the maximized main app is far bigger; tooltips+ far smaller). Detected by owning process + size, so the "Fusion360" title never+ matters. Returns the window dict, or None. The seat dialog is ~1235x527."""+ for w in family_windows():+ ww, hh = w["width"], w["height"]+ if 640 <= ww <= 1500 and 320 <= hh <= 820:+ return w+ return None++ def _fusion_pids() -> set: """PIDs of running Fusion360.exe processes (via tasklist CSV). Empty on error.""" try:
server.py+32−13@@ -21,7 +21,7 @@ from pathlib import Path # Ensure the plugin root is on the path sys.path.insert(0, str(Path(__file__).parent)) -from fusion_detect import detect_fusion, ensure_fusion_running, wait_for_addin, _is_fusion_running, fusion_update_in_progress+from fusion_detect import detect_fusion, ensure_fusion_running, wait_for_addin, _is_fusion_running, fusion_update_in_progress, find_licensing_dialog, family_windows from handlers.open_design import handle_open_design from handlers.close_fusion import handle_close_fusion, handle_fusion_stop, handle_fusion_kill from handlers.dismiss_recovery import dismiss_recovery_dialog@@ -172,20 +172,21 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict: if _dlg.get("category") in AUTO_DISMISS_CATEGORIES: close_dialog_bg(_dlg.get("hwnd")) # benign ack (WM_CLOSE == Cancel/OK) - # SEAT CONFLICT ("Active Sessions Exceeded") -> AUTO-RECLAIM the license, never ask- # (John 2026-07-06: "just grab the license back... no harm, the user can reclaim on- # the other machine"). The dialog is a CEF modal that ignores background SendInput,- # so instead of fighting it we free the seat at the SOURCE (stop Fusion on peer ADs- # via cross-AD), kill the local instance stuck on the dialog, and relaunch clean -- # no conflict dialog reappears. Guarded against infinite recursion by a one-shot arg.- _seat = [d for d in launch_dialogs if d.get("category") == "session_conflict_decision"]- if _seat and not (args or {}).get("_seatReclaimAttempted"):+ # SEAT/LICENSING dialog ("Active Sessions Exceeded") -> AUTO-RECLAIM the license,+ # never ask (John 2026-07-06: "just grab the license back... no harm"). DETECT IT+ # DETERMINISTICALLY by owning PROCESS + dialog size (find_licensing_dialog), NOT the+ # "Fusion360" window title that fooled the title-based classifier for a whole day.+ # The dialog is a CEF modal that ignores background SendInput, so instead of fighting+ # it we free the seat at the SOURCE (stop Fusion on peer ADs) + let the Autodesk+ # server release it, kill the local instance, and relaunch clean. One-shot guarded.+ _lic = find_licensing_dialog()+ if _lic and not (args or {}).get("_seatReclaimAttempted"): _reclaim_seat_from_peers() try: handle_fusion_kill(fusion_info, {}) except Exception: pass- _t.sleep(2.0)+ _t.sleep(4.0) # give the Autodesk licensing server a moment to release the seat return _handle_launch(fusion_info, {**(args or {}), "_seatReclaimAttempted": True}) # Surface anything still needing the USER: remaining seat DECISIONS (only if the@@ -852,6 +853,16 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: # free; requiring it stops a half-launched or stuck Fusion from reading as ready. ready = installed and running and addin_ok and not needs_signin + # SEAT/LICENSING dialog blocking the launch - detected DETERMINISTICALLY by owning+ # process + dialog size (not the "Fusion360" title). Reported so a caller knows the+ # block is a seat conflict (fusion_start auto-reclaims it), not a generic hang.+ licensing = False+ if running and not addin_ok:+ try:+ licensing = find_licensing_dialog() is not None+ except Exception:+ licensing = False+ # Fusion applying an AUTO-UPDATE (two+ webdeploy builds) crash-restarts itself every # ~30-60s, so a verb intermittently sees running:false. That is NOT a crash or a # bridge failure - surface it as a distinct non-fatal `updating` flag so a driving AI@@ -889,11 +900,16 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: hint = stale_info["_staleHint"] elif ready: hint = "READY - drive Fusion (fusion_open_lbr / build_library_3d / etc.)."+ elif installed and running and licensing:+ hint = ("Fusion is blocked by a SEAT/LICENSING dialog (e.g. 'Active Sessions Exceeded') - "+ "detected by owning process + dialog size, NOT by title. Call fusion_start: it "+ "AUTO-RECLAIMS the seat (frees it from peer machines, then relaunches clean). No user "+ "action needed - this is not a crash.") elif installed and running: hint = ("Fusion is RUNNING but its add-in is NOT responding yet - NOT ready to drive. Either it "- "is still finishing launch (poll fusion_readiness a few more times), or the add-in did "- "not load (fusion_stop then fusion_start to re-deploy + re-load it). Do NOT fire "- "modeling/export verbs until ready:true.")+ "is still finishing launch / signing in (poll fusion_readiness a few more times), or the "+ "add-in did not load (fusion_stop then fusion_start to re-deploy + re-load it). Do NOT "+ "fire modeling/export verbs until ready:true.") elif installed: hint = "Fusion is INSTALLED but not running - call fusion_start (blocks until the add-in is ready), then retry." else:@@ -920,6 +936,9 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: if needs_signin: result["needsSignin"] = True result["statusVerb"] = "fusion_readiness"+ if licensing:+ result["licensingDialog"] = True+ result["statusVerb"] = "fusion_readiness" return result