← Commit history

v1.6.12: launch watches licensing/error dialogs in code (auto-dismiss benign, ask on seat-decisions)

John Lauer ·58280e5c66 ·3mo ago ·parent 5fcd63f
7 files changed +154−9
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.11\ No newline at end of file+1.6.12\ No newline at end of file
CHANGELOG.md+14
@@ -3,6 +3,20 @@ 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.12 - 2026-07-03++- **Launch now WATCHES for licensing/launch dialogs in CODE (not just a skill), found live on the+  ADOMBASELINE first-run test.** `_handle_launch` enumerates ALL top-level windows the instant the+  Fusion process is up and classifies the dialogs that block a first launch but are owned by the+  Autodesk FAMILY (AdskIdentityManager / FusionLauncher), so the Fusion-scoped classifier misses+  them: **"Active Sessions Exceeded"** + **"Suspend Remote Session"** (seat conflict) and **"Error+  Launching Streamed Application / missing or incomplete / re-install"** (an incomplete webdeploy+  folder). Benign launch-error acks are **auto-dismissed** (WM_CLOSE, background). A seat-conflict is+  a **DECISION** dialog: the bridge returns `needsUserDecision:true` + `setupDialogs[]` + a `_hint`+  that says fire a `notify_user` and ASK the user which option to take (the options+  suspend/shut-down Fusion on their OTHER machine) - it NEVER auto-picks. New helpers in+  `dialog_classify.py`: `classify_launch_dialogs`, `close_dialog_bg`, `DECISION_CATEGORIES`.+ ## 1.6.11 - 2026-07-03  - **Detect a system-wide (`--globalinstall`) Fusion, not just per-user (fixes a broken onboarding
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.11",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.11/adom-bridge-fusion-v1.6.11.zip",-  "sha256": "c9d494cbf710ac86be6bb9bd851fc2cba38ba088d8adf8a910995d50a350aa40",-  "size": 299827,+  "version": "1.6.12",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.12/adom-bridge-fusion-v1.6.12.zip",+  "sha256": "566613333a5ab685a6ec1ed51545cf8d5080ff9698a31571c4766af56b229dc0",+  "size": 302393,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-03T16:15:00Z"+  "released_at": "2026-07-03T17:00:00Z" }\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.11",+  "version": "1.6.12",   "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.",
handlers/dialog_classify.py+109
@@ -64,8 +64,43 @@ _RULES = [     (("what do you want to design", "what to design"),      "what_to_design",      "Fusion's start picker. fusion_send_key {\"key\":\"escape\"} to dismiss."),++    # ── Launch / licensing / setup dialogs (owned by AdskIdentityManager /+    #    FusionLauncher, so classify_launch_dialogs enumerates ALL top-level+    #    windows to catch them - get_fusion_window_info would miss them). ──++    # Autodesk SEAT conflict — a DECISION dialog. The options suspend / shut down+    # Fusion on the user's OTHER machine (risking its unsaved work), so NEVER+    # auto-pick: notify + ASK the user which option they want.+    (("active sessions exceeded", "more active sessions", "more sessions running than are allowed",+      "sessions running than are allowed"),+     "session_conflict_decision",+     "DECISION DIALOG - do NOT auto-pick. This Autodesk seat is already active on another machine; "+     "the options SUSPEND or SHUT DOWN Fusion THERE (risking that machine's unsaved work). fire a "+     "notify_user and ASK the user which option they want, then click it. (Suspend = pausable/"+     "resumable; Shut down = saves the other machine's work to a recovery file.)"),++    (("suspend remote session",),+     "session_suspend_confirm",+     "Confirmation of a chosen 'suspend the OTHER machine's Fusion'. Only click Continue if the user "+     "ALREADY chose suspend; otherwise Go Back and ASK. Never auto-confirm a seat action."),++    # webdeploy launch failure - an INCOMPLETE/stale production folder (missing+    # FusionLauncher.exe.ini). Benign to dismiss; means you launched the wrong hash.+    (("error launching streamed application", "missing or incomplete", "please re-install",+      "re-install the application"),+     "launch_error",+     "Launch error from an INCOMPLETE webdeploy folder (missing FusionLauncher.exe.ini). Safe to "+     "dismiss (OK/close) - it is only an acknowledgement. Launch the COMPLETE production folder (the "+     "hash dir that HAS FusionLauncher.exe.ini); detect_fusion now prefers it."), ] +# Categories that require a USER decision — the launch code must NOT auto-dismiss+# these; it surfaces them + fires a notify_user + asks.+DECISION_CATEGORIES = {"session_conflict_decision", "session_suspend_confirm"}+# Launch/setup categories enumerated across ALL top-level windows (not just Fusion).+_LAUNCH_CATEGORIES = DECISION_CATEGORIES | {"launch_error"}+  def _category_for_title(title: str):     t = (title or "").strip().lower()@@ -97,6 +132,80 @@ def _category_for_title(title: str):             "Identify it from the screenshot, then take the work-preserving action.")  +def _enumerate_all_top_level():+    """(hwnd, title) for every visible, titled top-level window.++    Unlike get_fusion_window_info (scoped to the Fusion process), this catches+    dialogs owned by the Autodesk FAMILY - AdskIdentityManager (sign-in / seat+    conflict) and FusionLauncher (streamed-app launch errors) - which are the+    ones that block a first launch. Best-effort; never raises.+    """+    import ctypes+    results = []+    user32 = ctypes.windll.user32 if hasattr(ctypes, "windll") else None+    if not user32:+        return results+    WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)++    def _cb(hwnd, _lparam):+        try:+            if user32.IsWindowVisible(hwnd):+                n = user32.GetWindowTextLengthW(hwnd)+                if n:+                    buf = ctypes.create_unicode_buffer(n + 1)+                    user32.GetWindowTextW(hwnd, buf, n + 1)+                    if buf.value.strip():+                        results.append((hwnd, buf.value))+        except Exception:+            pass+        return True++    try:+        user32.EnumWindows(WNDENUMPROC(_cb), 0)+    except Exception:+        pass+    return results+++def classify_launch_dialogs() -> list:+    """Classify LAUNCH / licensing / setup dialogs across ALL top-level windows.++    Each entry: {hwnd, title, category, resolution, decision}. `decision:true`+    means a USER choice is required (seat conflict) - the launch path must NOT+    auto-dismiss it; it notifies + asks. Benign `launch_error` dialogs (decision:+    false) can be closed. Empty list if none. Never raises.+    """+    out = []+    for hwnd, title in _enumerate_all_top_level():+        classified = _category_for_title(title)+        if not classified:+            continue+        category, resolution = classified+        if category not in _LAUNCH_CATEGORIES:+            continue+        out.append({+            "hwnd": hwnd,+            "title": title,+            "category": category,+            "resolution": resolution,+            "decision": category in DECISION_CATEGORIES,+        })+    return out+++def close_dialog_bg(hwnd) -> bool:+    """WM_CLOSE a window in the BACKGROUND (PostMessage = no focus steal). Used to+    auto-dismiss benign launch-error acks. Never raises; returns True if posted."""+    import ctypes+    user32 = ctypes.windll.user32 if hasattr(ctypes, "windll") else None+    if not user32 or not hwnd:+        return False+    try:+        return bool(user32.PostMessageW(ctypes.c_void_p(int(hwnd)), 0x0010, 0, 0))  # WM_CLOSE+    except Exception:+        return False++ def classify_blocking_dialogs() -> list:     """Return classified blocking dialogs currently open in Fusion. 
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-desktop-fusion-bridge",-  "version": "1.6.12",+  "version": "1.6.13",   "type": "app",   "description": "Drive Autodesk Fusion 360 from the cloud via Adom Desktop: 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 with browse/recent/versions, cloud download/upload and folder creation, and in-app parametric modeling. Never-charge: APS calls are capped to the free tier.",   "dependencies": {},
server.py+23−1
@@ -33,7 +33,7 @@ from handlers.fusion_ui import (     close_window,     get_fusion_window_info, )-from handlers.dialog_classify import classify_blocking_dialogs+from handlers.dialog_classify import classify_blocking_dialogs, classify_launch_dialogs, close_dialog_bg from install_addin import install as install_addin import aps import describe@@ -134,6 +134,28 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict:     if err:         return err +    # WATCH for launch/licensing dialogs the moment the process is up - IN CODE, so+    # a launch is never blind to them (the seat-conflict + streamed-app-error dialogs+    # are owned by AdskIdentityManager/FusionLauncher, invisible to the Fusion-scoped+    # classifier). Auto-dismiss BENIGN errors; a DECISION dialog (seat conflict) is+    # returned for the AI to notify_user + ASK - never auto-picked.+    launch_dialogs = classify_launch_dialogs()+    for _dlg in launch_dialogs:+        if not _dlg.get("decision"):+            close_dialog_bg(_dlg.get("hwnd"))  # benign ack (incomplete-webdeploy launch error)+    _decisions = [d for d in launch_dialogs if d.get("decision")]+    if _decisions:+        return {+            "success": True,+            "needsUserDecision": True,+            "setupDialogs": launch_dialogs,+            "resolvedPath": fusion_info.get("exe_path", ""),+            "_hint": ("A licensing/seat DECISION dialog is up during launch (e.g. 'Active Sessions "+                      "Exceeded'). Do NOT auto-pick - the options affect the user's OTHER machine. "+                      "Fire a notify_user, ASK the user which option they want, then click it with "+                      "desktop_ui_click. " + " ".join(d["resolution"] for d in _decisions)),+        }+     _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)