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.
No dialog storms from a broken add-in deploy, and refuse incomplete payloads (issue 69) #5
Companion PR for issue 69, based on repo commit c11a96c2 (Publish 1.9.237). Three surgical changes, two files.
addin/AdomBridge/AdomBridge.py:
- run() shows the failure messageBox ONCE per Fusion session (_start_error_shown); repeats go to Application.log. Several installed copies each failing, plus lifecycle retries, turned one broken deploy into a dialog storm.
- stop() never raises a modal: Fusion re-fires the lifecycle on ordinary user actions, so a stop-time messageBox cost Colby one dialog PER ACTION today. run() already told them once; stop failures log.
- run() treats a port-8774 bind OSError as a healthy sibling copy serving (install() writes every AddIns location and current Fusion loads more than one; both loaded on Colby-ROG2), logs it, and stands down quietly. Without this, the fix for 69 trades the traceback dialog for a port-in-use dialog on every launch.
install_addin.py: 4. _check_relative_imports() preflight: every relative import in the payload must resolve to a file IN the payload, or install() refuses and leaves the installed add-in untouched. Replayed against 69's exact state: a payload missing commands/activity_palette.py is refused naming commands/init.py's import; the complete payload passes clean. This is the transactional-sync ask made cheap: the sync cannot deliver the half that references the missing half, and the mirror step cannot delete a hand-restored file to re-brick a machine.
py_compile clean on both files; the checker is unit-exercised offline (complete payload CLEAN, ablated payload refused with the named import). Not live-run inside Fusion; the run/stop changes are guard-path only. Base-drift caveat as always: if your working tree is ahead of c11a96c2, hand-lift rather than merge.
Diff Skip to comments (2)
@@ -1,114 +1,144 @@⋯ 14 unchanged lines ⋯ _custom_event = None _event_handler = None _http_thread = None+# One start-failure modal per Fusion session; repeats go to the log (#69).+_start_error_shown = False CUSTOM_EVENT_ID = "AdomBridgeCommandEvent" ⋯ 55 unchanged lines ⋯ # Start the HTTP server on a background daemon thread from . import http_server- _http_thread = http_server.start(CUSTOM_EVENT_ID, _app)+ try:+ _http_thread = http_server.start(CUSTOM_EVENT_ID, _app)+ except OSError:+ # Port 8774 already bound: a SIBLING copy of this add-in is+ # serving. install() writes every known AddIns location and newer+ # Fusion builds scan more than one of them (seen live on #69:+ # FusionAddins AND API/AddIns both loaded), so a second copy+ # binding the port is normal operation, not an error the user can+ # act on. Stand down quietly; the serving copy has the job.+ adsk.core.Application.log(+ "Adom Bridge add-in: port 8774 already served by a sibling copy; this copy is standing down")+ return # Log to the Text Commands palette (non-blocking) adsk.core.Application.log(f"Adom Bridge add-in started (port 8774)") except Exception:- # Only use messageBox for errors — worth blocking to show the user- if _ui:+ # One modal, total. The first failure is worth blocking to show the+ # user; repeats (Fusion can retry the lifecycle, and several installed+ # copies can each fail the same way) turn one broken deploy into a+ # dialog storm (#69). After the first box, log instead.+ global _start_error_shown+ if _ui and not _start_error_shown:+ _start_error_shown = True _ui.messageBox(f"Adom Bridge failed to start:\n{traceback.format_exc()}")+ else:+ try:+ adsk.core.Application.log(f"Adom Bridge failed to start:\n{traceback.format_exc()}")+ except Exception:+ pass def stop(context):⋯ 19 unchanged lines ⋯ _http_thread = None except Exception:- if _ui:- _ui.messageBox(f"Adom Bridge stop error:\n{traceback.format_exc()}")+ # NEVER a modal from stop. Fusion re-fires the lifecycle on ordinary+ # events, so when the add-in is broken a stop-time messageBox costs the+ # user one dialog PER ACTION they take (seen live 2026-08-28, #69).+ # run() already told them once; stop failures go to the log.+ try:+ adsk.core.Application.log(f"Adom Bridge stop error:\n{traceback.format_exc()}")+ except Exception:+ pass+@@ -1,152 +1,189 @@⋯ 77 unchanged lines ⋯ print(f" Removed dir: {rel}") +def _check_relative_imports(source: Path) -> list[str]:+ """Refuse an incomplete payload BEFORE it ships. #69's failure mode: a+ payload carried the new commands/__init__.py but not the+ activity_palette.py it imports, so the sync faithfully delivered the half+ that references the missing half (and, because the sync mirrors, deleted+ any hand-restored copy as an 'extra'). Every relative import in the+ payload must resolve inside the payload; anything else bricks Fusion+ startup with a raw traceback dialog."""+ import re+ problems = []+ for py in source.rglob("*.py"):+ try:+ text = py.read_text(encoding="utf-8", errors="replace")+ except OSError:+ continue+ for m in re.finditer(r"^\s*from\s+(\.+)([\w.]+)\s+import\s", text, re.M):+ dots, mod = m.group(1), m.group(2)+ base = py.parent+ for _ in range(len(dots) - 1):+ base = base.parent+ target = base.joinpath(*mod.split("."))+ if not (target.with_suffix(".py").exists() or (target / "__init__.py").exists()):+ problems.append(f"{py.relative_to(source)}: 'from {dots}{mod} import ...' resolves to no file in the payload")+ return problems++ def find_addins_dir() -> Path | None: """Find an existing Fusion AddIns directory, or return the first candidate.""" for candidate in TARGET_CANDIDATES:⋯ 20 unchanged lines ⋯ print(f"ERROR: Source add-in not found at {SOURCE_DIR}") sys.exit(1) + problems = _check_relative_imports(SOURCE_DIR)+ if problems:+ print("ERROR: refusing to install an incomplete add-in payload:")+ for p in problems:+ print(f" {p}")+ print("Syncing this would brick Fusion startup with a traceback dialog (#69), "+ "and the mirror step would delete any hand-restored copy of the missing file. "+ "The existing installed add-in is left untouched.")+ sys.exit(1)+ print(f"Source: {SOURCE_DIR}") installed_to = [] for addins_dir in TARGET_CANDIDATES:⋯ 40 unchanged lines ⋯ if __name__ == "__main__": main()+
Comments
Log in to comment.