← Commit history

v1.6.37: readiness no longer false-ready on stuck signin + proactive foreground-lock

John Lauer ·d4b141898f ·3mo ago ·parent 6cbbcf9
6 files changed +97−23
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.36\ No newline at end of file+1.6.37\ 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.37 - 2026-07-06++- **`fusion_readiness` no longer lies `ready:true` for a stuck/dead Fusion** (John caught it). It only+  checked for the "Signing in" window when the add-in was silent - but a STALE add-in server from a+  just-killed instance keeps port 8774 answering, so `addin_ok` went True while the CURRENT Fusion sat+  stuck on "Signing in", and readiness reported ready. Now the Fusion-process window title is checked+  WHENEVER running (authoritative: "Signing in" == not ready), and `ready` additionally requires the+  add-in to actually RESPOND (a live main thread), not just the process to exist. Added a distinct+  "running but add-in not responding yet" hint so a half-launched Fusion is never mistaken for ready.+- **Proactive foreground-lock during launch = no more flicker.** The reactive minimize-guard alone+  left a visible flash (window appears, then shrinks). Now the launch raises the Windows+  foreground-lock timeout so Fusion's splash/main/seat windows are DENIED a foreground grab outright+  (taskbar flash, not a real steal), restored to the user's original value when the launch window ends.+ ## 1.6.36 - 2026-07-06  - **Codified the background-launch rules in the `fusion-bridge-dev` skill** (John's request): the
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.36",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.36/adom-bridge-fusion-v1.6.36.zip",-  "sha256": "d5e0a7eeb9bb7d0229137d77241bdc8c612f4288df75da661af8466480427ced",-  "size": 336248,+  "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,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-06T11:34:07Z"+  "released_at": "2026-07-06T11:44:57Z" }
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.36",+  "version": "1.6.37",   "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+56−10
@@ -341,6 +341,40 @@ def _fusion_pids() -> set:     return {int(p) for p in re.findall(r'"Fusion360\.exe","(\d+)"', out)}  +_SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000+_SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001+++def _get_fg_lock_timeout():+    """Current SystemParametersInfo foreground-lock timeout (ms), or None."""+    try:+        user32 = ctypes.windll.user32+        val = ctypes.wintypes.DWORD(0)+        user32.SystemParametersInfoW(_SPI_GETFOREGROUNDLOCKTIMEOUT, 0, ctypes.byref(val), 0)+        return int(val.value)+    except Exception:+        return None+++def _set_fg_lock_timeout(ms) -> bool:+    """Set the foreground-lock timeout. A NONZERO value makes Windows DENY foreground+    changes from processes that don't own the foreground / have no recent user input -+    they get a taskbar flash instead of an actual foreground grab. We raise it right+    before launching Fusion so its splash/main/seat windows can't STEAL focus in the+    first place (proactive), rather than relying only on the reactive minimize-guard+    (which leaves a visible flash as the window appears-then-shrinks). Restored to the+    user's original value when the guard's launch window ends. Best-effort."""+    try:+        user32 = ctypes.windll.user32+        user32.SystemParametersInfoW.argtypes = [+            ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint]+        user32.SystemParametersInfoW(+            _SPI_SETFOREGROUNDLOCKTIMEOUT, 0, ctypes.c_void_p(int(ms)), 0)+        return True+    except Exception:+        return False++ def _popen_fusion_background(exe_path: str):     """Launch the Fusion launcher MINIMIZED + NOT ACTIVATED, console-less."""     cnw = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0@@ -361,7 +395,7 @@ def _popen_fusion_background(exe_path: str):     )  -def _focus_guard(seconds: float = 150.0):+def _focus_guard(seconds: float = 150.0, restore_fg_lock=None):     """For `seconds`, demote any Autodesk-FAMILY window that steals the foreground.      SURGICAL: only acts when the CURRENT foreground window belongs to an Autodesk@@ -405,12 +439,18 @@ def _focus_guard(seconds: float = 150.0):         except Exception:             pass         time.sleep(0.1)+    # Launch window over: restore the user's original foreground-lock so normal apps+    # can foreground again (we only wanted the lock DURING Fusion's noisy startup).+    if restore_fg_lock is not None:+        _set_fg_lock_timeout(restore_fg_lock)  -def _start_focus_guard(seconds: float = 120.0):+def _start_focus_guard(seconds: float = 150.0, restore_fg_lock=None):     """Fire-and-forget the focus guard on a daemon thread. Never raises."""     try:-        threading.Thread(target=_focus_guard, args=(seconds,), daemon=True).start()+        threading.Thread(+            target=_focus_guard, args=(seconds, restore_fg_lock), daemon=True+        ).start()     except Exception:         pass @@ -444,14 +484,20 @@ def ensure_fusion_running(fusion_info: dict, wait_addin: bool = False) -> dict |         return {"success": False, "error": "Fusion 360 executable not found"}      try:-        # Launch MINIMIZED + NOT ACTIVATED so Fusion never yanks focus off the-        # user's work (see the background-launch block above). Explicit DEVNULL on-        # all three std streams + CREATE_NO_WINDOW: the bridge is itself a detached,-        # console-less child of AD, so inherited handles are invalid.+        # PROACTIVE: raise the foreground-lock timeout so Windows DENIES Fusion's+        # splash/main/seat windows the ability to steal focus in the first place (they+        # get a taskbar flash, not a real foreground grab). This is what stops the+        # visible FLICKER the reactive guard alone leaves. Restored by the guard at the+        # end of the launch window.+        _orig_fg_lock = _get_fg_lock_timeout()+        _set_fg_lock_timeout(300000)+        # Launch MINIMIZED + NOT ACTIVATED (STARTUPINFO). DEVNULL on all three std+        # streams + CREATE_NO_WINDOW: the bridge is a detached, console-less child of+        # AD, so inherited handles are invalid.         _popen_fusion_background(exe_path)-        # Guard the noisy startup window: Fusion re-foregrounds itself several times-        # while its splash/main window come up, so STARTUPINFO alone isn't enough.-        _start_focus_guard(seconds=120.0)+        # Reactive backstop: minimize any family window that still slips to foreground,+        # and restore the user's original foreground-lock when the launch window ends.+        _start_focus_guard(seconds=150.0, restore_fg_lock=_orig_fg_lock)     except Exception as e:         return {"success": False, "error": f"Failed to launch Fusion 360: {e}"} 
server.py+20−6
@@ -828,12 +828,18 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     # ("Signing in - Autodesk Fusion" / "Welcome to Fusion") blocks the main thread,     # so the add-in never serves and every modeling verb hangs. The old code reported     # ready:True here purely on installed+running, which was a lie during sign-in.-    # Detect it from the FUSION-PROCESS window title/enabled state - NOT-    # classify_launch_dialogs, which scans ALL top-level windows and false-matches a-    # stray Edge "Sign in - Autodesk" BROWSER tab (caught live 2026-07-05). Only-    # considered when the add-in is NOT responding (a responding add-in => signed in).+    # Detect it from the FUSION-PROCESS window title - NOT classify_launch_dialogs,+    # which scans ALL top-level windows and false-matches a stray Edge "Sign in -+    # Autodesk" BROWSER tab (caught 2026-07-05).+    #+    # ⚠️ CHECK IT WHENEVER RUNNING, not only when the add-in is silent (fix 2026-07-06,+    # John caught it lying): a STALE add-in HTTP server from a just-killed instance can+    # still hold port 8774 and answer, so `addin_ok` goes True while the CURRENT Fusion+    # sits stuck on "Signing in". Gating the sign-in check on `not addin_ok` then let+    # readiness report ready:true for a dead, stuck-signing-in window. The Fusion-process+    # window title is authoritative - if it says "Signing in", we are NOT ready, period.     needs_signin = False-    if running and not addin_ok:+    if running:         try:             finfo = get_fusion_window_info() or {}             _t = (finfo.get("title") or "").lower()@@ -841,7 +847,10 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:                             or ("sign in - autodesk" in _t and "fusion" in _t))         except Exception:             needs_signin = False-    ready = installed and running and not needs_signin+    # "Ready to drive" requires the add-in to actually RESPOND (not just the process to+    # exist) AND no sign-in modal. A responding add-in proves Fusion's main thread is+    # 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      # 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@@ -880,6 +889,11 @@ 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:+        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.")     elif installed:         hint = "Fusion is INSTALLED but not running - call fusion_start (blocks until the add-in is ready), then retry."     else: