ApplicationImpl.Run.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. using System.Collections.Concurrent;
  2. using System.Diagnostics.CodeAnalysis;
  3. namespace Terminal.Gui.App;
  4. internal 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. //RaiseIteration ();
  131. LayoutAndDraw ();
  132. return token;
  133. }
  134. #endregion Session Lifecycle - Begin
  135. #region Session Lifecycle - Run
  136. /// <inheritdoc/>
  137. [RequiresUnreferencedCode ("AOT")]
  138. [RequiresDynamicCode ("AOT")]
  139. public IApplication Run<TRunnable> (Func<Exception, bool>? errorHandler = null, string? driverName = null)
  140. where TRunnable : IRunnable, new()
  141. {
  142. if (!Initialized)
  143. {
  144. // Init() has NOT been called. Auto-initialize as per interface contract.
  145. Init (driverName);
  146. }
  147. if (Driver is null)
  148. {
  149. throw new InvalidOperationException (@"Driver is null after Init.");
  150. }
  151. TRunnable runnable = new ();
  152. object? result = Run (runnable, errorHandler);
  153. // We created the runnable, so dispose it if it's disposable
  154. if (runnable is IDisposable disposable)
  155. {
  156. disposable.Dispose ();
  157. }
  158. return this;
  159. }
  160. /// <inheritdoc/>
  161. public object? Run (IRunnable runnable, Func<Exception, bool>? errorHandler = null)
  162. {
  163. ArgumentNullException.ThrowIfNull (runnable);
  164. if (!Initialized)
  165. {
  166. throw new NotInitializedException (@"Init must be called before Run.");
  167. }
  168. // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged)
  169. SessionToken? token;
  170. if (runnable.IsRunning)
  171. {
  172. // Find it on the stack
  173. token = SessionStack?.FirstOrDefault (st => st.Runnable == runnable);
  174. }
  175. else
  176. {
  177. token = Begin (runnable);
  178. }
  179. if (token is null)
  180. {
  181. Logging.Trace (@"Run - Begin session failed or was cancelled.");
  182. return null;
  183. }
  184. try
  185. {
  186. // All runnables block until RequestStop() is called
  187. RunLoop (runnable, errorHandler);
  188. }
  189. finally
  190. {
  191. // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack)
  192. End (token);
  193. }
  194. return token.Result;
  195. }
  196. private void RunLoop (IRunnable runnable, Func<Exception, bool>? errorHandler)
  197. {
  198. runnable.StopRequested = false;
  199. // Main loop - blocks until RequestStop() is called
  200. // Note: IsRunning is now a cached property, safe to check each iteration
  201. var firstIteration = true;
  202. while (runnable is { StopRequested: false, IsRunning: true })
  203. {
  204. if (Coordinator is null)
  205. {
  206. throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
  207. }
  208. try
  209. {
  210. // Process one iteration of the event loop
  211. Coordinator.RunIteration ();
  212. }
  213. catch (Exception ex)
  214. {
  215. if (errorHandler is null || !errorHandler (ex))
  216. {
  217. throw;
  218. }
  219. }
  220. if (StopAfterFirstIteration && firstIteration)
  221. {
  222. Logging.Information ("Run - Stopping after first iteration as requested");
  223. RequestStop (runnable);
  224. }
  225. firstIteration = false;
  226. }
  227. }
  228. #endregion Session Lifecycle - Run
  229. #region Session Lifecycle - End
  230. /// <inheritdoc/>
  231. public void End (SessionToken token)
  232. {
  233. ArgumentNullException.ThrowIfNull (token);
  234. if (token.Runnable is null)
  235. {
  236. return; // Already ended
  237. }
  238. // TODO: Move Poppover to utilize IRunnable arch; Get all refs to anyting
  239. // TODO: View-related out of ApplicationImpl.
  240. if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
  241. {
  242. ApplicationPopover.HideWithQuitCommand (visiblePopover);
  243. }
  244. IRunnable runnable = token.Runnable;
  245. // Get old IsRunning value (safe - cached value)
  246. bool oldIsRunning = runnable.IsRunning;
  247. // Raise IsRunningChanging OUTSIDE lock (true -> false) - can be canceled
  248. // This is where Result should be extracted!
  249. if (runnable.RaiseIsRunningChanging (oldIsRunning, false))
  250. {
  251. // Stopping was canceled - do not proceed with End
  252. return;
  253. }
  254. bool wasModal = runnable.IsModal;
  255. IRunnable? previousRunnable = null;
  256. // CRITICAL SECTION - Atomic stack + cached state update
  257. lock (_sessionStackLock)
  258. {
  259. // Pop token from SessionStack
  260. if (wasModal && SessionStack?.TryPop (out SessionToken? popped) == true && popped == token)
  261. {
  262. // Restore previous top runnable
  263. if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { })
  264. {
  265. previousRunnable = previousToken.Runnable;
  266. // Previous runnable becomes modal again
  267. previousRunnable.SetIsModal (true);
  268. }
  269. }
  270. // Update cached state atomically - IsRunning and IsModal are now consistent
  271. runnable.SetIsRunning (false);
  272. runnable.SetIsModal (false);
  273. }
  274. // END CRITICAL SECTION - IsRunning/IsModal now thread-safe
  275. // Fire events AFTER lock released
  276. if (wasModal)
  277. {
  278. runnable.RaiseIsModalChangedEvent (false);
  279. }
  280. TopRunnable = null;
  281. if (previousRunnable != null)
  282. {
  283. TopRunnable = previousRunnable;
  284. previousRunnable.RaiseIsModalChangedEvent (true);
  285. }
  286. runnable.RaiseIsRunningChangedEvent (false);
  287. token.Result = runnable.Result;
  288. _result = token.Result;
  289. // Clear the Runnable from the token
  290. token.Runnable = null;
  291. SessionEnded?.Invoke (this, new (token));
  292. }
  293. #endregion Session Lifecycle - End
  294. #region Session Lifecycle - RequestStop
  295. /// <inheritdoc/>
  296. public void RequestStop () { RequestStop (null); }
  297. /// <inheritdoc/>
  298. public void RequestStop (IRunnable? runnable)
  299. {
  300. // Get the runnable to stop
  301. if (runnable is null)
  302. {
  303. // Try to get from TopRunnable
  304. if (TopRunnableView is IRunnable r)
  305. {
  306. runnable = r;
  307. }
  308. else
  309. {
  310. return;
  311. }
  312. }
  313. runnable.StopRequested = true;
  314. // Note: The End() method will be called from the finally block in Run()
  315. // and that's where IsRunningChanging/IsRunningChanged will be raised
  316. }
  317. #endregion Session Lifecycle - RequestStop
  318. }