ApplicationImpl.Lifecycle.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. using System.Diagnostics;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Reflection;
  4. using Terminal.Gui.Examples;
  5. namespace Terminal.Gui.App;
  6. internal partial class ApplicationImpl
  7. {
  8. /// <inheritdoc/>
  9. public int? MainThreadId { get; set; }
  10. /// <inheritdoc/>
  11. public bool Initialized { get; set; }
  12. /// <inheritdoc/>
  13. public event EventHandler<EventArgs<bool>>? InitializedChanged;
  14. /// <inheritdoc/>
  15. [RequiresUnreferencedCode ("AOT")]
  16. [RequiresDynamicCode ("AOT")]
  17. public IApplication Init (string? driverName = null)
  18. {
  19. if (Initialized)
  20. {
  21. Logging.Error ("Init called multiple times without shutdown, aborting.");
  22. throw new InvalidOperationException ("Init called multiple times without Shutdown");
  23. }
  24. // Thread-safe fence check: Ensure we're not mixing application models
  25. // Use lock to make check-and-set atomic
  26. lock (_modelUsageLock)
  27. {
  28. // If this is a legacy static instance and instance-based model was used, throw
  29. if (this == _instance && ModelUsage == ApplicationModelUsage.InstanceBased)
  30. {
  31. throw new InvalidOperationException (ERROR_LEGACY_AFTER_MODERN);
  32. }
  33. // If this is an instance-based instance and legacy static model was used, throw
  34. if (this != _instance && ModelUsage == ApplicationModelUsage.LegacyStatic)
  35. {
  36. throw new InvalidOperationException (ERROR_MODERN_AFTER_LEGACY);
  37. }
  38. // If no model has been set yet, set it now based on which instance this is
  39. if (ModelUsage == ApplicationModelUsage.None)
  40. {
  41. ModelUsage = this == _instance ? ApplicationModelUsage.LegacyStatic : ApplicationModelUsage.InstanceBased;
  42. }
  43. }
  44. if (!string.IsNullOrWhiteSpace (driverName))
  45. {
  46. _driverName = driverName;
  47. }
  48. if (string.IsNullOrWhiteSpace (_driverName))
  49. {
  50. _driverName = ForceDriver;
  51. }
  52. // Debug.Assert (Navigation is null);
  53. // Navigation = new ();
  54. //Debug.Assert (Popover is null);
  55. //Popover = new ();
  56. // Preserve existing keyboard settings if they exist
  57. bool hasExistingKeyboard = _keyboard is { };
  58. Key existingQuitKey = _keyboard?.QuitKey ?? Application.QuitKey;
  59. Key existingArrangeKey = _keyboard?.ArrangeKey ?? Application.ArrangeKey;
  60. Key existingNextTabKey = _keyboard?.NextTabKey ?? Application.NextTabKey;
  61. Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Application.PrevTabKey;
  62. Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Application.NextTabGroupKey;
  63. Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Application.PrevTabGroupKey;
  64. // Reset keyboard to ensure fresh state with default bindings
  65. _keyboard = new KeyboardImpl { App = this };
  66. // Sync keys from Application static properties (or existing keyboard if it had custom values)
  67. // This ensures we respect any Application.QuitKey etc changes made before Init()
  68. _keyboard.QuitKey = existingQuitKey;
  69. _keyboard.ArrangeKey = existingArrangeKey;
  70. _keyboard.NextTabKey = existingNextTabKey;
  71. _keyboard.PrevTabKey = existingPrevTabKey;
  72. _keyboard.NextTabGroupKey = existingNextTabGroupKey;
  73. _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
  74. CreateDriver (_driverName);
  75. Screen = Driver!.Screen;
  76. Initialized = true;
  77. RaiseInitializedChanged (this, new (true));
  78. SubscribeDriverEvents ();
  79. // Setup example mode if requested
  80. if (Application.Apps.Contains (this))
  81. {
  82. SetupExampleMode ();
  83. }
  84. SynchronizationContext.SetSynchronizationContext (new ());
  85. MainThreadId = Thread.CurrentThread.ManagedThreadId;
  86. _result = null;
  87. return this;
  88. }
  89. #region IDisposable Implementation
  90. private bool _disposed;
  91. /// <summary>
  92. /// Disposes the application instance and releases all resources.
  93. /// </summary>
  94. /// <remarks>
  95. /// <para>
  96. /// This method implements the <see cref="IDisposable"/> pattern and performs the same cleanup
  97. /// as <see cref="IDisposable.Dispose"/>, but without returning a result.
  98. /// </para>
  99. /// <para>
  100. /// After calling <see cref="Dispose()"/>, use <see cref="GetResult"/> or <see cref="IApplication.GetResult{T}"/>
  101. /// to retrieve the result from the last run session.
  102. /// </para>
  103. /// </remarks>
  104. public void Dispose ()
  105. {
  106. Dispose (true);
  107. GC.SuppressFinalize (this);
  108. }
  109. /// <summary>
  110. /// Disposes the application instance and releases all resources.
  111. /// </summary>
  112. /// <param name="disposing">
  113. /// <see langword="true"/> if called from <see cref="Dispose()"/>;
  114. /// <see langword="false"/> if called from finalizer.
  115. /// </param>
  116. protected virtual void Dispose (bool disposing)
  117. {
  118. if (_disposed)
  119. {
  120. return;
  121. }
  122. if (disposing)
  123. {
  124. // Dispose managed resources
  125. DisposeCore ();
  126. }
  127. // For the singleton instance (legacy Application.Init/Shutdown pattern),
  128. // we need to allow re-initialization after disposal. This enables:
  129. // Application.Init() -> Application.Shutdown() -> Application.Init()
  130. // For modern instance-based usage, this doesn't matter as new instances are created.
  131. if (this == _instance)
  132. {
  133. // Reset disposed flag to allow re-initialization
  134. _disposed = false;
  135. }
  136. else
  137. {
  138. // For instance-based usage, mark as disposed
  139. _disposed = true;
  140. }
  141. }
  142. /// <summary>
  143. /// Core disposal logic - same as Shutdown() but without returning result.
  144. /// </summary>
  145. private void DisposeCore ()
  146. {
  147. // Stop the coordinator if running
  148. Coordinator?.Stop ();
  149. // Capture state before cleanup
  150. bool wasInitialized = Initialized;
  151. #if DEBUG
  152. // Check that all Application events have no remaining subscribers BEFORE clearing them
  153. // Only check if we were actually initialized
  154. if (wasInitialized)
  155. {
  156. AssertNoEventSubscribers (nameof (Iteration), Iteration);
  157. AssertNoEventSubscribers (nameof (SessionBegun), SessionBegun);
  158. AssertNoEventSubscribers (nameof (SessionEnded), SessionEnded);
  159. AssertNoEventSubscribers (nameof (ScreenChanged), ScreenChanged);
  160. //AssertNoEventSubscribers (nameof (InitializedChanged), InitializedChanged);
  161. }
  162. #endif
  163. // Clean up all application state (including sync context)
  164. // ResetState handles the case where Initialized is false
  165. ResetState ();
  166. // Configuration manager diagnostics
  167. ConfigurationManager.PrintJsonErrors ();
  168. // Raise the initialized changed event to notify shutdown
  169. if (wasInitialized)
  170. {
  171. bool init = Initialized; // Will be false after ResetState
  172. RaiseInitializedChanged (this, new (in init));
  173. }
  174. // Clear the event to prevent memory leaks
  175. InitializedChanged = null;
  176. }
  177. #endregion IDisposable Implementation
  178. /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
  179. [Obsolete ("Use Dispose() or a using statement instead. This method will be removed in a future version.")]
  180. public object? Shutdown ()
  181. {
  182. // Shutdown is now just a wrapper around Dispose that returns the result
  183. object? result = GetResult ();
  184. Dispose ();
  185. return result;
  186. }
  187. private object? _result;
  188. /// <inheritdoc/>
  189. public object? GetResult () => _result;
  190. /// <inheritdoc/>
  191. public void ResetState (bool ignoreDisposed = false)
  192. {
  193. // Shutdown is the bookend for Init. As such it needs to clean up all resources
  194. // Init created. Apps that do any threading will need to code defensively for this.
  195. // e.g. see Issue #537
  196. // === 0. Stop all timers ===
  197. TimedEvents?.StopAll ();
  198. // === 1. Stop all running runnables ===
  199. foreach (SessionToken token in SessionStack!.Reverse ())
  200. {
  201. if (token.Runnable is { })
  202. {
  203. End (token);
  204. }
  205. }
  206. // === 2. Close and dispose popover ===
  207. if (Popover?.GetActivePopover () is View popover)
  208. {
  209. // This forcefully closes the popover; invoking Command.Quit would be more graceful
  210. // but since this is shutdown, doing this is ok.
  211. popover.Visible = false;
  212. }
  213. // Any popovers added to Popover have their lifetime controlled by Popover
  214. Popover?.Dispose ();
  215. Popover = null;
  216. // === 3. Clean up runnables ===
  217. SessionStack?.Clear ();
  218. #if DEBUG_IDISPOSABLE
  219. // Don't dispose the TopRunnable. It's up to caller dispose it
  220. if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && TopRunnableView is { })
  221. {
  222. Debug.Assert (TopRunnableView.WasDisposed, $"Title = {TopRunnableView.Title}, Id = {TopRunnableView.Id}");
  223. }
  224. #endif
  225. // === 4. Clean up driver ===
  226. if (Driver is { })
  227. {
  228. UnsubscribeDriverEvents ();
  229. Driver?.End ();
  230. Driver = null;
  231. }
  232. // Reset screen
  233. ResetScreen ();
  234. _screen = null;
  235. // === 5. Clear run state ===
  236. Iteration = null;
  237. SessionBegun = null;
  238. SessionEnded = null;
  239. StopAfterFirstIteration = false;
  240. ClearScreenNextIteration = false;
  241. // === 6. Reset input systems ===
  242. // Dispose keyboard and mouse to unsubscribe from events
  243. if (_keyboard is IDisposable keyboardDisposable)
  244. {
  245. keyboardDisposable.Dispose ();
  246. }
  247. if (_mouse is IDisposable mouseDisposable)
  248. {
  249. mouseDisposable.Dispose ();
  250. }
  251. // Mouse and Keyboard will be lazy-initialized on next access
  252. _mouse = null;
  253. _keyboard = null;
  254. Mouse.ResetState ();
  255. // === 7. Clear navigation and screen state ===
  256. ScreenChanged = null;
  257. //Navigation = null;
  258. // === 8. Reset initialization state ===
  259. Initialized = false;
  260. MainThreadId = null;
  261. // === 9. Clear graphics ===
  262. Sixel.Clear ();
  263. // === 10. Reset ForceDriver ===
  264. // Note: ForceDriver and Force16Colors are reset
  265. // If they need to persist across Init/Shutdown cycles
  266. // then the user of the library should manage that state
  267. Force16Colors = false;
  268. ForceDriver = string.Empty;
  269. // === 11. Reset synchronization context ===
  270. // IMPORTANT: Always reset sync context, even if not initialized
  271. // This ensures cleanup works correctly even if Shutdown is called without Init
  272. // Reset synchronization context to allow the user to run async/await,
  273. // as the main loop has been ended, the synchronization context from
  274. // gui.cs does no longer process any callbacks. See #1084 for more details:
  275. // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
  276. SynchronizationContext.SetSynchronizationContext (null);
  277. // === 12. Unsubscribe from Application static property change events ===
  278. UnsubscribeApplicationEvents ();
  279. }
  280. /// <summary>
  281. /// Raises the <see cref="InitializedChanged"/> event.
  282. /// </summary>
  283. internal void RaiseInitializedChanged (object sender, EventArgs<bool> e) { InitializedChanged?.Invoke (sender, e); }
  284. #if DEBUG
  285. /// <summary>
  286. /// DEBUG ONLY: Asserts that an event has no remaining subscribers.
  287. /// </summary>
  288. /// <param name="eventName">The name of the event for diagnostic purposes.</param>
  289. /// <param name="eventDelegate">The event delegate to check.</param>
  290. private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate)
  291. {
  292. if (eventDelegate is null)
  293. {
  294. return;
  295. }
  296. Delegate [] subscribers = eventDelegate.GetInvocationList ();
  297. if (subscribers.Length > 0)
  298. {
  299. string subscriberInfo = string.Join (
  300. ", ",
  301. subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}"
  302. )
  303. );
  304. Debug.Fail (
  305. $"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}"
  306. );
  307. }
  308. }
  309. #endif
  310. // Event handlers for Application static property changes
  311. private void OnForce16ColorsChanged (object? sender, ValueChangedEventArgs<bool> e) { Force16Colors = e.NewValue; }
  312. private void OnForceDriverChanged (object? sender, ValueChangedEventArgs<string> e) { ForceDriver = e.NewValue; }
  313. /// <summary>
  314. /// Unsubscribes from Application static property change events.
  315. /// </summary>
  316. private void UnsubscribeApplicationEvents ()
  317. {
  318. Application.Force16ColorsChanged -= OnForce16ColorsChanged;
  319. Application.ForceDriverChanged -= OnForceDriverChanged;
  320. }
  321. #region Example Mode
  322. private bool _exampleModeDemoKeysSent;
  323. /// <summary>
  324. /// Sets up example mode functionality - collecting metadata and sending demo keys
  325. /// when the first TopRunnable becomes modal.
  326. /// </summary>
  327. private void SetupExampleMode ()
  328. {
  329. if (Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME) is null)
  330. {
  331. return;
  332. }
  333. // Subscribe to SessionBegun to monitor when runnables start
  334. SessionBegun += OnSessionBegunForExample;
  335. }
  336. private void OnSessionBegunForExample (object? sender, SessionTokenEventArgs e)
  337. {
  338. // Only send demo keys once
  339. if (_exampleModeDemoKeysSent)
  340. {
  341. return;
  342. }
  343. // Subscribe to IsModalChanged event on the TopRunnable
  344. if (e.State.Runnable is Runnable { } runnable)
  345. {
  346. e.State.Runnable.IsModalChanged += OnIsModalChangedForExample;
  347. //// Check if already modal - if so, send keys immediately
  348. //if (e.State.Runnable.IsModal)
  349. //{
  350. // _exampleModeDemoKeysSent = true;
  351. // e.State.Runnable.IsModalChanged -= OnIsModalChangedForExample;
  352. // SendDemoKeys ();
  353. //}
  354. }
  355. // Unsubscribe from SessionBegun - we only need to set up the modal listener once
  356. SessionBegun -= OnSessionBegunForExample;
  357. }
  358. private void OnIsModalChangedForExample (object? sender, EventArgs<bool> e)
  359. {
  360. // Only send demo keys once, when a runnable becomes modal (not when it stops being modal)
  361. if (_exampleModeDemoKeysSent || !e.Value)
  362. {
  363. return;
  364. }
  365. // Mark that we've sent the keys
  366. _exampleModeDemoKeysSent = true;
  367. // Unsubscribe - we only need to do this once
  368. if (TopRunnable is { })
  369. {
  370. TopRunnable.IsModalChanged -= OnIsModalChangedForExample;
  371. }
  372. // Send demo keys from assembly attributes
  373. SendDemoKeys ();
  374. }
  375. private void SendDemoKeys ()
  376. {
  377. // Get the assembly of the currently running example
  378. // Use TopRunnable's type assembly instead of entry assembly
  379. // This works correctly when examples are loaded dynamically by ExampleRunner
  380. Assembly? assembly = TopRunnable?.GetType ().Assembly;
  381. if (assembly is null)
  382. {
  383. return;
  384. }
  385. // Look for ExampleDemoKeyStrokesAttribute
  386. List<ExampleDemoKeyStrokesAttribute> demoKeyAttributes = assembly.GetCustomAttributes (typeof (ExampleDemoKeyStrokesAttribute), false)
  387. .OfType<ExampleDemoKeyStrokesAttribute> ()
  388. .ToList ();
  389. if (!demoKeyAttributes.Any ())
  390. {
  391. return;
  392. }
  393. // Sort by Order and collect all keystrokes
  394. IOrderedEnumerable<ExampleDemoKeyStrokesAttribute> sortedSequences = demoKeyAttributes.OrderBy (a => a.Order);
  395. // Default delay between keys is 100ms
  396. int currentDelay = 100;
  397. // Track cumulative timeout for scheduling
  398. int cumulativeTimeout = 0;
  399. foreach (ExampleDemoKeyStrokesAttribute attr in sortedSequences)
  400. {
  401. // Handle KeyStrokes array
  402. if (attr.KeyStrokes is not { Length: > 0 })
  403. {
  404. continue;
  405. }
  406. foreach (string keyStr in attr.KeyStrokes)
  407. {
  408. // Check for SetDelay command
  409. if (keyStr.StartsWith ("SetDelay:", StringComparison.OrdinalIgnoreCase))
  410. {
  411. string delayValue = keyStr.Substring ("SetDelay:".Length);
  412. if (int.TryParse (delayValue, out int newDelay))
  413. {
  414. currentDelay = newDelay;
  415. }
  416. continue;
  417. }
  418. // Regular key
  419. if (Key.TryParse (keyStr, out Key? key))
  420. {
  421. cumulativeTimeout += currentDelay;
  422. // Capture key by value to avoid closure issues
  423. Key keyToSend = key;
  424. AddTimeout (TimeSpan.FromMilliseconds (cumulativeTimeout), () =>
  425. {
  426. Keyboard.RaiseKeyDownEvent (keyToSend);
  427. return false;
  428. });
  429. }
  430. }
  431. // Handle RepeatKey
  432. if (!string.IsNullOrEmpty (attr.RepeatKey))
  433. {
  434. if (Key.TryParse (attr.RepeatKey, out Key? key))
  435. {
  436. for (var i = 0; i < attr.RepeatCount; i++)
  437. {
  438. cumulativeTimeout += currentDelay;
  439. // Capture key by value to avoid closure issues
  440. Key keyToSend = key;
  441. AddTimeout (TimeSpan.FromMilliseconds (cumulativeTimeout), () =>
  442. {
  443. Keyboard.RaiseKeyDownEvent (keyToSend);
  444. return false;
  445. });
  446. }
  447. }
  448. }
  449. }
  450. }
  451. #endregion Example Mode
  452. }