MainLoopCoordinator.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. using System.Collections.Concurrent;
  2. using Terminal.Gui.Drivers;
  3. using Microsoft.Extensions.Logging;
  4. namespace Terminal.Gui.App;
  5. /// <summary>
  6. /// <para>
  7. /// Coordinates the creation and startup of the main UI loop and input thread.
  8. /// </para>
  9. /// <para>
  10. /// This class bootstraps the <see cref="ApplicationMainLoop{T}"/> that handles
  11. /// UI layout, drawing, and event processing while also managing a separate thread
  12. /// for reading console input asynchronously.
  13. /// </para>
  14. /// <para>This class is designed to be managed by <see cref="ApplicationImpl"/></para>
  15. /// </summary>
  16. /// <typeparam name="T">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
  17. internal class MainLoopCoordinator<T> : IMainLoopCoordinator
  18. {
  19. private readonly ConcurrentQueue<T> _inputBuffer;
  20. private readonly IInputProcessor _inputProcessor;
  21. private readonly IApplicationMainLoop<T> _loop;
  22. private readonly IComponentFactory<T> _componentFactory;
  23. private readonly CancellationTokenSource _tokenSource = new ();
  24. private IConsoleInput<T> _input;
  25. private IConsoleOutput _output;
  26. private readonly object _oLockInitialization = new ();
  27. private ConsoleDriverFacade<T> _facade;
  28. private Task _inputTask;
  29. private readonly ITimedEvents _timedEvents;
  30. private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
  31. /// <summary>
  32. /// Creates a new coordinator that will manage the main UI loop and input thread.
  33. /// </summary>
  34. /// <param name="timedEvents">Handles scheduling and execution of user timeout callbacks</param>
  35. /// <param name="inputBuffer">Thread-safe queue for buffering raw console input</param>
  36. /// <param name="loop">The main application loop instance</param>
  37. /// <param name="componentFactory">Factory for creating driver-specific components (input, output, etc.)</param>
  38. public MainLoopCoordinator (
  39. ITimedEvents timedEvents,
  40. ConcurrentQueue<T> inputBuffer,
  41. IApplicationMainLoop<T> loop,
  42. IComponentFactory<T> componentFactory
  43. )
  44. {
  45. _timedEvents = timedEvents;
  46. _inputBuffer = inputBuffer;
  47. _inputProcessor = componentFactory.CreateInputProcessor (_inputBuffer);
  48. _loop = loop;
  49. _componentFactory = componentFactory;
  50. }
  51. /// <summary>
  52. /// Starts the input loop thread in separate task (returning immediately).
  53. /// </summary>
  54. public async Task StartAsync ()
  55. {
  56. Logging.Logger.LogInformation ("Main Loop Coordinator booting...");
  57. _inputTask = Task.Run (RunInput);
  58. // Main loop is now booted on same thread as rest of users application
  59. BootMainLoop ();
  60. // Wait asynchronously for the semaphore or task failure.
  61. Task waitForSemaphore = _startupSemaphore.WaitAsync ();
  62. // Wait for either the semaphore to be released or the input task to crash.
  63. // ReSharper disable once UseConfigureAwaitFalse
  64. Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask);
  65. // Check if the task was the input task and if it has failed.
  66. if (completedTask == _inputTask)
  67. {
  68. if (_inputTask.IsFaulted)
  69. {
  70. throw _inputTask.Exception;
  71. }
  72. Logging.Logger.LogCritical("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
  73. }
  74. Logging.Logger.LogInformation ("Main Loop Coordinator booting complete");
  75. }
  76. private void RunInput ()
  77. {
  78. try
  79. {
  80. lock (_oLockInitialization)
  81. {
  82. // Instance must be constructed on the thread in which it is used.
  83. _input = _componentFactory.CreateInput ();
  84. _input.Initialize (_inputBuffer);
  85. BuildFacadeIfPossible ();
  86. }
  87. try
  88. {
  89. _input.Run (_tokenSource.Token);
  90. }
  91. catch (OperationCanceledException)
  92. { }
  93. _input.Dispose ();
  94. }
  95. catch (Exception e)
  96. {
  97. Logging.Logger.LogCritical (e, "Input loop crashed");
  98. throw;
  99. }
  100. if (_stopCalled)
  101. {
  102. Logging.Logger.LogInformation ("Input loop exited cleanly");
  103. }
  104. else
  105. {
  106. Logging.Logger.LogCritical ("Input loop exited early (stop not called)");
  107. }
  108. }
  109. /// <inheritdoc/>
  110. public void RunIteration () { _loop.Iteration (); }
  111. private void BootMainLoop ()
  112. {
  113. lock (_oLockInitialization)
  114. {
  115. // Instance must be constructed on the thread in which it is used.
  116. _output = _componentFactory.CreateOutput ();
  117. _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output,_componentFactory);
  118. BuildFacadeIfPossible ();
  119. }
  120. }
  121. private void BuildFacadeIfPossible ()
  122. {
  123. if (_input != null && _output != null)
  124. {
  125. _facade = new (
  126. _inputProcessor,
  127. _loop.OutputBuffer,
  128. _output,
  129. _loop.AnsiRequestScheduler,
  130. _loop.ConsoleSizeMonitor);
  131. Application.Driver = _facade;
  132. _startupSemaphore.Release ();
  133. }
  134. }
  135. private bool _stopCalled;
  136. /// <inheritdoc/>
  137. public void Stop ()
  138. {
  139. // Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
  140. if (_stopCalled)
  141. {
  142. return;
  143. }
  144. _stopCalled = true;
  145. _tokenSource.Cancel ();
  146. _output.Dispose ();
  147. // Wait for input infinite loop to exit
  148. _inputTask.Wait ();
  149. }
  150. }