← Commit history

Upload 1 file(s)

John Lauer ·ce95a1b0fb ·2mo ago ·parent 89641e4
1 file changed +134−3
server.py+134−3
@@ -1367,25 +1367,40 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     elif ready and stale_info.get("addinStale"):         hint = stale_info["_staleHint"]     elif ready:+        # AUTO-UPDATE, checked on EVERY launch and applied SILENTLY (John, 2026-07-22: "i just+        # always want the latest fusion... make this generally invisible to me"). Fusion nags+        # constantly; we click it in the BACKGROUND so the user never sees it. Sticky opt-out.+        _upd = _apply_fusion_update_silently()         # APS is checked on EVERY launch now (John, 2026-07-22: "why aren't those just driven from         # 1 login?"). Fusion sign-in and APS are both Autodesk logins; the user should authenticate         # ONCE. They cannot share a token (Autodesk DPAPI-encrypts Fusion's store), but they CAN         # share the warm browser SSO session - so APS consent is silent right after a Fusion login.         _aps = _aps_quick_state()+        _updmsg = ""+        if _upd.get("applied"):+            _updmsg = (" AUTO-UPDATE: Fusion was offering an update and I clicked %r for the user "+                       "IN THE BACKGROUND - it downloads now and the new build comes up on the next "+                       "launch. TELL THE USER it is handled; they never need to click the nag. "+                       "They can opt out with fusion_set_auto_update {enabled:false} (sticky)."+                       % _upd.get("button"))+        elif not _auto_update_enabled():+            _updmsg = (" AUTO-UPDATE is OFF (the user opted out, sticky) - do NOT click Fusion's "+                       "update prompts for them.")         if _aps.get("signedIn"):             hint = ("READY - drive Fusion (fusion_open_lbr / build_library_3d / etc.). APS cloud "-                    "search is signed in too, so use fusion_aps_search for ~2s cloud lookups.")+                    "search is signed in too, so use fusion_aps_search for ~2s cloud lookups."+                    + _updmsg)         elif _aps.get("configured"):             hint = ("READY to drive Fusion, but APS CLOUD SEARCH IS NOT SIGNED IN - cloud file "                     "search will be slow/blocked. DO IT NOW, do not make the user log in a second "                     "time: call fusion_aps_signin RIGHT AFTER a Fusion sign-in, while the browser "                     "profile still has a warm Autodesk SSO session - it consents silently (no "-                    "password, no 2FA). Then fusion_aps_search works.")+                    "password, no 2FA). Then fusion_aps_search works." + _updmsg)         else:             hint = ("READY to drive Fusion. APS cloud search is NOT SET UP (no client ID) - cloud "                     "search is the slow path without it. Offer to set it up: fusion_aps_set_client_id "                     "then fusion_aps_signin (silent while the browser SSO session is warm). APS is "-                    "how cloud search gets to ~2 seconds.")+                    "how cloud search gets to ~2 seconds." + _updmsg)     elif installed and running and licensing:         hint = ("Fusion is blocked by a SEAT/LICENSING dialog ('Active Sessions Exceeded' / 'Suspend "                 "Remote Session') - detected DETERMINISTICALLY as an OWNED POPUP of the main Fusion "@@ -1685,6 +1700,7 @@ COMMAND_HANDLERS = {     "describe": lambda fi, args: describe.handle_describe(args),     "readiness": _handle_fusion_readiness,     "set_preference": _handle_set_preference,+    "set_auto_update": _handle_set_auto_update,     "get_preferences": _handle_get_preferences,     "install_fusion": _handle_install_fusion,     "open_design": handle_open_design,@@ -3677,6 +3693,121 @@ def _ad_call(verb: str, args: dict, timeout: int = 60) -> dict:     return {"_adCallError": "; ".join(why)}  +# ── FUSION AUTO-UPDATE (John, 2026-07-22) ────────────────────────────────────────────────────+# "fusion ships updates non-stop and its annoying to click this. i just always want the latest+# fusion... just make this generally invisible to me and do that by default for all other adom+# users. but let them tell you they don't want you doing that and you make that a sticky setting."+#+# So: ON BY DEFAULT for everyone, applied silently in the BACKGROUND, opt-out is sticky per user.+# Fusion's own updater downloads in the background and swaps the build on the next launch, so all+# we have to do is stop making the human click "Update Now" on a nag panel.+def _bridge_prefs_path():+    import pathlib+    d = pathlib.Path(os.path.expanduser("~")) / ".adom" / "fusion-bridge"+    d.mkdir(parents=True, exist_ok=True)+    return d / "prefs.json"+++def _get_bridge_pref(key: str, default=None):+    try:+        import json as _j+        p = _bridge_prefs_path()+        if p.exists():+            v = (_j.loads(p.read_text()) or {})+            return v.get(key, default)+    except Exception:+        pass+    return default+++def _set_bridge_pref(key: str, value) -> bool:+    try:+        import json as _j+        p = _bridge_prefs_path()+        cur = {}+        if p.exists():+            try: cur = _j.loads(p.read_text()) or {}+            except Exception: cur = {}+        cur[key] = value+        p.write_text(_j.dumps(cur, indent=2))+        return True+    except Exception:+        return False+++def _auto_update_enabled() -> bool:+    """Default TRUE. Only a user who explicitly opted out gets False, and that sticks."""+    return bool(_get_bridge_pref("autoUpdateFusion", True))+++# Buttons Fusion puts on its update nag / Job Status panel, best-first.+_UPDATE_BUTTONS = ("Update Now", "Update now", "Install Now", "Restart Now")+++def _find_update_offer(hwnd=None) -> dict:+    """Is Fusion offering an update right now? Detected via UIA (background, no foreground).+    Returns {offered, button, hwnd, detail} - never raises."""+    try:+        hw = hwnd or _fusion_window_hwnd()+        if not hw:+            return {"offered": False}+        for label in _UPDATE_BUTTONS:+            r = _unwrap(_ad_call("desktop_find_control", {"hwnd": int(hw), "name": label}, timeout=25))+            best = r.get("best") or {}+            if best.get("name") and best.get("invokable"):+                return {"offered": True, "button": best.get("name"), "hwnd": hw,+                        "detail": "Fusion is offering an update ('%s' is on screen)" % best.get("name")}+        return {"offered": False, "hwnd": hw}+    except Exception:+        return {"offered": False}+++def _apply_fusion_update_silently(hwnd=None) -> dict:+    """Click Fusion's update button IN THE BACKGROUND so the download starts and the new build+    is applied on the next launch. No foreground, no caption - the user asked for this to be+    invisible. Returns {applied, button, why}."""+    if not _auto_update_enabled():+        return {"applied": False, "why": "user opted out (sticky pref autoUpdateFusion=false)"}+    offer = _find_update_offer(hwnd)+    if not offer.get("offered"):+        return {"applied": False, "why": "no update offered"}+    r = _unwrap(_ad_call("desktop_ui_click",+                         {"hwnd": int(offer["hwnd"]), "name": offer["button"]}, timeout=30))+    ok = bool(r.get("invoked") or r.get("success"))+    if ok:+        print("[Fusion Bridge] auto-update: clicked %r in the background" % offer["button"])+    return {"applied": ok, "button": offer["button"],+            "why": ("clicked %r in the background; Fusion downloads now and swaps on next launch"+                    % offer["button"]) if ok else "found the button but the UIA invoke did not take"}+++def _handle_set_auto_update(fusion_info: dict, args: dict) -> dict:+    """Turn Fusion auto-updating on/off. STICKY - remembered for this user forever."""+    if "enabled" not in (args or {}):+        cur = _auto_update_enabled()+        return {"success": True, "autoUpdateFusion": cur,+                "_hint": ("Fusion auto-update is currently %s. It is ON BY DEFAULT: the bridge "+                          "clicks Fusion's 'Update Now' for the user in the BACKGROUND so they "+                          "always run the latest build and never see the nag. To opt out: "+                          "fusion_set_auto_update {\"enabled\": false} - that choice is STICKY."+                          % ("ON" if cur else "OFF")),+                "statusVerb": "fusion_readiness"}+    enabled = bool(args.get("enabled"))+    _set_bridge_pref("autoUpdateFusion", enabled)+    return {"success": True, "autoUpdateFusion": enabled,+            "narrate": ("I'll keep Fusion updated automatically in the background."+                        if enabled else+                        "I'll stop auto-updating Fusion. You'll see Autodesk's own update prompts."),+            "_hint": ("Saved and sticky (~/.adom/fusion-bridge/prefs.json). %s"+                      % ("Auto-update ON: the bridge silently clicks 'Update Now' whenever Fusion "+                         "offers one, so the user always runs the newest build. Tell them it is "+                         "handled and they never need to click it."+                         if enabled else+                         "Auto-update OFF: the bridge will NOT touch update prompts; the user "+                         "handles Autodesk's nag themselves. Do not override this.")),+            "statusVerb": "fusion_readiness"}++ def _fusion_signin_cfg_path():     import pathlib     d = pathlib.Path(os.path.expanduser("~")) / ".adom" / "fusion-signin"