using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; internal partial class ApplicationImpl { // Lock object to protect session stack operations and cached state updates private readonly object _sessionStackLock = new (); #region Session State - Stack and TopRunnable /// public ConcurrentStack? SessionStack { get; } = new (); /// public IRunnable? TopRunnable { get; private set; } /// public View? TopRunnableView => TopRunnable as View; /// public event EventHandler? SessionBegun; /// public event EventHandler? SessionEnded; #endregion Session State - Stack and TopRunnable #region Main Loop Iteration /// public bool StopAfterFirstIteration { get; set; } /// public event EventHandler>? Iteration; /// public void RaiseIteration () { Iteration?.Invoke (null, new (this)); } #endregion Main Loop Iteration #region Timeouts and Invoke private readonly ITimedEvents _timedEvents = new TimedEvents (); /// public ITimedEvents? TimedEvents => _timedEvents; /// public object AddTimeout (TimeSpan time, Func callback) => _timedEvents.Add (time, callback); /// public bool RemoveTimeout (object token) => _timedEvents.Remove (token); /// public void Invoke (Action? action) { // If we are already on the main UI thread if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { action?.Invoke (this); return; } _timedEvents.Add ( TimeSpan.Zero, () => { action?.Invoke (this); return false; } ); } /// public void Invoke (Action action) { // If we are already on the main UI thread if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { action?.Invoke (); return; } _timedEvents.Add ( TimeSpan.Zero, () => { action?.Invoke (); return false; } ); } #endregion Timeouts and Invoke #region Session Lifecycle - Begin /// public SessionToken? Begin (IRunnable runnable) { ArgumentNullException.ThrowIfNull (runnable); if (runnable.IsRunning) { throw new ArgumentException (@"The runnable is already running.", nameof (runnable)); } // Create session token SessionToken token = new (runnable); // Get old IsRunning value BEFORE any stack changes (safe - cached value) bool oldIsRunning = runnable.IsRunning; // Raise IsRunningChanging OUTSIDE lock (false -> true) - can be canceled if (runnable.RaiseIsRunningChanging (oldIsRunning, true)) { // Starting was canceled return null; } // Set the application reference in the runnable runnable.SetApp (this); // Ensure the mouse is ungrabbed Mouse.UngrabMouse (); IRunnable? previousTop = null; // CRITICAL SECTION - Atomic stack + cached state update lock (_sessionStackLock) { // Get the previous top BEFORE pushing new token if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { }) { previousTop = previousToken.Runnable; } if (previousTop == runnable) { throw new ArgumentOutOfRangeException (nameof (runnable), runnable, @"Attempt to Run the runnable that's already the top runnable."); } // Push token onto SessionStack SessionStack?.Push (token); TopRunnable = runnable; // Update cached state atomically - IsRunning and IsModal are now consistent SessionBegun?.Invoke (this, new (token)); runnable.SetIsRunning (true); runnable.SetIsModal (true); // Previous top is no longer modal if (previousTop != null) { previousTop.SetIsModal (false); } } // END CRITICAL SECTION - IsRunning/IsModal now thread-safe // Fire events AFTER lock released (avoid deadlocks in event handlers) if (previousTop != null) { previousTop.RaiseIsModalChangedEvent (false); } runnable.RaiseIsRunningChangedEvent (true); runnable.RaiseIsModalChangedEvent (true); //RaiseIteration (); LayoutAndDraw (); return token; } #endregion Session Lifecycle - Begin #region Session Lifecycle - Run /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new() { if (!Initialized) { // Init() has NOT been called. Auto-initialize as per interface contract. Init (driverName); } if (Driver is null) { throw new InvalidOperationException (@"Driver is null after Init."); } TRunnable runnable = new (); object? result = Run (runnable, errorHandler); // We created the runnable, so dispose it if it's disposable if (runnable is IDisposable disposable) { disposable.Dispose (); } return this; } /// public object? Run (IRunnable runnable, Func? errorHandler = null) { ArgumentNullException.ThrowIfNull (runnable); if (!Initialized) { throw new NotInitializedException (@"Init must be called before Run."); } // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged) SessionToken? token; if (runnable.IsRunning) { // Find it on the stack token = SessionStack?.FirstOrDefault (st => st.Runnable == runnable); } else { token = Begin (runnable); } if (token is null) { Logging.Trace (@"Run - Begin session failed or was cancelled."); return null; } try { // All runnables block until RequestStop() is called RunLoop (runnable, errorHandler); } finally { // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack) End (token); } return token.Result; } private void RunLoop (IRunnable runnable, Func? errorHandler) { runnable.StopRequested = false; // Main loop - blocks until RequestStop() is called // Note: IsRunning is now a cached property, safe to check each iteration var firstIteration = true; while (runnable is { StopRequested: false, IsRunning: true }) { if (Coordinator is null) { throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run"); } try { // Process one iteration of the event loop Coordinator.RunIteration (); } catch (Exception ex) { if (errorHandler is null || !errorHandler (ex)) { throw; } } if (StopAfterFirstIteration && firstIteration) { Logging.Information ("Run - Stopping after first iteration as requested"); RequestStop (runnable); } firstIteration = false; } } #endregion Session Lifecycle - Run #region Session Lifecycle - End /// public void End (SessionToken token) { ArgumentNullException.ThrowIfNull (token); if (token.Runnable is null) { return; // Already ended } // TODO: Move Poppover to utilize IRunnable arch; Get all refs to anyting // TODO: View-related out of ApplicationImpl. if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) { ApplicationPopover.HideWithQuitCommand (visiblePopover); } IRunnable runnable = token.Runnable; // Get old IsRunning value (safe - cached value) bool oldIsRunning = runnable.IsRunning; // Raise IsRunningChanging OUTSIDE lock (true -> false) - can be canceled // This is where Result should be extracted! if (runnable.RaiseIsRunningChanging (oldIsRunning, false)) { // Stopping was canceled - do not proceed with End return; } bool wasModal = runnable.IsModal; IRunnable? previousRunnable = null; // CRITICAL SECTION - Atomic stack + cached state update lock (_sessionStackLock) { // Pop token from SessionStack if (wasModal && SessionStack?.TryPop (out SessionToken? popped) == true && popped == token) { // Restore previous top runnable if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { }) { previousRunnable = previousToken.Runnable; // Previous runnable becomes modal again previousRunnable.SetIsModal (true); } } // Update cached state atomically - IsRunning and IsModal are now consistent runnable.SetIsRunning (false); runnable.SetIsModal (false); } // END CRITICAL SECTION - IsRunning/IsModal now thread-safe // Fire events AFTER lock released if (wasModal) { runnable.RaiseIsModalChangedEvent (false); } TopRunnable = null; if (previousRunnable != null) { TopRunnable = previousRunnable; previousRunnable.RaiseIsModalChangedEvent (true); } runnable.RaiseIsRunningChangedEvent (false); token.Result = runnable.Result; _result = token.Result; // Clear the Runnable from the token token.Runnable = null; SessionEnded?.Invoke (this, new (token)); } #endregion Session Lifecycle - End #region Session Lifecycle - RequestStop /// public void RequestStop () { RequestStop (null); } /// public void RequestStop (IRunnable? runnable) { // Get the runnable to stop if (runnable is null) { // Try to get from TopRunnable if (TopRunnableView is IRunnable r) { runnable = r; } else { return; } } runnable.StopRequested = true; // Note: The End() method will be called from the finally block in Run() // and that's where IsRunningChanging/IsRunningChanged will be raised } #endregion Session Lifecycle - RequestStop }