Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions Terminal.Gui/App/ApplicationImpl.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,15 +227,30 @@ public void Invoke (Action action)
return null;
}

try
// Loop to handle the case where End is cancelled by an IsRunningChanging handler.
// When End is cancelled, IsRunning remains true; we reset StopRequested and re-run the loop.
while (true)
{
// All runnables block until RequestStop() is called
RunLoop (runnable, errorHandler);
}
finally
{
// End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
End (token);
try
{
// All runnables block until RequestStop() is called
RunLoop (runnable, errorHandler);
}
finally
{
// End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
End (token);
}

// If End succeeded IsRunning is now false — we are done
if (!runnable.IsRunning)
{
break;
}

// End was cancelled by an IsRunningChanging handler (e.g., "Are you sure?" veto).
// Reset StopRequested so RunLoop can re-enter its while condition correctly.
runnable.StopRequested = false;
}

return token.Result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,34 @@ public void Run_IsRunningChanging_Cancel_IsRunningChanged_Not_Raised ()
Assert.Equal (0, isRunningChanged);
}

// Copilot
/// <summary>
/// Regression test for: https://github.com/gui-cs/Terminal.Gui/issues/4920
/// StopRequested not reset when IsRunningChanging cancels a stop.
/// When a handler vetoes the stop (by cancelling IsRunningChanging), Run() must re-enter
/// the RunLoop instead of returning prematurely with IsRunning still true.
/// </summary>
[Fact]
public void Run_WhenStopCancelledByIsRunningChanging_RunLoopResumes ()
{
IApplication app = NewMockedApplicationImpl ()!;
app.Init (DriverRegistry.Names.ANSI);
app.StopAfterFirstIteration = true;

OnceCancelStopRunnable runnable = new ();

// Act — must complete without hanging
app.Run (runnable);

// The first stop attempt was cancelled (IsRunning stayed true temporarily).
// After the fix, StopRequested was reset and RunLoop re-entered; the second stop
// attempt succeeded so IsRunning is now false.
Assert.False (runnable.IsRunning);
Assert.Equal (1, runnable.CancelCount);

app.Dispose ();
}

private bool IdleExit (IApplication app)
{
if (app.TopRunnableView != null)
Expand Down Expand Up @@ -490,4 +518,29 @@ public void ApplicationImpl_UsesInstanceFields_NotStaticReferences ()
Assert.Null (v2.TopRunnableView);
Assert.Empty (v2.SessionStack!);
}

/// <summary>
/// A runnable that cancels only the first stop attempt.
/// Used to simulate a "Are you sure?" veto pattern.
/// </summary>
private class OnceCancelStopRunnable : Runnable<int>
{
private bool _firstStop = true;

/// <summary>Gets how many times the stop was cancelled.</summary>
public int CancelCount { get; private set; }

protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning)
{
if (!newIsRunning && _firstStop)
{
_firstStop = false;
CancelCount++;

return true; // Cancel this stop attempt
}

return base.OnIsRunningChanging (oldIsRunning, newIsRunning);
}
}
}
Loading