MainLoopCoordinator.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. using System.Collections.Concurrent;
  2. using Microsoft.Extensions.Logging;
  3. namespace Terminal.Gui;
  4. /// <summary>
  5. /// <para>
  6. /// Handles creating the input loop thread and bootstrapping the
  7. /// <see cref="MainLoop{T}"/> that handles layout/drawing/events etc.
  8. /// </para>
  9. /// <para>This class is designed to be managed by <see cref="ApplicationV2"/></para>
  10. /// </summary>
  11. /// <typeparam name="T"></typeparam>
  12. internal class MainLoopCoordinator<T> : IMainLoopCoordinator
  13. {
  14. private readonly Func<IConsoleInput<T>> _inputFactory;
  15. private readonly ConcurrentQueue<T> _inputBuffer;
  16. private readonly IInputProcessor _inputProcessor;
  17. private readonly IMainLoop<T> _loop;
  18. private readonly CancellationTokenSource _tokenSource = new ();
  19. private readonly Func<IConsoleOutput> _outputFactory;
  20. private IConsoleInput<T> _input;
  21. private IConsoleOutput _output;
  22. private readonly object _oLockInitialization = new ();
  23. private ConsoleDriverFacade<T> _facade;
  24. private Task _inputTask;
  25. private readonly ITimedEvents _timedEvents;
  26. private readonly bool _isWindowsTerminal;
  27. private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
  28. /// <summary>
  29. /// Creates a new coordinator
  30. /// </summary>
  31. /// <param name="timedEvents"></param>
  32. /// <param name="inputFactory">
  33. /// Function to create a new input. This must call <see langword="new"/>
  34. /// explicitly and cannot return an existing instance. This requirement arises because Windows
  35. /// console screen buffer APIs are thread-specific for certain operations.
  36. /// </param>
  37. /// <param name="inputBuffer"></param>
  38. /// <param name="inputProcessor"></param>
  39. /// <param name="outputFactory">
  40. /// Function to create a new output. This must call <see langword="new"/>
  41. /// explicitly and cannot return an existing instance. This requirement arises because Windows
  42. /// console screen buffer APIs are thread-specific for certain operations.
  43. /// </param>
  44. /// <param name="loop"></param>
  45. public MainLoopCoordinator (
  46. ITimedEvents timedEvents,
  47. Func<IConsoleInput<T>> inputFactory,
  48. ConcurrentQueue<T> inputBuffer,
  49. IInputProcessor inputProcessor,
  50. Func<IConsoleOutput> outputFactory,
  51. IMainLoop<T> loop
  52. )
  53. {
  54. _timedEvents = timedEvents;
  55. _inputFactory = inputFactory;
  56. _inputBuffer = inputBuffer;
  57. _inputProcessor = inputProcessor;
  58. _outputFactory = outputFactory;
  59. _loop = loop;
  60. _isWindowsTerminal = Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null;
  61. }
  62. /// <summary>
  63. /// Starts the input loop thread in separate task (returning immediately).
  64. /// </summary>
  65. public async Task StartAsync ()
  66. {
  67. Logging.Logger.LogInformation ("Main Loop Coordinator booting...");
  68. _inputTask = Task.Run (RunInput);
  69. // Main loop is now booted on same thread as rest of users application
  70. BootMainLoop ();
  71. // Wait asynchronously for the semaphore or task failure.
  72. Task waitForSemaphore = _startupSemaphore.WaitAsync ();
  73. // Wait for either the semaphore to be released or the input task to crash.
  74. // ReSharper disable once UseConfigureAwaitFalse
  75. Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask);
  76. // Check if the task was the input task and if it has failed.
  77. if (completedTask == _inputTask)
  78. {
  79. if (_inputTask.IsFaulted)
  80. {
  81. throw _inputTask.Exception;
  82. }
  83. throw new ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
  84. }
  85. Logging.Logger.LogInformation ("Main Loop Coordinator booting complete");
  86. }
  87. private void RunInput ()
  88. {
  89. try
  90. {
  91. lock (_oLockInitialization)
  92. {
  93. // Instance must be constructed on the thread in which it is used.
  94. _input = _inputFactory.Invoke ();
  95. _input.Initialize (_inputBuffer);
  96. BuildFacadeIfPossible ();
  97. }
  98. try
  99. {
  100. _input.Run (_tokenSource.Token);
  101. }
  102. catch (OperationCanceledException)
  103. { }
  104. _input.Dispose ();
  105. }
  106. catch (Exception e)
  107. {
  108. Logging.Logger.LogCritical (e, "Input loop crashed");
  109. throw;
  110. }
  111. if (_stopCalled)
  112. {
  113. Logging.Logger.LogInformation ("Input loop exited cleanly");
  114. }
  115. else
  116. {
  117. Logging.Logger.LogCritical ("Input loop exited early (stop not called)");
  118. }
  119. }
  120. /// <inheritdoc/>
  121. public void RunIteration () { _loop.Iteration (); }
  122. private void BootMainLoop ()
  123. {
  124. lock (_oLockInitialization)
  125. {
  126. // Instance must be constructed on the thread in which it is used.
  127. _output = _outputFactory.Invoke ();
  128. _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output);
  129. BuildFacadeIfPossible ();
  130. }
  131. }
  132. private void BuildFacadeIfPossible ()
  133. {
  134. if (_input != null && _output != null)
  135. {
  136. _facade = new (
  137. _inputProcessor,
  138. _loop.OutputBuffer,
  139. _output,
  140. _loop.AnsiRequestScheduler,
  141. _loop.WindowSizeMonitor);
  142. if (!_isWindowsTerminal)
  143. {
  144. Application.Force16Colors = _facade.Force16Colors = true;
  145. }
  146. Application.Driver = _facade;
  147. _startupSemaphore.Release ();
  148. }
  149. }
  150. private bool _stopCalled;
  151. /// <inheritdoc/>
  152. public void Stop ()
  153. {
  154. // Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
  155. if (_stopCalled)
  156. {
  157. return;
  158. }
  159. _stopCalled = true;
  160. _tokenSource.Cancel ();
  161. _output.Dispose ();
  162. // Wait for input infinite loop to exit
  163. _inputTask.Wait ();
  164. }
  165. }