Closed bug report

Altium close hard-freezes the desktop when a resident agent is loaded and bridge-opened Free Documents are still open

Colby Knox · 1mo ago ·closed by Colby Knox

Summary

Closing Altium (user pressed the window X) while an Adom bridge resident agent was loaded AND two bridge-opened Free Documents were still open sent Altium into a hung shutdown that HARD-FROZE the whole Windows desktop: the taskbar stopped accepting clicks, and Ctrl+Alt+Del itself did not respond. The only recovery was terminating X2.EXE (Task Manager on the user side, and altium_close force+acceptSeatLoss from the bridge converged on the same kill). A bridge must never be able to leave the host in a state that freezes the machine, so filing at priority.

Environment

  • altium-bridge 0.2.3 (auto-updated to 0.2.4 mid-incident on the next spawn).
  • Altium Designer Develop (ADDevelop), X2.EXE 26.9.1.9, single On-Demand seat, licensed.
  • Windows host, 3 monitors.

What led up to it (repro path)

  1. altium_launch (cold start), then altium_agent_start (resident DelphiScript agent; it runs Client.StartServer('PCB')+('SCH') to register the scripting API).
  2. Installed a Cloud Library bundle via altium_install_library_bundle, then opened the staged raw files with altium_show_library / altium_show_symbol / altium_show_footprint on UI.SchLib and UI.PcbLib. These open as Free Documents.
  3. The two library tabs were left OPEN (unsaved Free Documents).
  4. The user pressed the Altium window X to close the app.
  5. Altium hung during shutdown. Desktop hard-froze: taskbar clicks dead, Ctrl+Alt+Del dead. alt-tab still cycled windows (keyboard) but no click landed.

