Skip to content

Commit 10cf766

Browse files
Fixes #4920. Re-enter RunLoop when End is cancelled by IsRunningChanging (#4921)
* Fix StopRequested not reset when IsRunningChanging cancels a stop When an IsRunningChanging handler vetoed a stop (e.g. 'Are you sure?' pattern), End() returned early but Run() also returned - leaving IsRunning=true, StopRequested=true, and no event loop running, causing an application freeze. Fix: wrap the try/finally in ApplicationImpl.Run() in a while(true) loop. After End() returns, check runnable.IsRunning: - false -> End succeeded, break (no behaviour change) - true -> End was cancelled, reset StopRequested=false and re-enter RunLoop Also adds regression test Run_WhenStopCancelledByIsRunningChanging_RunLoopResumes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d979cbd commit 10cf766

2 files changed

Lines changed: 76 additions & 8 deletions

File tree

Terminal.Gui/App/ApplicationImpl.Run.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,30 @@ public void Invoke (Action action)
227227
return null;
228228
}
229229

230-
try
230+
// Loop to handle the case where End is cancelled by an IsRunningChanging handler.
231+
// When End is cancelled, IsRunning remains true; we reset StopRequested and re-run the loop.
232+
while (true)
231233
{
232-
// All runnables block until RequestStop() is called
233-
RunLoop (runnable, errorHandler);
234-
}
235-
finally
236-
{
237-
// End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
238-
End (token);
234+
try
235+
{
236+
// All runnables block until RequestStop() is called
237+
RunLoop (runnable, errorHandler);
238+
}
239+
finally
240+
{
241+
// End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
242+
End (token);
243+
}
244+
245+
// If End succeeded IsRunning is now false — we are done
246+
if (!runnable.IsRunning)
247+
{
248+
break;
249+
}
250+
251+
// End was cancelled by an IsRunningChanging handler (e.g., "Are you sure?" veto).
252+
// Reset StopRequested so RunLoop can re-enter its while condition correctly.
253+
runnable.StopRequested = false;
239254
}
240255

241256
return token.Result;

Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,34 @@ public void Run_IsRunningChanging_Cancel_IsRunningChanged_Not_Raised ()
392392
Assert.Equal (0, isRunningChanged);
393393
}
394394

395+
// Copilot
396+
/// <summary>
397+
/// Regression test for: https://github.com/gui-cs/Terminal.Gui/issues/4920
398+
/// StopRequested not reset when IsRunningChanging cancels a stop.
399+
/// When a handler vetoes the stop (by cancelling IsRunningChanging), Run() must re-enter
400+
/// the RunLoop instead of returning prematurely with IsRunning still true.
401+
/// </summary>
402+
[Fact]
403+
public void Run_WhenStopCancelledByIsRunningChanging_RunLoopResumes ()
404+
{
405+
IApplication app = NewMockedApplicationImpl ()!;
406+
app.Init (DriverRegistry.Names.ANSI);
407+
app.StopAfterFirstIteration = true;
408+
409+
OnceCancelStopRunnable runnable = new ();
410+
411+
// Act — must complete without hanging
412+
app.Run (runnable);
413+
414+
// The first stop attempt was cancelled (IsRunning stayed true temporarily).
415+
// After the fix, StopRequested was reset and RunLoop re-entered; the second stop
416+
// attempt succeeded so IsRunning is now false.
417+
Assert.False (runnable.IsRunning);
418+
Assert.Equal (1, runnable.CancelCount);
419+
420+
app.Dispose ();
421+
}
422+
395423
private bool IdleExit (IApplication app)
396424
{
397425
if (app.TopRunnableView != null)
@@ -490,4 +518,29 @@ public void ApplicationImpl_UsesInstanceFields_NotStaticReferences ()
490518
Assert.Null (v2.TopRunnableView);
491519
Assert.Empty (v2.SessionStack!);
492520
}
521+
522+
/// <summary>
523+
/// A runnable that cancels only the first stop attempt.
524+
/// Used to simulate a "Are you sure?" veto pattern.
525+
/// </summary>
526+
private class OnceCancelStopRunnable : Runnable<int>
527+
{
528+
private bool _firstStop = true;
529+
530+
/// <summary>Gets how many times the stop was cancelled.</summary>
531+
public int CancelCount { get; private set; }
532+
533+
protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning)
534+
{
535+
if (!newIsRunning && _firstStop)
536+
{
537+
_firstStop = false;
538+
CancelCount++;
539+
540+
return true; // Cancel this stop attempt
541+
}
542+
543+
return base.OnIsRunningChanging (oldIsRunning, newIsRunning);
544+
}
545+
}
493546
}

0 commit comments

Comments
 (0)