InputImpl.cs 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. using System.Collections.Concurrent;
  2. namespace Terminal.Gui.Drivers;
  3. /// <summary>
  4. /// Base class for reading console input in perpetual loop.
  5. /// The <see cref="Peek"/> and <see cref="Read"/> methods are executed
  6. /// on the input thread created by <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>.
  7. /// </summary>
  8. /// <typeparam name="TInputRecord"></typeparam>
  9. public abstract class InputImpl<TInputRecord> : IInput<TInputRecord>
  10. {
  11. private ConcurrentQueue<TInputRecord>? _inputQueue;
  12. /// <summary>
  13. /// Determines how to get the current system type, adjust
  14. /// in unit tests to simulate specific timings.
  15. /// </summary>
  16. public Func<DateTime> Now { get; set; } = () => DateTime.Now;
  17. /// <inheritdoc/>
  18. public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
  19. /// <inheritdoc/>
  20. public void Initialize (ConcurrentQueue<TInputRecord> inputQueue) { _inputQueue = inputQueue; }
  21. /// <inheritdoc/>
  22. public void Run (CancellationToken runCancellationToken)
  23. {
  24. // Create a linked token source if we have an external one
  25. CancellationTokenSource? linkedCts = null;
  26. CancellationToken effectiveToken = runCancellationToken;
  27. if (ExternalCancellationTokenSource != null)
  28. {
  29. linkedCts = CancellationTokenSource.CreateLinkedTokenSource (runCancellationToken, ExternalCancellationTokenSource.Token);
  30. effectiveToken = linkedCts.Token;
  31. }
  32. try
  33. {
  34. if (_inputQueue == null)
  35. {
  36. throw new ("Cannot run input before Initialization");
  37. }
  38. do
  39. {
  40. while (Peek ())
  41. {
  42. foreach (TInputRecord r in Read ())
  43. {
  44. _inputQueue.Enqueue (r);
  45. }
  46. }
  47. effectiveToken.ThrowIfCancellationRequested ();
  48. // Throttle the input loop to avoid CPU spinning when no input is available
  49. // This is especially important when multiple ApplicationImpl instances are created
  50. // in parallel tests without calling Shutdown() - prevents thread pool exhaustion
  51. Task.Delay (20, effectiveToken).Wait (effectiveToken);
  52. }
  53. while (!effectiveToken.IsCancellationRequested);
  54. }
  55. catch (OperationCanceledException)
  56. { }
  57. finally
  58. {
  59. Logging.Trace ("Stopping input processing");
  60. linkedCts?.Dispose ();
  61. }
  62. }
  63. /// <summary>
  64. /// When implemented in a derived class, returns true if there is data available
  65. /// to read from console.
  66. /// </summary>
  67. /// <returns></returns>
  68. public abstract bool Peek ();
  69. /// <summary>
  70. /// Returns the available data without blocking, called when <see cref="Peek"/>
  71. /// returns <see langword="true"/>.
  72. /// </summary>
  73. /// <returns></returns>
  74. public abstract IEnumerable<TInputRecord> Read ();
  75. /// <inheritdoc/>
  76. public virtual void Dispose () { }
  77. }