ApplicationImpl.Lifecycle.cs 13 KB

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