fix(shutdown): defer reclose via Dispatcher so OnClosing yields first#47
Merged
Conversation
OnClosing is async void using a cancel-then-reclose pattern: first entry sets e.Cancel=true, runs async cleanup, then calls Close() to re-enter through the _shutdownComplete branch. The cancel only takes effect once OnClosing yields control back to WPF — that's when WPF reads e.Cancel and resets its internal _isClosing flag. In --clean mode none of the awaits actually yield: SaveStateAsync short-circuits (if App.CleanStart return), the foreach over _vm.Sessions is empty because clean mode clears the session list, and the remaining work is synchronous. So OnClosing runs straight through to the final Close() with WPF still in the closing state, and Close() throws InvalidOperationException: "Cannot set Visibility to Visible or call Show, ShowDialog, Close, or WindowInteropHelper.EnsureHandle while a Window is closing." Queue the reclose via Dispatcher.BeginInvoke so OnClosing returns first, WPF resets _isClosing, and the queued Close() then re-enters cleanly. Works for both the no-yield clean case and the normal case with live sessions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
InvalidOperationException: Cannot ... call Close ... while a Window is closing.thrown fromMainWindow.OnClosingat line 4770 when shutting down in--cleandebug mode.Dispatcher.BeginInvokeinstead of callingClose()inline, so WPF gets a chance to process the cancelled first pass and reset its internal_isClosingflag.Root cause
OnClosingisasync voidusing a cancel-then-reclose pattern: first entry setse.Cancel = true, runs async cleanup, then callsClose()to re-enter through the_shutdownCompletebranch. The cancellation only takes effect onceOnClosingyields control back to WPF — that's when WPF readse.Canceland resets_isClosing.In
--cleanmode none of the awaits actually yield:SaveStateAsync()short-circuits (if (App.CleanStart) return;), so itsawaitresolves synchronouslyforeachover_vm.Sessionsis empty because clean mode clears the session list, soawait DisposeAndWaitForExitAsyncnever runsSo
OnClosingruns straight through to the finalClose()without ever yielding — WPF's_isClosingis stilltrueandClose()throws.Fix
Swap inline
Close()for_ = Dispatcher.BeginInvoke(new System.Action(Close)).OnClosingreturns first, WPF resets_isClosing, then the queuedClose()re-enters cleanly through the_shutdownCompletebranch. Works in both the no-yield clean case and the normal case with live sessions.Test plan
dotnet run --project src/CodeShellManager/CodeShellManager.csproj -- --clean→ close window → verify no exception incrash.logand the app exits cleanly.--cleanwith several live sessions (including a Claude session) → close window → verify normal shutdown,state.jsonis written, and Claude sessions are disposed serially._isShuttingDowngate and doesn't re-run cleanup.🤖 Generated with Claude Code