ApplicationImpl.Lifecycle.cs 12 KB

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