ApplicationImpl.Lifecycle.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. using System.Diagnostics;
  2. using System.Diagnostics.CodeAnalysis;
  3. namespace Terminal.Gui.App;
  4. internal partial class ApplicationImpl
  5. {
  6. /// <inheritdoc/>
  7. public int? MainThreadId { get; set; }
  8. /// <inheritdoc/>
  9. public bool Initialized { get; set; }
  10. /// <inheritdoc/>
  11. public event EventHandler<EventArgs<bool>>? InitializedChanged;
  12. /// <inheritdoc/>
  13. [RequiresUnreferencedCode ("AOT")]
  14. [RequiresDynamicCode ("AOT")]
  15. public IApplication Init (string? driverName = null)
  16. {
  17. if (Initialized)
  18. {
  19. Logging.Error ("Init called multiple times without shutdown, aborting.");
  20. throw new InvalidOperationException ("Init called multiple times without Shutdown");
  21. }
  22. // Thread-safe fence check: Ensure we're not mixing application models
  23. // Use lock to make check-and-set atomic
  24. lock (_modelUsageLock)
  25. {
  26. // If this is a legacy static instance and instance-based model was used, throw
  27. if (this == _instance && ModelUsage == ApplicationModelUsage.InstanceBased)
  28. {
  29. throw new InvalidOperationException (ERROR_LEGACY_AFTER_MODERN);
  30. }
  31. // If this is an instance-based instance and legacy static model was used, throw
  32. if (this != _instance && ModelUsage == ApplicationModelUsage.LegacyStatic)
  33. {
  34. throw new InvalidOperationException (ERROR_MODERN_AFTER_LEGACY);
  35. }
  36. // If no model has been set yet, set it now based on which instance this is
  37. if (ModelUsage == ApplicationModelUsage.None)
  38. {
  39. ModelUsage = this == _instance ? ApplicationModelUsage.LegacyStatic : ApplicationModelUsage.InstanceBased;
  40. }
  41. }
  42. if (!string.IsNullOrWhiteSpace (driverName))
  43. {
  44. _driverName = driverName;
  45. }
  46. if (string.IsNullOrWhiteSpace (_driverName))
  47. {
  48. _driverName = ForceDriver;
  49. }
  50. // Debug.Assert (Navigation is null);
  51. // Navigation = new ();
  52. //Debug.Assert (Popover is null);
  53. //Popover = new ();
  54. // Preserve existing keyboard settings if they exist
  55. bool hasExistingKeyboard = _keyboard is { };
  56. Key existingQuitKey = _keyboard?.QuitKey ?? Application.QuitKey;
  57. Key existingArrangeKey = _keyboard?.ArrangeKey ?? Application.ArrangeKey;
  58. Key existingNextTabKey = _keyboard?.NextTabKey ?? Application.NextTabKey;
  59. Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Application.PrevTabKey;
  60. Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Application.NextTabGroupKey;
  61. Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Application.PrevTabGroupKey;
  62. // Reset keyboard to ensure fresh state with default bindings
  63. _keyboard = new KeyboardImpl { App = this };
  64. // Sync keys from Application static properties (or existing keyboard if it had custom values)
  65. // This ensures we respect any Application.QuitKey etc changes made before Init()
  66. _keyboard.QuitKey = existingQuitKey;
  67. _keyboard.ArrangeKey = existingArrangeKey;
  68. _keyboard.NextTabKey = existingNextTabKey;
  69. _keyboard.PrevTabKey = existingPrevTabKey;
  70. _keyboard.NextTabGroupKey = existingNextTabGroupKey;
  71. _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
  72. CreateDriver (_driverName);
  73. Initialized = true;
  74. RaiseInitializedChanged (this, new (true));
  75. SubscribeDriverEvents ();
  76. SynchronizationContext.SetSynchronizationContext (new ());
  77. MainThreadId = Thread.CurrentThread.ManagedThreadId;
  78. _result = null;
  79. return this;
  80. }
  81. #region IDisposable Implementation
  82. private bool _disposed;
  83. /// <summary>
  84. /// Disposes the application instance and releases all resources.
  85. /// </summary>
  86. /// <remarks>
  87. /// <para>
  88. /// This method implements the <see cref="IDisposable"/> pattern and performs the same cleanup
  89. /// as <see cref="IDisposable.Dispose"/>, but without returning a result.
  90. /// </para>
  91. /// <para>
  92. /// After calling <see cref="Dispose()"/>, use <see cref="GetResult"/> or <see cref="IApplication.GetResult{T}"/>
  93. /// to retrieve the result from the last run session.
  94. /// </para>
  95. /// </remarks>
  96. public void Dispose ()
  97. {
  98. Dispose (true);
  99. GC.SuppressFinalize (this);
  100. }
  101. /// <summary>
  102. /// Disposes the application instance and releases all resources.
  103. /// </summary>
  104. /// <param name="disposing">
  105. /// <see langword="true"/> if called from <see cref="Dispose()"/>;
  106. /// <see langword="false"/> if called from finalizer.
  107. /// </param>
  108. protected virtual void Dispose (bool disposing)
  109. {
  110. if (_disposed)
  111. {
  112. return;
  113. }
  114. if (disposing)
  115. {
  116. // Dispose managed resources
  117. DisposeCore ();
  118. }
  119. // For the singleton instance (legacy Application.Init/Shutdown pattern),
  120. // we need to allow re-initialization after disposal. This enables:
  121. // Application.Init() -> Application.Shutdown() -> Application.Init()
  122. // For modern instance-based usage, this doesn't matter as new instances are created.
  123. if (this == _instance)
  124. {
  125. // Reset disposed flag to allow re-initialization
  126. _disposed = false;
  127. }
  128. else
  129. {
  130. // For instance-based usage, mark as disposed
  131. _disposed = true;
  132. }
  133. }
  134. /// <summary>
  135. /// Core disposal logic - same as Shutdown() but without returning result.
  136. /// </summary>
  137. private void DisposeCore ()
  138. {
  139. // Stop the coordinator if running
  140. Coordinator?.Stop ();
  141. // Capture state before cleanup
  142. bool wasInitialized = Initialized;
  143. #if DEBUG
  144. // Check that all Application events have no remaining subscribers BEFORE clearing them
  145. // Only check if we were actually initialized
  146. if (wasInitialized)
  147. {
  148. AssertNoEventSubscribers (nameof (Iteration), Iteration);
  149. AssertNoEventSubscribers (nameof (SessionBegun), SessionBegun);
  150. AssertNoEventSubscribers (nameof (SessionEnded), SessionEnded);
  151. AssertNoEventSubscribers (nameof (ScreenChanged), ScreenChanged);
  152. //AssertNoEventSubscribers (nameof (InitializedChanged), InitializedChanged);
  153. }
  154. #endif
  155. // Clean up all application state (including sync context)
  156. // ResetState handles the case where Initialized is false
  157. ResetState ();
  158. // Configuration manager diagnostics
  159. ConfigurationManager.PrintJsonErrors ();
  160. // Raise the initialized changed event to notify shutdown
  161. if (wasInitialized)
  162. {
  163. bool init = Initialized; // Will be false after ResetState
  164. RaiseInitializedChanged (this, new (in init));
  165. }
  166. // Clear the event to prevent memory leaks
  167. InitializedChanged = null;
  168. }
  169. #endregion IDisposable Implementation
  170. /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
  171. [Obsolete ("Use Dispose() or a using statement instead. This method will be removed in a future version.")]
  172. public object? Shutdown ()
  173. {
  174. // Shutdown is now just a wrapper around Dispose that returns the result
  175. object? result = GetResult ();
  176. Dispose ();
  177. return result;
  178. }
  179. private object? _result;
  180. /// <inheritdoc/>
  181. public object? GetResult () => _result;
  182. /// <inheritdoc/>
  183. public void ResetState (bool ignoreDisposed = false)
  184. {
  185. // Shutdown is the bookend for Init. As such it needs to clean up all resources
  186. // Init created. Apps that do any threading will need to code defensively for this.
  187. // e.g. see Issue #537
  188. // === 0. Stop all timers ===
  189. TimedEvents?.StopAll ();
  190. // === 1. Stop all running runnables ===
  191. foreach (SessionToken token in SessionStack!.Reverse ())
  192. {
  193. if (token.Runnable is { })
  194. {
  195. End (token);
  196. }
  197. }
  198. // === 2. Close and dispose popover ===
  199. if (Popover?.GetActivePopover () is View popover)
  200. {
  201. // This forcefully closes the popover; invoking Command.Quit would be more graceful
  202. // but since this is shutdown, doing this is ok.
  203. popover.Visible = false;
  204. }
  205. // Any popovers added to Popover have their lifetime controlled by Popover
  206. Popover?.Dispose ();
  207. Popover = null;
  208. // === 3. Clean up runnables ===
  209. SessionStack?.Clear ();
  210. #if DEBUG_IDISPOSABLE
  211. // Don't dispose the TopRunnable. It's up to caller dispose it
  212. if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && TopRunnableView is { })
  213. {
  214. Debug.Assert (TopRunnableView.WasDisposed, $"Title = {TopRunnableView.Title}, Id = {TopRunnableView.Id}");
  215. }
  216. #endif
  217. // === 4. Clean up driver ===
  218. if (Driver is { })
  219. {
  220. UnsubscribeDriverEvents ();
  221. Driver?.End ();
  222. Driver = null;
  223. }
  224. // === 5. Clear run state ===
  225. Iteration = null;
  226. SessionBegun = null;
  227. SessionEnded = null;
  228. StopAfterFirstIteration = false;
  229. ClearScreenNextIteration = false;
  230. // === 6. Reset input systems ===
  231. // Dispose keyboard and mouse to unsubscribe from events
  232. if (_keyboard is IDisposable keyboardDisposable)
  233. {
  234. keyboardDisposable.Dispose ();
  235. }
  236. if (_mouse is IDisposable mouseDisposable)
  237. {
  238. mouseDisposable.Dispose ();
  239. }
  240. // Mouse and Keyboard will be lazy-initialized on next access
  241. _mouse = null;
  242. _keyboard = null;
  243. Mouse.ResetState ();
  244. // === 7. Clear navigation and screen state ===
  245. ScreenChanged = null;
  246. //Navigation = null;
  247. // === 8. Reset initialization state ===
  248. Initialized = false;
  249. MainThreadId = null;
  250. // === 9. Clear graphics ===
  251. Sixel.Clear ();
  252. // === 10. Reset ForceDriver ===
  253. // Note: ForceDriver and Force16Colors are reset
  254. // If they need to persist across Init/Shutdown cycles
  255. // then the user of the library should manage that state
  256. Terminal.Gui.Drivers.Driver.Force16Colors = false;
  257. ForceDriver = string.Empty;
  258. // === 11. Reset synchronization context ===
  259. // IMPORTANT: Always reset sync context, even if not initialized
  260. // This ensures cleanup works correctly even if Shutdown is called without Init
  261. // Reset synchronization context to allow the user to run async/await,
  262. // as the main loop has been ended, the synchronization context from
  263. // gui.cs does no longer process any callbacks. See #1084 for more details:
  264. // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
  265. SynchronizationContext.SetSynchronizationContext (null);
  266. // === 12. Unsubscribe from Application static property change events ===
  267. UnsubscribeApplicationEvents ();
  268. }
  269. /// <summary>
  270. /// Raises the <see cref="InitializedChanged"/> event.
  271. /// </summary>
  272. internal void RaiseInitializedChanged (object sender, EventArgs<bool> e) { InitializedChanged?.Invoke (sender, e); }
  273. #if DEBUG
  274. /// <summary>
  275. /// DEBUG ONLY: Asserts that an event has no remaining subscribers.
  276. /// </summary>
  277. /// <param name="eventName">The name of the event for diagnostic purposes.</param>
  278. /// <param name="eventDelegate">The event delegate to check.</param>
  279. private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate)
  280. {
  281. if (eventDelegate is null)
  282. {
  283. return;
  284. }
  285. Delegate [] subscribers = eventDelegate.GetInvocationList ();
  286. if (subscribers.Length > 0)
  287. {
  288. string subscriberInfo = string.Join (
  289. ", ",
  290. subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}"
  291. )
  292. );
  293. Debug.Fail (
  294. $"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}"
  295. );
  296. }
  297. }
  298. #endif
  299. // Event handlers for Application static property changes
  300. private void OnForceDriverChanged (object? sender, ValueChangedEventArgs<string> e) { ForceDriver = e.NewValue; }
  301. /// <summary>
  302. /// Unsubscribes from Application static property change events.
  303. /// </summary>
  304. private void UnsubscribeApplicationEvents ()
  305. {
  306. Application.ForceDriverChanged -= OnForceDriverChanged;
  307. }
  308. }