app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
Update 10 file(s)
10 files changed
+98−30
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.79+1.9.80
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.79",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.91/adom-bridge-fusion-v1.9.79.zip",- "sha256": "3bb33e40f9a332140a43ad008d3e2cbc76b918d669db7efc07b906e6d1c3942f",- "size": 418806,+ "version": "1.9.80",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.92/adom-bridge-fusion-v1.9.80.zip",+ "sha256": "976127b414acfdb036046e11b5b0488b0e9eec5d2c374d7fd0ca258d3dd24ef7",+ "size": 420002, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.79",+ "version": "1.9.80", "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/fusion-bridge", "author": "Adom Inc.",
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.91"+version = "1.9.92" edition = "2021" [dependencies]
fusion_detect.py+51−1@@ -345,11 +345,61 @@ def _has_fusion_process() -> bool: creationflags=creationflags, ) # tasklist returns "INFO: No tasks..." when no match- return "Fusion360.exe" in output+ if "Fusion360.exe" not in output:+ return False+ # ⚠️ TASKLIST LISTS THE DEAD (root-caused live on ConfRoomROG, 2026-08-19).+ # A Fusion360.exe whose parent died without reaping it stays in the process+ # table as a ZOMBIE: tasklist AND WMI still show it (1GB working set, 1+ # thread), but HasExited is True and taskkill answers "There is no running+ # instance of the task". Trusting tasklist alone made this bridge report+ # running:true forever, which meant fusion_start returned "already running"+ # and NEVER LAUNCHED, every kill failed, and readiness waited on an add-in+ # that could never come. The box was unusable for hours with no error+ # anywhere. So confirm at least one PID is GENUINELY alive.+ pids = []+ for line in output.splitlines():+ parts = [p.strip('"') for p in line.split('","')]+ if len(parts) > 1:+ try:+ pids.append(int(parts[1]))+ except Exception:+ pass+ if not pids:+ return "Fusion360.exe" in output # cannot parse -> old behaviour+ return any(_pid_alive(p) for p in pids) except Exception: return False +_STILL_ACTIVE = 259+_PROCESS_QUERY_LIMITED_INFORMATION = 0x1000+++def _pid_alive(pid: int) -> bool:+ """Is this PID a LIVE process, or a zombie the process table has not let go of?++ GetExitCodeProcess is the ground truth: a zombie returns its real exit code,+ a live process returns STILL_ACTIVE (259). If the handle cannot be opened we+ answer True - an access-denied process is running, just not ours to inspect,+ and guessing "dead" there would make the bridge launch a second Fusion.+ """+ try:+ k32 = ctypes.WinDLL("kernel32", use_last_error=True)+ h = k32.OpenProcess(_PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))+ if not h:+ return True+ try:+ code = ctypes.wintypes.DWORD()+ ok = k32.GetExitCodeProcess(h, ctypes.byref(code))+ if not ok:+ return True+ return code.value == _STILL_ACTIVE+ finally:+ k32.CloseHandle(h)+ except Exception:+ return True++ def _find_windows_by_title(substring: str) -> list: """Find all visible windows whose title contains the given substring.""" results = []
handlers/close_fusion.py+20−11@@ -116,7 +116,7 @@ def _force_kill_fusion() -> int: # Escalate on survivors: per-PID, whole tree. try: time.sleep(1.0)- for pid in _fusion_pids():+ for pid in _live_fusion_pids(): try: r = subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)@@ -129,16 +129,25 @@ def _force_kill_fusion() -> int: return killed +def _live_fusion_pids() -> list:+ """Surviving pids that are GENUINELY alive - a zombie is not something to kill."""+ try:+ from fusion_detect import _pid_alive+ except Exception:+ return _fusion_pids()+ return [p for p in _fusion_pids() if _pid_alive(p)]++ def _has_fusion_process() -> bool:- """Check if any Fusion process is still running."""+ """Is a GENUINELY LIVE Fusion process still running?++ Deliberately not a raw tasklist check: a zombie Fusion360.exe (exited, parent+ gone, never reaped) still appears there, and counting it made a successful kill+ report "Some Fusion processes survived force-kill" forever - a failure nobody+ could act on, because there was nothing left to kill (ConfRoomROG, 2026-08-19).+ """ try:- output = subprocess.check_output(- ["tasklist", "/FI", "IMAGENAME eq Fusion360.exe", "/FO", "CSV", "/NH"],- stderr=subprocess.DEVNULL,- text=True,- timeout=5,- )- return "Fusion360.exe" in output+ return bool(_live_fusion_pids()) except Exception: return False @@ -250,7 +259,7 @@ def handle_close_fusion(fusion_info: dict, args: dict) -> dict: "hand this to the user as a Task Manager chore: check whether the survivor " "is a crash-report host (fusion_readiness reports fusionCrashed) or is " "pinned by a modal, and re-call fusion_kill after clearing that."),- "survivingPids": _fusion_pids(),+ "survivingPids": _live_fusion_pids(), } return { "success": True,@@ -394,7 +403,7 @@ def handle_fusion_kill(fusion_info: dict, args: dict) -> dict: "this to the user as a Task Manager chore: check whether the survivor is a " "crash-report host (fusion_readiness reports fusionCrashed) or is pinned by a " "modal, and re-call fusion_kill after clearing that."),- "survivingPids": _fusion_pids(),+ "survivingPids": _live_fusion_pids(), } return { "success": True,
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.91"+DASH_VERSION="1.9.92" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.91",+ "version": "1.9.92", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+11−8@@ -1,16 +1,20 @@ { "slug": "fusion-bridge",- "version": "1.9.91",+ "version": "1.9.92", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {- "adom/adom-desktop": "*"+ "adom/adom-desktop": "^1.9.0" }, "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -70,7 +74,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -99,9 +106,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }+ ] }
timings.py+7−1@@ -153,8 +153,14 @@ def progress_block(phase, started, step_label, percent=None, start_kind=None, if not est0: percent = 0 elif not overdue:- percent = int(100 * elapsed / est0)+ # Runs 0 -> 80 across the estimate. It deliberately does NOT reach 100: the+ # estimate is a p50, so half of all runs are still going at this point.+ percent = int(80 * elapsed / est0) else:+ # Continues 80 -> 95 from exactly where the branch above ended. The two+ # formulas MUST meet at elapsed == est0, or percent walks BACKWARDS at the+ # crossover - which it did (91% then 84% on a live poll, 2026-08-19), breaking+ # the contract's monotonic rule with the very code meant to honour it. over = elapsed - est0 percent = int(80 + 15 * (over / (over + est0))) # 80 -> 95, never 100 percent = max(0, min(99 if est else 0, int(percent)))