Skip to content

Fix #1050: defer re-close off the Closing event (VerifyNotClosing throw)#1109

Merged
erikdarlingdata merged 1 commit into
devfrom
feature/1050-close-reentrancy
Jun 11, 2026
Merged

Fix #1050: defer re-close off the Closing event (VerifyNotClosing throw)#1109
erikdarlingdata merged 1 commit into
devfrom
feature/1050-close-reentrancy

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

What

MainWindow_Closing cancels the first close, runs async cleanup, then calls Close() again at the end. That re-close is only safe if one of the preceding awaits actually suspended (yielding to WPF, which clears Window._isClosing). When the cleanup runs straight through without suspending — MCP disabled (StopMcpServerAsync completes synchronously) and the collector already idle (BackgroundService.StopAsync's Task.WhenAny completes synchronously) — the final Close() fires while we're still inside the first Closing event with _isClosing == true, and InternalClose → VerifyNotClosing() throws:

InvalidOperationException: Cannot set Visibility to Visible or call Show, ShowDialog, Close, or WindowInteropHelper.EnsureHandle while a Window is closing.

That timing dependence is exactly why it was intermittent in the field report (#1050): a relaunch with the collector mid-cycle gave StopAsync something real to await, so the re-close landed a beat later and worked.

Fix

Defer the re-close with Dispatcher.BeginInvoke(new Action(Close)) so this Closing event fully unwinds — clearing _isClosing — before the real close runs. The existing two-pass contract is unchanged: the queued Close() re-enters the handler, hits _closingCleanupDone, returns early, and the window closes for real.

This was a latent bug in the close path, surfaced (not caused) by the #1050 relaunch fix — once a normally-rendered window could be cleanly closed, the re-entrancy became reachable.

Test plan

WPF window-lifecycle change, not unit-testable; validate manually:

  • Open Lite → click Close → window closes cleanly, no exception dialog.
  • Minimize-to-tray → restore → Close → closes cleanly.
  • Relaunch (collector mid-cycle) → Close immediately → still closes cleanly (the path that already worked).

Reported by @bbishop-neovest in #1050 (his #5/#6 observations pinned the timing).

🤖 Generated with Claude Code

…ing throw

MainWindow_Closing cancels the first close, runs async cleanup, then calls
Close() again. That second call is only safe if one of the preceding awaits
actually suspended (yielding to WPF, which clears Window._isClosing). When the
cleanup runs straight through without suspending — MCP disabled, so
StopMcpServerAsync completes synchronously, and the collector already idle, so
BackgroundService.StopAsync's Task.WhenAny completes synchronously too — the
Close() at the end fires while we're still inside the first Closing event with
_isClosing == true, and InternalClose → VerifyNotClosing() throws
"Cannot ... call Close ... while a Window is closing." That timing dependence
is why it was intermittent (a relaunch with the collector mid-cycle gave
StopAsync something to await, so the re-close landed a beat later and worked).

Defer the re-close with Dispatcher.BeginInvoke so this Closing event fully
unwinds — clearing _isClosing — before the real close runs. The existing
two-pass contract is unchanged: the queued Close() re-enters the handler, hits
_closingCleanupDone, returns early, and the window closes for real.

Latent bug in the close path, surfaced (not caused) by the #1050 relaunch fix
once a normally-rendered window could be cleanly closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit d21f014 into dev Jun 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant