← Commit history

v1.6.31: readiness updating:true (auto-update detect) + pref verbs retry-once (fresh-VM battery hardening)

John Lauer ·667f9f28da ·3mo ago ·parent bba5579
6 files changed +90−11
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.30\ No newline at end of file+1.6.31\ 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.31 - 2026-07-05++- **`fusion_readiness` now reports a distinct `updating:true` status when Fusion is applying an+  auto-update.** New `fusion_update_in_progress()` detects the tell-tale TWO+ webdeploy `production`+  builds coexisting; while that applies, Fusion crash-restarts itself every ~30-60s, so verbs+  intermittently saw a bare "not running" that read as a crash. Readiness now flags `updating:true`+  with a hint to poll/retry (it stabilizes once the update finishes) rather than treat the flap as+  fatal. Both hardening items came straight out of the fresh-VM battery.+- **Preference verbs (`fusion_set_preference`/`fusion_get_preferences`) retry once on a transient+  add-in connection error.** The first call can hit the add-in mid doc-transition (e.g. right after+  opening a Library, when the pref script must create a Design) and come back "not running" though+  Fusion is up; a ~1.5s retry clears it (seen live on the battery - failed first call, passed on+  retry). A genuinely-down Fusion just costs one extra 1.5s attempt.+ ## 1.6.30 - 2026-07-05  - **`fusion_install_fusion` and `fusion_start` now live-detect too** (completing the 1.6.29 fix). Both
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.30",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.30/adom-bridge-fusion-v1.6.30.zip",-  "sha256": "18af4245248fb8803c767164ba74fce5b8c25c54114e0661f7da44982850578e",-  "size": 328197,+  "version": "1.6.31",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.31/adom-bridge-fusion-v1.6.31.zip",+  "sha256": "f40e26b5e42760dbec3bad48330606aa69884a67015513ffb546afc97130701b",+  "size": 329710,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-05T20:44:46Z"+  "released_at": "2026-07-05T21:35:14Z" }
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.30",+  "version": "1.6.31",   "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+24
@@ -110,6 +110,30 @@ def _find_fusion_launcher() -> Path | None:     return candidates[0][2]  +def fusion_update_in_progress() -> bool:+    """Best-effort: is Fusion mid AUTO-UPDATE? Signalled by TWO+ production build+    dirs (each with a FusionLauncher.exe) coexisting under webdeploy - the current+    build plus the new one Fusion's streamer is materializing. While that applies,+    Fusion crash-restarts ITSELF every ~30-60s, so verbs intermittently see 'not+    running' (observed live on a fresh VM 2026-07-05: hashes 441fa88... + 6a0c961...).+    Callers surface a distinct non-fatal `updating` status instead of a fatal error so+    a driving AI waits/retries rather than treating the flap as a crash. Never raises."""+    hashes = set()+    for base in _WEBDEPLOY_BASES:+        try:+            if not base.exists():+                continue+            for d in base.iterdir():+                try:+                    if d.is_dir() and (d / "FusionLauncher.exe").exists():+                        hashes.add(d.name)+                except OSError:+                    continue+        except OSError:+            continue+    return len(hashes) > 1++ def _find_addins_dir() -> Path | None:     """Find the Fusion 360 add-ins directory."""     for candidate in ADDINS_DIR_CANDIDATES:
server.py+45−4
@@ -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+from fusion_detect import detect_fusion, ensure_fusion_running, wait_for_addin, _is_fusion_running, fusion_update_in_progress 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@@ -802,6 +802,18 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:             needs_signin = False     ready = installed and running 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+    # bridge failure - surface it as a distinct non-fatal `updating` flag so a driving AI+    # waits/retries instead of giving up. Only check when installed but not currently+    # ready (avoid noise when Fusion is happily drivable).+    updating = False+    if installed and not ready:+        try:+            updating = fusion_update_in_progress()+        except Exception:+            updating = False+     # When Fusion is up + signed in, also check the add-in isn't STALE (issue #55) so     # a fresh readiness call surfaces the mismatch instead of a later verb failing     # silently. Reuse the add-in status already fetched above (no second probe).@@ -809,7 +821,13 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     if addin_ok:         stale_info = _addin_staleness(addin_status.get("version")) -    if needs_signin:+    if updating and not ready:+        hint = ("Fusion is applying an AUTO-UPDATE (multiple webdeploy builds present) and "+                "RESTARTS ITSELF every ~30-60s - this is NOT a crash or a bridge failure. Poll "+                "fusion_readiness; it stabilizes to ready:true once the update finishes (can take "+                "10-30 min). Meanwhile, wrap modeling/export verbs in a short retry/catch-loop to "+                "ride the up-windows rather than treating an intermittent 'not running' as fatal.")+    elif needs_signin:         hint = ("Fusion is RUNNING but stuck at the first-run Autodesk SIGN-IN (main UI blocked, "                 "add-in cannot serve) - NOT ready. AUTOMATE the sign-in (see the fusion-onboarding "                 "skill for the proven sequence: clear stale signin.autodesk.com tabs, 'Retry browser "@@ -841,6 +859,9 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:         **stale_info,         "_hint": hint,     }+    if updating:+        result["updating"] = True+        result["statusVerb"] = "fusion_readiness"     if needs_signin:         result["needsSignin"] = True         result["statusVerb"] = "fusion_readiness"@@ -968,6 +989,26 @@ def _shape_pref_result(res: dict, wrote: bool) -> dict:     return out  +def _run_pref_script(script: str, timeout: int) -> dict:+    """Proxy a preferences script to the add-in, retrying ONCE on a transient add-in+    connection error. The first pref call can hit the add-in mid doc-transition (e.g.+    right after opening a Library, when run_modeling_script must create a Design) and+    come back 'not running'/'not responding' even though Fusion is up - seen live on+    the fresh-VM battery, where a ~1.5s retry cleared it. A genuinely-down Fusion just+    costs one extra 1.5s try."""+    res = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=timeout)+    if res and res.get("success"):+        return res+    err = ((res or {}).get("error") or "").lower()+    if any(s in err for s in ("not running", "not responding", "add-in",+                              "connection", "timed out", "refused")):+        _time.sleep(1.5)+        res2 = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=timeout)+        if res2 is not None:+            return res2+    return res++ def _handle_set_preference(fusion_info: dict, args: dict) -> dict:     reqs = {k: v for k, v in (args or {}).items() if k not in ("settle",)}     if not reqs:@@ -975,13 +1016,13 @@ def _handle_set_preference(fusion_info: dict, args: dict) -> dict:                 "_hint": "Pass e.g. {\"theme\":\"dark\"} or {\"invertScrollZoom\":true,"                          "\"orbitScheme\":\"solidworks\"}. Call fusion_get_preferences to see current values + keys."}     script = _PREF_SCRIPT.replace("__REQS__", json.dumps(json.dumps(reqs)))-    res = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=30)+    res = _run_pref_script(script, timeout=30)     return _shape_pref_result(res, wrote=True)   def _handle_get_preferences(fusion_info: dict, args: dict) -> dict:     script = _PREF_SCRIPT.replace("__REQS__", json.dumps(json.dumps({})))-    res = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=20)+    res = _run_pref_script(script, timeout=20)     return _shape_pref_result(res, wrote=False)