MainLoopCoordinator.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. using System.Collections.Concurrent;
  2. namespace Terminal.Gui.App;
  3. /// <summary>
  4. /// <para>
  5. /// Coordinates the creation and startup of the main UI loop and input thread.
  6. /// </para>
  7. /// <para>
  8. /// This class bootstraps the <see cref="ApplicationMainLoop{T}"/> that handles
  9. /// UI layout, drawing, and event processing while also managing a separate thread
  10. /// for reading console input asynchronously.
  11. /// </para>
  12. /// <para>This class is designed to be managed by <see cref="ApplicationImpl"/></para>
  13. /// </summary>
  14. /// <typeparam name="TInputRecord">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
  15. internal class MainLoopCoordinator<TInputRecord> : IMainLoopCoordinator where TInputRecord : struct
  16. {
  17. /// <summary>
  18. /// Creates a new coordinator that will manage the main UI loop and input thread.
  19. /// </summary>
  20. /// <param name="timedEvents">Handles scheduling and execution of user timeout callbacks</param>
  21. /// <param name="inputQueue">Thread-safe queue for buffering raw console input</param>
  22. /// <param name="loop">The main application loop instance</param>
  23. /// <param name="componentFactory">Factory for creating driver-specific components (input, output, etc.)</param>
  24. public MainLoopCoordinator (
  25. ITimedEvents timedEvents,
  26. ConcurrentQueue<TInputRecord> inputQueue,
  27. IApplicationMainLoop<TInputRecord> loop,
  28. IComponentFactory<TInputRecord> componentFactory
  29. )
  30. {
  31. _timedEvents = timedEvents;
  32. _inputQueue = inputQueue;
  33. _inputProcessor = componentFactory.CreateInputProcessor (_inputQueue);
  34. _loop = loop;
  35. _componentFactory = componentFactory;
  36. }
  37. private readonly IApplicationMainLoop<TInputRecord> _loop;
  38. private readonly IComponentFactory<TInputRecord> _componentFactory;
  39. private readonly CancellationTokenSource _runCancellationTokenSource = new ();
  40. private readonly ConcurrentQueue<TInputRecord> _inputQueue;
  41. private readonly IInputProcessor _inputProcessor;
  42. private readonly object _oLockInitialization = new ();
  43. private readonly ITimedEvents _timedEvents;
  44. private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
  45. private IInput<TInputRecord>? _input;
  46. private Task? _inputTask;
  47. private IOutput? _output;
  48. private DriverImpl? _driver;
  49. private bool _stopCalled;
  50. /// <summary>
  51. /// Starts the input loop thread in separate task (returning immediately).
  52. /// </summary>
  53. /// <param name="app">The <see cref="IApplication"/> instance that is running the input loop.</param>
  54. public async Task StartInputTaskAsync (IApplication? app)
  55. {
  56. Logging.Trace ("Booting... ()");
  57. _inputTask = Task.Run (() => RunInput (app));
  58. // Main loop is now booted on same thread as rest of users application
  59. BootMainLoop (app);
  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.Critical ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
  73. }
  74. Logging.Trace ("Booting complete");
  75. }
  76. /// <inheritdoc/>
  77. public void RunIteration ()
  78. {
  79. lock (_oLockInitialization)
  80. {
  81. _loop.Iteration ();
  82. }
  83. }
  84. /// <inheritdoc/>
  85. public void Stop ()
  86. {
  87. // Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
  88. if (_stopCalled)
  89. {
  90. return;
  91. }
  92. _stopCalled = true;
  93. _runCancellationTokenSource.Cancel ();
  94. _output?.Dispose ();
  95. // Wait for input infinite loop to exit
  96. _inputTask?.Wait ();
  97. }
  98. private void BootMainLoop (IApplication? app)
  99. {
  100. //Logging.Trace ($"_inputProcessor: {_inputProcessor}, _output: {_output}, _componentFactory: {_componentFactory}");
  101. lock (_oLockInitialization)
  102. {
  103. // Instance must be constructed on the thread in which it is used.
  104. _output = _componentFactory.CreateOutput ();
  105. _loop.Initialize (_timedEvents, _inputQueue, _inputProcessor, _output, _componentFactory, app);
  106. BuildDriverIfPossible (app);
  107. }
  108. }
  109. private void BuildDriverIfPossible (IApplication? app)
  110. {
  111. if (_input != null && _output != null)
  112. {
  113. _driver = new (
  114. _inputProcessor,
  115. _loop.OutputBuffer,
  116. _output,
  117. _loop.AnsiRequestScheduler,
  118. _loop.SizeMonitor);
  119. app!.Driver = _driver;
  120. _startupSemaphore.Release ();
  121. Logging.Trace ($"Driver: _input: {_input}, _output: {_output}");
  122. }
  123. }
  124. /// <summary>
  125. /// INTERNAL: Runs the IInput read loop on a new thread called the "Input Thread".
  126. /// </summary>
  127. /// <param name="app"></param>
  128. private void RunInput (IApplication? app)
  129. {
  130. try
  131. {
  132. lock (_oLockInitialization)
  133. {
  134. // Instance must be constructed on the thread in which it is used.
  135. _input = _componentFactory.CreateInput ();
  136. _input.Initialize (_inputQueue);
  137. // Wire up InputImpl reference for ITestableInput support
  138. if (_inputProcessor is InputProcessorImpl<TInputRecord> impl)
  139. {
  140. impl.InputImpl = _input;
  141. }
  142. BuildDriverIfPossible (app);
  143. }
  144. try
  145. {
  146. _input.Run (_runCancellationTokenSource.Token);
  147. }
  148. catch (OperationCanceledException)
  149. { }
  150. _input.Dispose ();
  151. }
  152. catch (Exception e)
  153. {
  154. Logging.Critical ($"Input loop crashed: {e}");
  155. throw;
  156. }
  157. if (_stopCalled)
  158. {
  159. Logging.Information ("Input loop exited cleanly");
  160. }
  161. else
  162. {
  163. Logging.Critical ("Input loop exited early (stop not called)");
  164. }
  165. }
  166. }