ApplicationImpl.Run.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. using System.Collections.Concurrent;
  2. using System.Diagnostics.CodeAnalysis;
  3. namespace Terminal.Gui.App;
  4. public partial class ApplicationImpl
  5. {
  6. // Lock object to protect session stack operations and cached state updates
  7. private readonly object _sessionStackLock = new ();
  8. #region Session State - Stack and TopRunnable
  9. /// <inheritdoc/>
  10. public ConcurrentStack<SessionToken>? SessionStack { get; } = new ();
  11. /// <inheritdoc/>
  12. public IRunnable? TopRunnable { get; private set; }
  13. /// <inheritdoc/>
  14. public View? TopRunnableView => TopRunnable as View;
  15. /// <inheritdoc/>
  16. public event EventHandler<SessionTokenEventArgs>? SessionBegun;
  17. /// <inheritdoc/>
  18. public event EventHandler<SessionTokenEventArgs>? SessionEnded;
  19. #endregion Session State - Stack and TopRunnable
  20. #region Main Loop Iteration
  21. /// <inheritdoc/>
  22. public bool StopAfterFirstIteration { get; set; }
  23. /// <inheritdoc/>
  24. public event EventHandler<EventArgs<IApplication?>>? Iteration;
  25. /// <inheritdoc/>
  26. public void RaiseIteration () { Iteration?.Invoke (null, new (this)); }
  27. #endregion Main Loop Iteration
  28. #region Timeouts and Invoke
  29. private readonly ITimedEvents _timedEvents = new TimedEvents ();
  30. /// <inheritdoc/>
  31. public ITimedEvents? TimedEvents => _timedEvents;
  32. /// <inheritdoc/>
  33. public object AddTimeout (TimeSpan time, Func<bool> callback) => _timedEvents.Add (time, callback);
  34. /// <inheritdoc/>
  35. public bool RemoveTimeout (object token) => _timedEvents.Remove (token);
  36. /// <inheritdoc/>
  37. public void Invoke (Action<IApplication>? action)
  38. {
  39. // If we are already on the main UI thread
  40. if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
  41. {
  42. action?.Invoke (this);
  43. return;
  44. }
  45. _timedEvents.Add (
  46. TimeSpan.Zero,
  47. () =>
  48. {
  49. action?.Invoke (this);
  50. return false;
  51. }
  52. );
  53. }
  54. /// <inheritdoc/>
  55. public void Invoke (Action action)
  56. {
  57. // If we are already on the main UI thread
  58. if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
  59. {
  60. action?.Invoke ();
  61. return;
  62. }
  63. _timedEvents.Add (
  64. TimeSpan.Zero,
  65. () =>
  66. {
  67. action?.Invoke ();
  68. return false;
  69. }
  70. );
  71. }
  72. #endregion Timeouts and Invoke
  73. #region Session Lifecycle - Begin
  74. /// <inheritdoc/>
  75. public SessionToken? Begin (IRunnable runnable)
  76. {
  77. ArgumentNullException.ThrowIfNull (runnable);
  78. if (runnable.IsRunning)
  79. {
  80. throw new ArgumentException (@"The runnable is already running.", nameof (runnable));
  81. }
  82. // Create session token
  83. SessionToken token = new (runnable);
  84. // Get old IsRunning value BEFORE any stack changes (safe - cached value)
  85. bool oldIsRunning = runnable.IsRunning;
  86. // Raise IsRunningChanging OUTSIDE lock (false -> true) - can be canceled
  87. if (runnable.RaiseIsRunningChanging (oldIsRunning, true))
  88. {
  89. // Starting was canceled
  90. return null;
  91. }
  92. // Set the application reference in the runnable
  93. runnable.SetApp (this);
  94. // Ensure the mouse is ungrabbed
  95. Mouse.UngrabMouse ();
  96. IRunnable? previousTop = null;
  97. // CRITICAL SECTION - Atomic stack + cached state update
  98. lock (_sessionStackLock)
  99. {
  100. // Get the previous top BEFORE pushing new token
  101. if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { })
  102. {
  103. previousTop = previousToken.Runnable;
  104. }
  105. if (previousTop == runnable)
  106. {
  107. throw new ArgumentOutOfRangeException (nameof (runnable), runnable, @"Attempt to Run the runnable that's already the top runnable.");
  108. }
  109. // Push token onto SessionStack
  110. SessionStack?.Push (token);
  111. TopRunnable = runnable;
  112. // Update cached state atomically - IsRunning and IsModal are now consistent
  113. SessionBegun?.Invoke (this, new (token));
  114. runnable.SetIsRunning (true);
  115. runnable.SetIsModal (true);
  116. // Previous top is no longer modal
  117. if (previousTop != null)
  118. {
  119. previousTop.SetIsModal (false);
  120. }
  121. }
  122. // END CRITICAL SECTION - IsRunning/IsModal now thread-safe
  123. // Fire events AFTER lock released (avoid deadlocks in event handlers)
  124. if (previousTop != null)
  125. {
  126. previousTop.RaiseIsModalChangedEvent (false);
  127. }
  128. runnable.RaiseIsRunningChangedEvent (true);
  129. runnable.RaiseIsModalChangedEvent (true);
  130. LayoutAndDraw ();
  131. return token;
  132. }
  133. #endregion Session Lifecycle - Begin
  134. #region Session Lifecycle - Run
  135. /// <inheritdoc/>
  136. [RequiresUnreferencedCode ("AOT")]
  137. [RequiresDynamicCode ("AOT")]
  138. public IApplication Run<TRunnable> (Func<Exception, bool>? errorHandler = null, string? driverName = null)
  139. where TRunnable : IRunnable, new()
  140. {
  141. if (!Initialized)
  142. {
  143. // Init() has NOT been called. Auto-initialize as per interface contract.
  144. Init (driverName);
  145. }
  146. if (Driver is null)
  147. {
  148. throw new InvalidOperationException (@"Driver is null after Init.");
  149. }
  150. TRunnable runnable = new ();
  151. object? result = Run (runnable, errorHandler);
  152. // We created the runnable, so dispose it if it's disposable
  153. if (runnable is IDisposable disposable)
  154. {
  155. disposable.Dispose ();
  156. }
  157. return this;
  158. }
  159. /// <inheritdoc/>
  160. public object? Run (IRunnable runnable, Func<Exception, bool>? errorHandler = null)
  161. {
  162. ArgumentNullException.ThrowIfNull (runnable);
  163. if (!Initialized)
  164. {
  165. throw new NotInitializedException (@"Init must be called before Run.");
  166. }
  167. // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged)
  168. SessionToken? token;
  169. if (runnable.IsRunning)
  170. {
  171. // Find it on the stack
  172. token = SessionStack?.FirstOrDefault (st => st.Runnable == runnable);
  173. }
  174. else
  175. {
  176. token = Begin (runnable);
  177. }
  178. if (token is null)
  179. {
  180. Logging.Trace (@"Run - Begin session failed or was cancelled.");
  181. return null;
  182. }
  183. try
  184. {
  185. // All runnables block until RequestStop() is called
  186. RunLoop (runnable, errorHandler);
  187. }
  188. finally
  189. {
  190. // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
  191. End (token);
  192. }
  193. return token.Result;
  194. }
  195. private void RunLoop (IRunnable runnable, Func<Exception, bool>? errorHandler)
  196. {
  197. runnable.StopRequested = false;
  198. // Main loop - blocks until RequestStop() is called
  199. // Note: IsRunning is now a cached property, safe to check each iteration
  200. var firstIteration = true;
  201. while (runnable is { StopRequested: false, IsRunning: true })
  202. {
  203. if (Coordinator is null)
  204. {
  205. throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
  206. }
  207. try
  208. {
  209. // Process one iteration of the event loop
  210. Coordinator.RunIteration ();
  211. }
  212. catch (Exception ex)
  213. {
  214. if (errorHandler is null || !errorHandler (ex))
  215. {
  216. throw;
  217. }
  218. }
  219. if (StopAfterFirstIteration && firstIteration)
  220. {
  221. Logging.Information ("Run - Stopping after first iteration as requested");
  222. RequestStop (runnable);
  223. }
  224. firstIteration = false;
  225. }
  226. }
  227. #endregion Session Lifecycle - Run
  228. #region Session Lifecycle - End
  229. /// <inheritdoc/>
  230. public void End (SessionToken token)
  231. {
  232. ArgumentNullException.ThrowIfNull (token);
  233. if (token.Runnable is null)
  234. {
  235. return; // Already ended
  236. }
  237. // TODO: Move Poppover to utilize IRunnable arch; Get all refs to anyting
  238. // TODO: View-related out of ApplicationImpl.
  239. if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
  240. {
  241. ApplicationPopover.HideWithQuitCommand (visiblePopover);
  242. }
  243. IRunnable runnable = token.Runnable;
  244. // Get old IsRunning value (safe - cached value)
  245. bool oldIsRunning = runnable.IsRunning;
  246. // Raise IsRunningChanging OUTSIDE lock (true -> false) - can be canceled
  247. // This is where Result should be extracted!
  248. if (runnable.RaiseIsRunningChanging (oldIsRunning, false))
  249. {
  250. // Stopping was canceled - do not proceed with End
  251. return;
  252. }
  253. bool wasModal = runnable.IsModal;
  254. IRunnable? previousRunnable = null;
  255. // CRITICAL SECTION - Atomic stack + cached state update
  256. lock (_sessionStackLock)
  257. {
  258. // Pop token from SessionStack
  259. if (wasModal && SessionStack?.TryPop (out SessionToken? popped) == true && popped == token)
  260. {
  261. // Restore previous top runnable
  262. if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { })
  263. {
  264. previousRunnable = previousToken.Runnable;
  265. // Previous runnable becomes modal again
  266. previousRunnable.SetIsModal (true);
  267. }
  268. }
  269. // Update cached state atomically - IsRunning and IsModal are now consistent
  270. runnable.SetIsRunning (false);
  271. runnable.SetIsModal (false);
  272. }
  273. // END CRITICAL SECTION - IsRunning/IsModal now thread-safe
  274. // Fire events AFTER lock released
  275. if (wasModal)
  276. {
  277. runnable.RaiseIsModalChangedEvent (false);
  278. }
  279. TopRunnable = null;
  280. if (previousRunnable != null)
  281. {
  282. TopRunnable = previousRunnable;
  283. previousRunnable.RaiseIsModalChangedEvent (true);
  284. }
  285. runnable.RaiseIsRunningChangedEvent (false);
  286. token.Result = runnable.Result;
  287. _result = token.Result;
  288. // Clear the Runnable from the token
  289. token.Runnable = null;
  290. SessionEnded?.Invoke (this, new (token));
  291. }
  292. #endregion Session Lifecycle - End
  293. #region Session Lifecycle - RequestStop
  294. /// <inheritdoc/>
  295. public void RequestStop () { RequestStop (null); }
  296. /// <inheritdoc/>
  297. public void RequestStop (IRunnable? runnable)
  298. {
  299. // Get the runnable to stop
  300. if (runnable is null)
  301. {
  302. // Try to get from TopRunnable
  303. if (TopRunnableView is IRunnable r)
  304. {
  305. runnable = r;
  306. }
  307. else
  308. {
  309. return;
  310. }
  311. }
  312. runnable.StopRequested = true;
  313. // Note: The End() method will be called from the finally block in Run()
  314. // and that's where IsRunningChanging/IsRunningChanged will be raised
  315. }
  316. #endregion Session Lifecycle - RequestStop
  317. }