Skip to content

Latest commit

 

History

History
441 lines (350 loc) · 20 KB

File metadata and controls

441 lines (350 loc) · 20 KB

Debugging Power Tools

This guide covers debugging the Power Tools add-in from VS Code and from Zed. Both attach a Python debugger (debugpy) to the running Fusion process; they differ only in who starts the debug server.

For general setup, the repository layout, and tooling, see the Developer Guide.


Contents


The two debugging models

Fusion's Debug button in Scripts and Add-Ins is hardcoded to launch VS Code, and the ms-python extension injects debugpy for you. To use any other DAP client (Zed), you invert the model: the add-in starts a debugpy server in-process and the editor attaches.

VS Code Zed
How the server starts Fusion's Debug button injects debugpy The add-in starts an in-process debugpy server when the .debug marker is present
How you launch the add-in Debug button Run button (not Debug)
Editor role Attaches automatically You attach manually (F4)
Port 9000 (.vscode/launch.json) 5678 (.zed/debug.json, config.DEBUGGER_PORT)
Config file .vscode/launch.json (auto-generated by Fusion) .zed/debug.json (this repo)

Both config folders (.vscode/, .zed/) are git-ignored — they contain machine-specific absolute paths and the volatile Fusion webdeploy hash, so they are regenerated per machine.

Prerequisites

The in-process server needs debugpy installed into Fusion's bundled Python (not your .venv). It is a one-time step, repeated after each Fusion auto-update (the update rotates the webdeploy hash).

The helper scripts in schneik80/Zed_Debug automate this:

~/Source/Zed_Debug/scripts/setup-fusion-debug.sh /Users/schneik/Source/PowerTools

