← Commit history

v1.6.40: auto-resolve seat dialog via UIA background click in deterministic launch loop

John Lauer ·2fc3d1b80f ·3mo ago ·parent 25c1d41
5 files changed +80−87
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.39\ No newline at end of file+1.6.40\ No newline at end of file
CHANGELOG.md+13
@@ -3,6 +3,19 @@ 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.40 - 2026-07-06++- **THE seat-dialog fix: `fusion_start` now auto-resolves "Active Sessions Exceeded" via UIA in the+  BACKGROUND** (John pushed me back to UIA - it was the answer). The launch handler is now a+  deterministic state loop: poll the add-in round-trip (ground truth for drivable) while, each tick,+  detecting the seat dialog (`find_licensing_dialog`, by process+size) and invoking its native+  'Continue' button with `desktop_ui_click` via the AD direct API - which UIA-Invokes it WITHOUT+  foregrounding (proven: "Clicked in the BACKGROUND - the window did NOT come to the foreground").+  Suspend is pre-selected, so Continue grabs the license. This un-sticks the launch, which is what+  caused the endless foreground-stealing: a blocked seat dialog made Fusion retry sign-in in a loop,+  foregrounding every retry. No more kill+relaunch (the licensing server keeps the seat anyway) and+  no notify/punt to the user.+ ## 1.6.39 - 2026-07-06  - **Dev skill: added THE CORE GOAL** (John) - the entire reason this bridge exists is for the AI to
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.39",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.39/adom-bridge-fusion-v1.6.39.zip",-  "sha256": "5df9ca5388a850cd77291bd4fba74e2124205e36b492a0e1fa2f736de436ca4e",-  "size": 340095,+  "version": "1.6.40",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.40/adom-bridge-fusion-v1.6.40.zip",+  "sha256": "f82a746b72475e247f420a14a7b67a313eca9c12d6a02dc5ed0cb57db960ede1",+  "size": 339593,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-06T12:17:21Z"+  "released_at": "2026-07-06T12:56:14Z" }
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.39",+  "version": "1.6.40",   "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+60−80
@@ -116,6 +116,27 @@ def _reclaim_seat_from_peers() -> int:     return n  +def _resolve_seat_via_uia(dlg: dict) -> bool:+    """Resolve the 'Active Sessions Exceeded' seat dialog by UIA-invoking its native+    'Continue' button (Suspend is pre-selected) - entirely IN THE BACKGROUND.++    THE breakthrough (John 2026-07-06, after a long day of failing): UIA Invoke does+    NOT foreground the window, so this grabs the license without ever disturbing the+    user - unlike SendInput coordinate clicks (need foreground) or kill+relaunch (the+    server keeps the seat). Proven live: `desktop_ui_click {hwnd, name:"Continue"}`+    returned "Clicked in the BACKGROUND - the window did NOT come to the foreground"+    and the dialog cleared. The bridge calls the AD `desktop_ui_click` verb on ITSELF+    via the AD 1.9.84 direct API. Never raises."""+    try:+        hwnd = dlg.get("hwnd")+        if not hwnd or not ad_client.available():+            return False+        res = ad_client.call("desktop_ui_click", {"hwnd": hwnd, "name": "Continue"})+        return bool(res and (res.get("success") or res.get("status") == "ok"))+    except Exception:+        return False++ def _handle_launch(fusion_info: dict, args: dict) -> dict:     """Launch Fusion 360 and optionally wait for the AdomBridge add-in."""     # LIVE detect (not the stale bridge-start fusion_info snapshot) so a launch right@@ -172,86 +193,43 @@ 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/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()+    # ── DETERMINISTIC launch state loop (John 2026-07-06: track + handle state in+    #    CODE, in the BACKGROUND, never punt to the user, never foreground). Poll the+    #    add-in round-trip (the ground truth for "drivable") while, each tick:+    #      • AUTO-RESOLVING the seat/licensing dialog ("Active Sessions Exceeded") via+    #        UIA Invoke of its 'Continue' button - background, no foreground (THE fix;+    #        Suspend is pre-selected so Continue grabs the license). This is what+    #        un-sticks the launch: a blocked seat dialog made Fusion retry sign-in in a+    #        loop, foregrounding on every retry - the source of the endless fg-steal.+    #      • dismissing benign recovery/startup dialogs.+    #    Detection is deterministic (find_licensing_dialog by owning process + size,+    #    NOT the "Fusion360" title). No kill/relaunch (the server keeps the seat), no+    #    notify (we handle it ourselves). ──+    _RELAY_BUDGET = 50  # keep total handler time under AD's ~60s relay cap+    _deadline = _t.time() + max(6, int(_RELAY_BUDGET - (_t.time() - _start)))+    _seat_clicks = 0+    addin_ok = False+    while _t.time() < _deadline:+        if _probe_addin(timeout=1.5):+            addin_ok = True+            break         try:-            handle_fusion_kill(fusion_info, {})+            _lic = find_licensing_dialog()         except Exception:-            pass-        _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-    # auto-reclaim above couldn't free it - e.g. the seat holder isn't a reachable AD)-    # + first-run SIGN-IN. Never auto-close these (a WM_CLOSE would kill the window).-    _needs_user = [d for d in launch_dialogs-                   if d.get("decision") or d.get("category") == "signin_required"]-    if _needs_user:-        _signin = any(d.get("category") == "signin_required" for d in _needs_user)-        _decision = any(d.get("decision") for d in _needs_user)-        notify_payload = ({"title": "Fusion needs your Autodesk sign-in",-                           "body": "Fusion is at the sign-in screen. Complete it (or I will drive it) "-                                   "so I can keep going.", "level": "warning"} if _signin else-                          {"title": "Fusion launch needs a decision",-                           "body": "A licensing dialog needs your choice.", "level": "warning"})-        # A seat/licensing DECISION is a TRUE WALL (must NOT auto-pick - it can-        # suspend or shut down Fusion on the user's OTHER machine, risking its-        # unsaved work). As of AD 1.9.84 we fire notify_user OURSELVES via the-        # direct API, cross-AD (reach_user) so the toast lands on whichever machine-        # the user is at - this bridge may be running unattended on a VM. Sign-in-        # is deliberately NOT auto-notified: automate it first (see hint); the-        # payload is the last-resort escalation only.-        notified = None-        if _decision and ad_client.available():-            notified = ad_client.notify(reach_user=True, **notify_payload)-        return {-            "success": True,-            "needsUserDecision": True,-            "needsSignin": _signin,-            "setupDialogs": launch_dialogs,-            "resolvedPath": fusion_info.get("exe_path", ""),-            "notifyUser": notify_payload,-            "notifyDelivered": bool(notified and (notified.get("action") == "displayed"-                                                  or notified.get("status") == "ok")),-            "_hint": (("First-run Autodesk SIGN-IN required. AUTOMATE IT FIRST, don't punt to the user. "-                       "PROVEN sequence (2026-07-05): (1) if the user's Autodesk web session is already "-                       "warm, clear any STALE 'signin.autodesk.com' tabs first - a dead tab holds an "-                       "EXPIRED code and is the #1 cause of 'Sign-in request expired'. (2) desktop_ui_"-                       "click 'Retry browser sign-in' (or 'Sign In') on the Fusion window to mint ONE "-                       "fresh request. (3) On the 'You're signed in' page, click 'Open Product' with "-                       "desktop_click (SendInput = isTrusted) NOT desktop_ui_click (UIA Invoke does NOT "-                       "fire the protocol handler). (4) An in-page 'open Autodesk Identity Manager?' "-                       "overlay appears (NOT an owned popup - screenshot catches it, popup-capture "-                       "won't): tick 'Always allow' then click 'Open', both via desktop_click, FAST "-                       "before the code expires. (5) poll fusion_readiness - the window flips 'Signing "-                       "in' -> 'Home'. nbrowser (CDP) can drive steps 3-4 headlessly IF it's on the "-                       "SAME browser window Fusion opened. NEVER auto-enter the password/2FA. If you "-                       "genuinely must ask the user, notify_user works directly now (AD 1.9.84); when "-                       "you're on a VM and the user is elsewhere, add `target` from the `targets` verb "-                       "(or 'all') so the toast lands where they are. " if _signin else "")-                      + (("A licensing/seat DECISION dialog is up - do NOT auto-pick. I already fired "-                          "notify_user cross-AD (notifyDelivered=%s); ASK the user which option, then "-                          "click it. " % (bool(notified and (notified.get("action") == "displayed"-                                                             or notified.get("status") == "ok"))))-                         if _decision else "")-                      + " ".join(d["resolution"] for d in _needs_user)),-        }--    _RELAY_BUDGET = 50  # keep total handler time under AD's ~60s relay cap-    _remaining = max(6, int(_RELAY_BUDGET - (_t.time() - _start)))-    addin_ok = wait_for_addin(timeout=_remaining, dismiss_dialogs=True)+            _lic = None+        if _lic and _seat_clicks < 5:+            if _resolve_seat_via_uia(_lic):+                _seat_clicks += 1+        else:+            try:+                dismiss_recovery_dialog()+            except Exception:+                pass+        _t.sleep(2.5)      if not addin_ok:-        # Add-in not up within our budget. On a first launch this is EXPECTED-        # (still initializing), not a failure and not a retry-able timeout.+        # Add-in not up within our budget. On a first launch this is EXPECTED (still+        # initializing / signing in), not a failure and not a retry-able timeout.         return {             "success": True,             "stillLaunching": True,@@ -259,11 +237,13 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict:             "statusVerb": "fusion_readiness",             "recoveryFilesRelocated": reloc["moved"],             "resolvedPath": fusion_info.get("exe_path", ""),+            "seatDialogResolved": _seat_clicks > 0,             "_hint": (-                "Fusion is LAUNCHING but the add-in isn't up yet - a FIRST launch can take "-                "2-4 min (component downloads / updates / cloud sync). This is NOT a failure and "-                "NOT a timeout to retry: do NOT re-call fusion_start. POLL fusion_readiness every "-                "~10s until ready:true (or fusion_addin_status until it responds), then drive."+                ("Auto-resolved a seat/licensing dialog in the background (UIA, no foreground). "+                 if _seat_clicks else "")+                + "Fusion is LAUNCHING but the add-in isn't up yet - a FIRST launch / a fresh "+                "sign-in can take 2-4 min. This is NOT a failure and NOT a timeout to retry: do NOT "+                "re-call fusion_start. POLL fusion_readiness until ready:true, then drive."             ),         }