From bd61e8df2ada1e90090efafb4500de75a064608e Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 13 Nov 2026 09:42:44 -0800 Subject: [PATCH] fix(lifecycle): no dialogs while the app is closing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1ce6b9c made closing stop the session first, which was right and incomplete: the stop path opens TWO modals, and WPF forbids that during a close. The creator's #2 test produced it immediately — System.InvalidOperationException: Cannot set Visibility to Visible or call Show, ShowDialog, Close, or WindowInteropHelper.EnsureHandle while a Window is closing. Two prompts are suppressed by a `_shuttingDown` flag, each taking its safe default rather than a prompt nobody can answer: - The rename dialog: the recording is saved under its automatic name. Never discard footage the creator did not choose to discard. - The private-broadcast keep/delete prompt: the broadcast is KEPT (stays private on YouTube) instead of being deleted by an answer that was never given. The general rule this establishes: a teardown path may not ask questions. Any future prompt reached from stop must check IsShuttingDown. Not covered by a unit test — the trigger is a real WPF close, not reachable from the view model. Verified against the exception in %APPDATA%\ytLlive\startup.log (09:40:27) and it needs the creator's retest. --- ViewModels/MainViewModel.Recording.cs | 18 ++++++++++++++---- .../MainViewModel.Streaming.Operations.cs | 6 +++++- ViewModels/MainViewModel.cs | 6 ++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ViewModels/MainViewModel.Recording.cs b/ViewModels/MainViewModel.Recording.cs index e0f8385..1b77188 100644 --- a/ViewModels/MainViewModel.Recording.cs +++ b/ViewModels/MainViewModel.Recording.cs @@ -77,11 +77,21 @@ public partial class MainViewModel : ViewModelBase // CREATOR RULING 2026-09-26: Cancel means DISCARD, not "save under the // default name". This used to fall through and File.Move anyway, so a // cancelled dialog silently kept the recording — Cancel was a lie. - var dialog = new RenameRecordingDialog(autoStem); - var creatorSaved = dialog.ShowDialog() == true; + // + // APP CLOSE (2026-11-13): a modal cannot open while the window is closing — WPF throws + // "Cannot call ShowDialog while a Window is closing", which killed the app mid-shutdown. + // So closing saves under the automatic name instead: never discard footage the creator + // did not choose to discard, and never hold the window hostage to a dialog. + var creatorSaved = true; + string? chosenName = null; + if (!_shuttingDown) + { + var dialog = new RenameRecordingDialog(autoStem); + creatorSaved = dialog.ShowDialog() == true; + chosenName = dialog.ResultFileName; + } - var result = CompleteRecordingSave( - startPath, dir, autoStem, dialog.ResultFileName, creatorSaved); + var result = CompleteRecordingSave(startPath, dir, autoStem, chosenName, creatorSaved); switch (result.Outcome) { case RecordingSaveOutcome.Saved: diff --git a/ViewModels/MainViewModel.Streaming.Operations.cs b/ViewModels/MainViewModel.Streaming.Operations.cs index e33b5df..2080b33 100644 --- a/ViewModels/MainViewModel.Streaming.Operations.cs +++ b/ViewModels/MainViewModel.Streaming.Operations.cs @@ -690,7 +690,11 @@ public partial class MainViewModel : ViewModelBase { var title = BroadcastForm.Title; var (outcome, error) = await ClosePrivateBroadcastAsync(_youtubeStream, broadcastId, - () => KeepBroadcastPromptOverride?.Invoke(title) ?? new KeepBroadcastDialog(title).ShowDialog() == true); + // Closing the app cannot open a modal (WPF forbids ShowDialog while a window closes), so the + // safe default is KEEP: the broadcast stays on YouTube as a private video for the creator to + // deal with, instead of being deleted by a prompt nobody could answer. + () => _shuttingDown + || (KeepBroadcastPromptOverride?.Invoke(title) ?? new KeepBroadcastDialog(title).ShowDialog() == true)); switch (outcome) { case PrivateBroadcastOutcome.Kept: diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 9dc8da6..f53d538 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -624,10 +624,16 @@ public partial class MainViewModel : ViewModelBase /// while it was still working. The window now holds itself open until the ordinary stop completes /// (bounded — a stuck encoder must not trap the creator in an unclosable app). /// + /// True while the app is closing. Dialogs cannot open then, so the stop path skips its + /// prompts and takes the safe default (save under the automatic name, keep the broadcast). + internal bool IsShuttingDown => _shuttingDown; + private bool _shuttingDown; + internal async Task StopBeforeShutdownAsync() { if (!IsLive && !IsRecording) return true; + _shuttingDown = true; StopStream(); for (var i = 0; i < 120 && (IsLive || IsRecording); i++) // up to ~6 s await Task.Delay(50).ConfigureAwait(true);