fix(lifecycle): no dialogs while the app is closing
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.
This commit is contained in:
@@ -77,11 +77,21 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
// CREATOR RULING 2026-09-26: Cancel means DISCARD, not "save under the
|
// 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
|
// default name". This used to fall through and File.Move anyway, so a
|
||||||
// cancelled dialog silently kept the recording — Cancel was a lie.
|
// cancelled dialog silently kept the recording — Cancel was a lie.
|
||||||
|
//
|
||||||
|
// 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);
|
var dialog = new RenameRecordingDialog(autoStem);
|
||||||
var creatorSaved = dialog.ShowDialog() == true;
|
creatorSaved = dialog.ShowDialog() == true;
|
||||||
|
chosenName = dialog.ResultFileName;
|
||||||
|
}
|
||||||
|
|
||||||
var result = CompleteRecordingSave(
|
var result = CompleteRecordingSave(startPath, dir, autoStem, chosenName, creatorSaved);
|
||||||
startPath, dir, autoStem, dialog.ResultFileName, creatorSaved);
|
|
||||||
switch (result.Outcome)
|
switch (result.Outcome)
|
||||||
{
|
{
|
||||||
case RecordingSaveOutcome.Saved:
|
case RecordingSaveOutcome.Saved:
|
||||||
|
|||||||
@@ -690,7 +690,11 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
var title = BroadcastForm.Title;
|
var title = BroadcastForm.Title;
|
||||||
var (outcome, error) = await ClosePrivateBroadcastAsync(_youtubeStream, broadcastId,
|
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)
|
switch (outcome)
|
||||||
{
|
{
|
||||||
case PrivateBroadcastOutcome.Kept:
|
case PrivateBroadcastOutcome.Kept:
|
||||||
|
|||||||
@@ -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
|
/// 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).</para>
|
/// (bounded — a stuck encoder must not trap the creator in an unclosable app).</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>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).</summary>
|
||||||
|
internal bool IsShuttingDown => _shuttingDown;
|
||||||
|
private bool _shuttingDown;
|
||||||
|
|
||||||
internal async Task<bool> StopBeforeShutdownAsync()
|
internal async Task<bool> StopBeforeShutdownAsync()
|
||||||
{
|
{
|
||||||
if (!IsLive && !IsRecording) return true;
|
if (!IsLive && !IsRecording) return true;
|
||||||
|
|
||||||
|
_shuttingDown = true;
|
||||||
StopStream();
|
StopStream();
|
||||||
for (var i = 0; i < 120 && (IsLive || IsRecording); i++) // up to ~6 s
|
for (var i = 0; i < 120 && (IsLive || IsRecording); i++) // up to ~6 s
|
||||||
await Task.Delay(50).ConfigureAwait(true);
|
await Task.Delay(50).ConfigureAwait(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user