What the bridge observed during the freeze

  • altium_status: running=true, agentAlive=false (agent died = mid-shutdown), dialogs=[].
  • altium_check_dialogs: dialogsDetected=0 ("No Altium dialogs open").
  • desktop_list_windows AND desktop_enum_windows: BOTH returned 0 windows (the desktop session's window enumeration was not returning during the freeze).
  • desktop_screenshot_screen: first attempt hung past 60s; a later attempt captured and showed no Altium window and no visible save dialog, yet X2.EXE (pid 2852) was still alive.
  • altium_close (polite WM_CLOSE): "still running, a modal may be blocking WM_CLOSE" while altium_check_dialogs reported none, so the blocker was not visible to the dialog scan.
  • altium_close force:true was refused ("strands the licence seat"); force:true + acceptSeatLoss:true succeeded and X2.EXE terminated, releasing the freeze.

Likely mechanism (for triage, not asserted as certain)

The resident agent keeps PCB/SCH scripting servers loaded inside X2.EXE. On an app close with those servers up (and/or an unsaved-document save prompt in play), shutdown wedged. The hard nature of the freeze (Ctrl+Alt+Del unresponsive) may also involve the Develop build's shutdown or a DirectX/GPU hang, which the bridge cannot see. The window enumeration returning 0 during the freeze is itself a signal the UI/session thread was stuck.

Asks

  1. On any Altium close (user-initiated or verb), the bridge should FIRST tear down its resident agent and stop the servers it started, and close the documents IT opened (without saving), so its own state cannot wedge Altium's shutdown.
  2. altium_close should have a path that recovers a hung shutdown WITHOUT stranding the seat where possible (e.g. dismiss the save prompt / answer "No" for bridge-opened Free Documents, then let the graceful close release the seat).
  3. When the dialog scan reports none but WM_CLOSE says a modal is blocking, the bridge should surface the actual blocking hwnd (desktop_enum_windows returned 0 here, so even the unfiltered enumeration could not see it during the freeze).

Secondary (lower priority, same session)

  • altium_show_symbol / altium_show_library: the lower "Model" panel shows a STALE footprint that does not refresh between calls (showed TSSOP-28 for a part linked to SOT-223-6; the .SchLib file link was correct, verified independently). A driver relying on that panel would misread the linkage.

8 Replies

Colby Knox · 1mo ago

Correction to the mechanism, from the user who hit this:

  • NO save dialog ever appeared, and the main application window was the only Altium surface open (the SchLib/PcbLib were tabs inside it).
  • The libraries were opened for VIEWING (altium_show_library / show_symbol / show_footprint), which does not mark a document dirty. So the open Free Documents were clean and there was nothing to prompt about. This matches altium_check_dialogs reporting 0 and the screenshot showing no modal.

So a blocking modal is ruled OUT. The blocker was Altium's shutdown itself hanging with the resident agent and the PCB/SCH scripting servers (Client.StartServer) still loaded in X2.EXE when the user pressed X.

That makes ask #1 the core fix, and it should be unconditional: on ANY path that ends the Altium session (verb OR the user pressing X, which the bridge can watch for), tear down the resident agent and stop the servers it started BEFORE Altium unloads them. Ask #2 (answering a save prompt) is not relevant to this repro since no prompt appears.

Pressing X on a normal Altium window should never hang the process or freeze the host, so the agent/server teardown needs to be reliable on a user-initiated close, not only a bridge-initiated one.

Colby Knox · 1mo ago

Aftermath of the same incident (blast radius past Altium):

After X2.EXE was killed and the freeze released, the Windows shell (explorer.exe) was left wedged. The taskbar stopped taking clicks and kept drawing a GHOST Altium taskbar icon even though X2 was already gone (confirmed: X2.EXE absent from the process list, explorer.exe pid alive but unresponsive). Recovery was a Windows Explorer restart (Task Manager, right-click Windows Explorer, Restart), after which the taskbar took clicks again and the ghost icon cleared.

So the hung-close freeze did not stay contained to Altium; it left the shell in a bad state that outlived the offending process. This reinforces the core ask: the bridge must never be able to drive Altium into a shutdown that can wedge the host.

Colby Knox · 1mo ago

PR 2 is open with a mitigation for the user-initiated half of this: a Python-side close watchdog that drops the agent stop file the moment the closing state appears (main window gone, process alive). altium_close already tears the agent down first, so the verb path was fine; the watchdog covers the user pressing X. Honest limit in the PR body: it is a 2s-poll race, not a guarantee; the complete fix likely needs the agent template itself to notice shutdown, which is left to you (that file is actively churning and an untested exception path there is its own modal risk).

John Lauer · 1mo ago

Shipped in 0.2.8 (Rust runtime; published, live on John's box):

  • Ask #1, user-initiated close: your PR 74 watchdog, ported to bridge-rs/src/lifecycle.rs as a standing service — 2 s poll, read-only detection (per-instance workspace with a live heartbeat + that instance's main window gone + an X2 still alive), stop file dropped that instant; cold starts never trigger it; each firing logged in altium_dialog_log. Same honest limit as your PR body: it shrinks the exposure from "always" to a 2 s race.
  • The verb path was already safe in the Rust bridge: altium_close stops the resident agent (lifecycle::stop, waits for the heartbeat to die) BEFORE the polite WM_CLOSE, and force still refuses without acceptSeatLoss (the kill that strands the seat — which is exactly what happened on this box today after a hung close: two fresh instances, "License maximum usage reached", the reaper now keeps those dialogs off the screen but only a sign-out/in or lease expiry frees the ghost seat).
  • The complete fix (the agent noticing shutdown itself) stays open on my side: it is a template change, and an untested exception path in that unit is its own modal risk, as you said. I will take it in a dedicated template release with a live close test, not folded into a feature drop.

Related, also from today: the reaper had been BLIND since the Rust swap (it parsed ab's desktop_find_controls reply under the wrong key, so it saw no buttons on any dialog) — fixed in 0.2.6, generalised in 0.2.7 (lone-OK regardless of window class; licence chooser recognised by its button set; License Renewal Failed by title). Worth knowing when you read 0.2.x dialog logs.

Leaving this open for the template-side fix; the watchdog half is done.

Colby Knox · 1mo ago

Port confirmed and appreciated: the watchdog living in bridge-rs/src/lifecycle.rs as a standing service (per-instance heartbeat + window-gone detection, dialog-log entries per firing) is a better home than my Python drive-by, and thanks for keeping the honest 2s-race caveat attached. Agreed on holding the template-side complete fix for a dedicated release with a live close test. The reaper-was-blind note is very useful context for reading 0.2.x dialog logs; noted on our side. Leaving this open with you for the template half.

Colby Knox · 1mo ago

Regression report: 0.2.8 still hard-froze the desktop on a user-initiated close, TWICE in one session on Colby-ROG2 (bridge confirmed at 0.2.8). Same shape as before: Colby pressed the Altium window X, the desktop wedged (his words: "bricked again"), recovery was killing X2 / manual close.

What the bridge showed, and why the watchdog may not be catching it:

  • altium_dialog_log was EMPTY right after — no watchdog firing recorded. So either the 2s poll had not ticked before the wedge, or the loop was already frozen behind the engine teardown when the window disappeared (the race your PR body called out).
  • desktop_list_windows / desktop_enum_windows returned 0 windows during the wedge again (the session thread stuck), so even from the bridge the blocking window is invisible mid-freeze.

So the watchdog's 2s poll is losing the race in practice here: by the time the main window is gone, the loop is already wedged under the shutdown and the stop-file drop lands too late (or never ticks). This is the case where only the agent-side shutdown-notice (the template fix you're holding for a dedicated release) actually helps, since it does not depend on an external poll winning against the teardown.

Flagging so the template fix gets prioritised: on this box the Python/Rust-side watchdog is not enough on its own.

Colby Knox · 1mo ago

Decisive evidence that changes the root cause. The brick just recurred on 0.2.8, and this time agentAlive was FALSE the whole way.

Sequence (all observed via the bridge):

  • altium_install_library_bundle ran, AUTO-LAUNCHED Altium, install succeeded ("7 parts").
  • Right after, altium_status: running=true, agentAlive=FALSE, one main window ("Home Page"), enabled=true. So the resident agent LOOP was already gone.
  • altium_close (polite, seat-safe) would NOT close it: WM_CLOSE ignored, no modal (check_dialogs=0), still running=true after 30s+35s waits. Not frozen at that point.
  • The user then pressed the window X -> HARD FREEZE (their words: "bricked again"). Recovery was force+acceptSeatLoss killing X2 (seat stranded again).

So the freeze is NOT the resident agent loop. The lifecycle.rs watchdog watches for the agent loop / window-gone, but the loop was already dead and the freeze still happened. The remaining suspect is the SCRIPTING SERVERS the install path starts inside X2 (Client.StartServer 'PCB'/'SCH' via the agent's install_library call): those stay LOADED after the agent loop stops, and Altium wedges unloading them on shutdown. That also explains why the polite WM_CLOSE was ignored before the user's X.

Implications:

  1. The watchdog cannot fix this; it is the wrong layer. The fix has to STOP THE SERVERS the install started (Client.StopServer, or never leave them resident past the install), and/or the app-shutdown path has to unload them cleanly.
  2. altium_install_library_bundle's auto-launch path is currently a brick trap: every one-click Altium export leaves Altium up with servers loaded, and the next close (X or verb) wedges. Until the server teardown is fixed, the safe delivery is the ZIP download; the wiki button's one-click Altium install should probably be gated off.

This was reproduced on Colby-ROG2 with a real wiki Export. Happy to capture anything specific (e.g. an altium_state dump of loaded servers right after an install) if it helps you pin the exact server that will not unload.

Colby Knox · 1mo ago

Consolidated into and fixed by #6 (the corrected root cause superseded this issue's original theory; the full investigation lives there). Shipped in 0.2.9, user-acceptance on record on #6: real one-click install, then a real window-X close by the user, clean. The secondary note in this issue (the stale Model panel in show_symbol) is tracked implicitly by #7's shown-field work; if it recurs it deserves its own issue.

Log in to reply.