The script locates the current Fusion Python, bootstraps pip via ensurepip (Fusion ships without pip — trap #1), installs debugpy into Fusion's --user site (which survives Fusion upgrades), and rewrites the editor config's webdeploy paths.

Channel caveat — the checkout on ADSKMVG91G2F5W (MacBook Air M4, macOS 26.5.1). Both Fusion devices (ADSKMVG91G2F5W and g16win.local) have production and pre-production installed side by side — that machine carries five webdeploy trees (production, pre-production, develop, feature--1fx-globalnav, meta). So the debugger must be pointed at the build you are actually running; attaching to the wrong tree looks like breakpoints that never trip.

This checkout is configured against a pre-production build (see the paths in .zed/settings.json and .env), while setup-fusion-debug.sh scans webdeploy/**production**. Run the stock script and it silently repoints the config at production. The fix is update_debug_path.py, which takes the channel as an argument — run it after the setup script to put the paths back, or instead of it whenever debugpy is already installed.

This is a property of the checkout, not the device — the same choice exists on g16win.local, under %LOCALAPPDATA%\Autodesk\webdeploy\.

On ADSKMVG91G2F5W debugpy is already installed (~/Library/Python/3.14/lib/python/site-packages/debugpy), so no install is needed — only re-run the step after a Fusion update if imports or the listener stop working. That location is the user site for Python 3.14, and every channel checked bundles Python 3.14, so the one install serves production and pre-production alike; it needs redoing only if a Fusion update bumps the bundled Python minor version. On g16win.local, verify before assuming.

The PYTHONPATH shape is easy to get wrong. The API packages live at <channel>/<hash>/Autodesk Fusion.app/Contents/Api/Python/packages — note the Autodesk Fusion.app/Contents/ segment. Check the path in .env still resolves after any Fusion update; a rotated hash leaves it silently pointing at nothing:

ls -d "$(grep -o '/Users/.*packages' .env)" || echo "STALE — re-run setup"

A given hash is not unique to a channel — 8d5cf31c… currently appears under both production/ and pre-production/ — so the hash alone does not tell you which build you are attached to.

Verify at any time:

"<Fusion Python>" -c "import debugpy; print(debugpy.__version__)"

Pointing the config at a build (update_debug_path.py)

Two config values are absolute paths into the webdeploy tree, and both rotate out from under you on every Fusion update:

File Key Points at
.env PYTHONPATH …/<build>/Autodesk Fusion.app/Contents/Api/Python/packages
.zed/settings.json lsp.pyright.settings.python.pythonPath …/<build>/Autodesk Fusion.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python

Rather than hand-editing them, select a channel and let the script find the newest build that is actually complete:

python3 tools/debug/update_debug_path.py --list          # what is available
python3 tools/debug/update_debug_path.py pre-production  # repoint both configs
python3 tools/debug/update_debug_path.py                 # prompt for a channel
python3 tools/debug/update_debug_path.py develop --dry-run
pre-production
  -> 5c7e4bae1a38  Fusion 2705.1.4     deployed 2026-08-21 19:26

production
  -> 5b508d94493e  Fusion 2704.1.53    deployed 2026-08-05 09:50

It writes only those two keys — extraPaths, the Debugpy adapter path and anything else in .zed/settings.json are preserved — and skips .zed/settings.json entirely if the file is absent. Both files are git-ignored, so this is per-device state and never lands in a commit. Restart the add-in (Stop, then Run) and re-attach afterwards.

Why "latest" is not just the newest folder

Most hash directories under a channel are partial. webdeploy keeps incremental delta payloads beside the real installs, and they are hard to tell apart from outside: a partial directory still has an Autodesk Fusion.app with a Contents/. Only a few carry the full Api/Python subtree.

On 2026-09-03 pre-production held 8 hash directories and exactly one was complete — and both .env and .zed/settings.json were pointing at a partial one (8d5cf31c…), so the debug PYTHONPATH resolved to nothing. That fails silently: no import error, just unresolved adsk.* stubs and breakpoints that never trip.

So the script only considers a build a candidate when both artifacts exist — Api/Python/packages/adsk and the bundled interpreter — which are exactly the two things the configs need. Among those it takes the most recently deployed, by app-bundle mtime. It prints the Fusion version beside each candidate so you can sanity-check the ordering; the version is displayed only, never used to sort, because it is not readable on every layout.

Two related traps:

  • A hash is not unique to a channel. 8d5cf31c… currently appears under both production/ and pre-production/, so you cannot tell which build you are attached to from the hash alone. Pick the channel explicitly.
  • develop and feature--… channels are included if they have a complete build; meta is skipped, as it is webdeploy bookkeeping rather than a Fusion install.

The script is stdlib-only and probes for the webdeploy root instead of hardcoding it. The macOS layout is verified; the Windows candidates are unverified, and if none match it exits with what it tried rather than writing a guess into your config. If you hit that on g16win.local, print the real tree and add its relative paths to API_RELS / PYTHON_RELS.

Enabling the attach-debug server

The add-in starts the debugpy server only when debug mode is on, gated on the git-ignored .debug marker file in the repository root (config.WAIT_FOR_DEBUGGER = config.DEBUG):

# from the repository root
touch .debug     # enable debug mode (verbose logging + debug server)
rm .debug        # disable

The relevant config.py flags:

WAIT_FOR_DEBUGGER = DEBUG          # tied to the .debug marker
DEBUGGER_PORT = 5678               # the in-process server port
DEBUGGER_BLOCK_UNTIL_ATTACHED = False   # True → run() blocks until a debugger attaches

PowerTools.py starts the server in _maybe_start_debug_server(), called at the top of run(). It is non-fatal: if debugpy is missing or the listener cannot start, it logs a warning and the add-in still loads. It also uses in_process_debug_adapter=True (trap #2) and tolerates a double listen() (trap #4).

Set DEBUGGER_BLOCK_UNTIL_ATTACHED = True only when you need to debug startup itself — run() will then pause until an editor attaches.

Debugging in VS Code

VS Code uses Fusion's native integration; you do not need the .debug marker for this path.

  1. In Fusion: Utilities › Add-Ins (Shift+S) → Add-Ins tab → select PowerTools → click Debug. Fusion launches VS Code and generates .vscode/launch.json ("Python: Attach", port 9000) the first time.
  2. VS Code attaches automatically. Set breakpoints, then invoke a command from the Fusion toolbar; execution stops at the breakpoint.
  3. Use F10 / F11 to step, and the Variables / Watch panels to inspect.

To reload after an edit: Stop the add-in in Scripts and Add-Ins, then Debug again (Python module caching means a fresh attach is required).

Alternative — attach VS Code to the in-process server (port 5678). If you prefer the Zed-style model in VS Code, create the .debug marker, Run the add-in (not Debug), and add an attach configuration to .vscode/launch.json pointing at localhost:5678. This avoids the Debug-button contamination in trap #4.

Debugging in Zed

Open the repository in Zed (zed ~/Source/PowerTools). The repo already ships a .zed/debug.json with an Attach to Fusion (PowerTools) configuration (connect to 127.0.0.1:5678).

Per-session workflow:

  1. Ensure the .debug marker exists (touch .debug).
  2. In Fusion: Scripts and Add-Ins → select PowerToolsRun (not Debug — Debug forces VS Code).
  3. Verify the server is listening:
    lsof -nP -iTCP:5678 -sTCP:LISTEN
    Exactly one Fusion PID should appear.
  4. In Zed: press F4 → choose Attach to Fusion (PowerTools). The session connects; threads and the call stack populate.
  5. Set breakpoints, invoke commands from Fusion, step, and inspect.

Reload cycle after a code edit: in Scripts and Add-Ins click Stop, then Run, then re-attach in Zed (F4). Python module caching means you cannot simply continue — a fresh attach is required.

When Zed cannot fetch debugpy (offline or proxy)

Symptom. The debug session never starts and Zed's log (~/Library/Logs/Zed/Zed.log) shows:

ERROR [debugger_ui::debugger_panel] debugpy installation failed (could not fetch Debugpy's wheel)

Cause. Unlike VS Code, Zed's Debugpy adapter runs its own copy of debugpy (a local DAP bridge) that it installs by downloading a wheel from PyPI into ~/Library/Application Support/Zed/debug_adapters/Debugpy/. Zed recreates that folder on every attempt, so copying debugpy into it — or into the project .venv — does not help on its own. Behind a corporate proxy (or fully offline) the download fails and the session aborts.

Fix — point Zed at an existing debugpy so it skips the download. In .zed/settings.json, set dap.Debugpy.binary to the debugpy/adapter directory of any installed debugpy:

{
  "lsp": { "…": "" },
  "dap": {
    "Debugpy": {
      "binary": "/Users/you/Library/Python/3.14/lib/python/site-packages/debugpy/adapter"
    }
  }
}

Zed then runs <python3> <that path> … instead of fetching (confirmed in Zed's dap_store.rspython.rs: a set binary short-circuits the install — note this is a settings.json key, not a debugAdapterPath field in debug.json, which the code does not read). The adapter is pure Python and adds its own site-packages to sys.path, so it runs under whatever python3 Zed picks, regardless of the interpreter that built the debugpy you point at.

Use debugpy from Fusion's user site (the same one the in-process server uses, maintained by setup-fusion-debug.sh) or from the repo .venv. The setup-fusion-debug.sh / zed-enable-addin.sh scripts now emit this dap.Debugpy.binary line automatically.

In Zed's F4 picker choose the labeled Attach to Fusion (PowerTools) entry. The auto-generated per-file / project-root scenarios lack this config and will still try to download.

Port map

Purpose Host Port Where configured
VS Code (Fusion Debug button, injected debugpy) localhost 9000 .vscode/launch.json
Zed / manual attach (in-process debugpy) 127.0.0.1 5678 .zed/debug.json + config.DEBUGGER_PORT

The two ports are independent, so the two workflows do not collide — but do not launch via the Debug button and expect the in-process server in the same session (see trap #4).

The four setup traps

These are the non-obvious failures encountered wiring up the Zed workflow (documented in full in schneik80/Zed_Debug):

  1. Fusion's bundled Python ships without pip. python -m pip install … fails with No module named pip. Bootstrap with python -m ensurepip --user --upgrade first (the setup script does this).
  2. macOS launches a second Fusion. debugpy.listen() spawns the adapter via sys.executable, which lives inside Fusion.app — LaunchServices then "opens" the app and a second Fusion starts. Fix: pass in_process_debug_adapter=True (done in PowerTools.py). Do not try to redirect the adapter to an external Python.
  3. Zed uses connect, not tcp_connection. The tcp_connection key is silently ignored by Zed's Debugpy adapter (symptom: process exited before debugger attached). Use "connect": { "host": "127.0.0.1", "port": 5678 }.
  4. The VS Code Debug button contaminates the process. If Fusion was launched via Debug this session, VS Code has already injected and listen()-ed its own debugpy, so our listen() raises already called. PowerTools.py catches this RuntimeError. If it gets in the way, fully quit Fusion (⌘Q) and relaunch via Finder/Spotlight — not the Debug button.

A fifth, environment-specific trap — Zed failing to download debugpy behind a proxy — has its own section: When Zed cannot fetch debugpy.

Verification checklist

  • Attach works. .debug present → Stop/Run the add-in → F4 in Zed (or Debug in VS Code) → session connects, threads + call stack populate.
  • Breakpoint binds. Set a breakpoint in a command's command_created (e.g. commands/roundsketchdimensions/entry.py), invoke it from the toolbar → execution stops, the Variables panel shows the command args.
  • Watch evaluates. Add a watch on adsk.core.Application.get() → resolves to the running application object.
  • Step works. F10 / F11 step over / into.
  • Reload cycle. Stop, edit a string, Run, re-attach → the breakpoint hits and the new value is visible.
  • Ship-mode toggle. Remove .debug, relaunch Fusion, confirm lsof -nP -iTCP:5678 -sTCP:LISTEN is empty and the add-in runs normally.

Disabling debugging (ship mode)

Delete the .debug marker. config.DEBUG and config.WAIT_FOR_DEBUGGER both become False, _maybe_start_debug_server() returns immediately, and no port is opened. Because the marker is git-ignored, a distribution never contains it, so a shipped build is always in ship mode. Confirm with:

lsof -nP -iTCP:5678 -sTCP:LISTEN   # should return nothing

After a Fusion update

A Fusion auto-update rotates the webdeploy hash, which breaks the absolute paths in .zed/settings.json / .env and can wipe debugpy. Symptoms: pyright stops resolving adsk.*, lsof no longer shows port 5678, or ModuleNotFoundError: debugpy appears in the Text Commands log.

For the rotated paths, repoint both configs at the newest complete build of whichever channel you are running — this is the normal fix and it respects the channel you chose:

python3 tools/debug/update_debug_path.py pre-production

See Pointing the config at a build.

Only if debugpy itself is gone do you need the upstream setup script, which also rewrites the paths — minding the channel caveat, since it scans production:

~/Source/Zed_Debug/scripts/setup-fusion-debug.sh /Users/schneik/Source/PowerTools

When Fusion crashes: reading a CER report

The debugger is no help for the failure mode this add-in hits most: a native fault. When Fusion segfaults (0xC0000005 on Windows, SIGSEGV on macOS) there is no Python exception and no traceback, so nothing appears in the DEBUG log and the breakpoint never trips. Absence of a traceback is not evidence that a handler ran.

Fusion writes a Customer Error Report instead. On macOS:

ls -t ~/Library/Application\ Support/Autodesk/CER/*/*/ | head
# newest folder holds crashLog.txt.dmp.zip
unzip -o <path>/crashLog.txt.dmp.zip -d /tmp/cer && sed -n '1,120p' /tmp/cer/crashLog.txt

On Windows the reports live under %LOCALAPPDATA%\Autodesk\CER\. Read the top of the stack; two frame patterns in this codebase name their cause directly:

Stack contains Cause
Xl::APICommandDefinitionImpl::doOnCreateCommand beneath createCommandNu::CommandMgr::executeCommand Something re-entered the command manager from command_created — almost always an args.command.doExecute() call. Use commands/_command_abort.py instead (14871d7)
A fault after a long save/close run A Document/Design handle held across a pumped wait went stale. Re-acquire after every wait; check isValid before closing (a1d22e1)

Two habits that have saved real time here:

  • Read the log and the crash stack before theorising. Two identical-looking "Preferences needs a document" bugs had different root causes, and each was settled by the log line or CER frame that pinned it.
  • When the user hands you a crash zip path, open it. It is faster and more reliable than reasoning about which of several recent changes was responsible.

The full symptom-to-cause table, including the non-crashing failure modes, is .agent/symptom-index.md.

Reference


Copyright © 2026 IMA LLC. All rights reserved.