using System.Collections.Concurrent;
namespace Terminal.Gui.App;
///
///
/// Coordinates the creation and startup of the main UI loop and input thread.
///
///
/// This class bootstraps the that handles
/// UI layout, drawing, and event processing while also managing a separate thread
/// for reading console input asynchronously.
///
/// This class is designed to be managed by
///
/// Type of raw input events, e.g. for .NET driver
internal class MainLoopCoordinator : IMainLoopCoordinator where TInputRecord : struct
{
///
/// Creates a new coordinator that will manage the main UI loop and input thread.
///
/// Handles scheduling and execution of user timeout callbacks
/// Thread-safe queue for buffering raw console input
/// The main application loop instance
/// Factory for creating driver-specific components (input, output, etc.)
public MainLoopCoordinator (
ITimedEvents timedEvents,
ConcurrentQueue inputQueue,
IApplicationMainLoop loop,
IComponentFactory componentFactory
)
{
_timedEvents = timedEvents;
_inputQueue = inputQueue;
_inputProcessor = componentFactory.CreateInputProcessor (_inputQueue);
_loop = loop;
_componentFactory = componentFactory;
}
private readonly IApplicationMainLoop _loop;
private readonly IComponentFactory _componentFactory;
private readonly CancellationTokenSource _runCancellationTokenSource = new ();
private readonly ConcurrentQueue _inputQueue;
private readonly IInputProcessor _inputProcessor;
private readonly object _oLockInitialization = new ();
private readonly ITimedEvents _timedEvents;
private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
private IInput? _input;
private Task? _inputTask;
private IOutput? _output;
private DriverImpl? _driver;
private bool _stopCalled;
///
/// Starts the input loop thread in separate task (returning immediately).
///
/// The instance that is running the input loop.
public async Task StartInputTaskAsync (IApplication? app)
{
Logging.Trace ("Booting... ()");
_inputTask = Task.Run (() => RunInput (app));
// Main loop is now booted on same thread as rest of users application
BootMainLoop (app);
// Wait asynchronously for the semaphore or task failure.
Task waitForSemaphore = _startupSemaphore.WaitAsync ();
// Wait for either the semaphore to be released or the input task to crash.
// ReSharper disable once UseConfigureAwaitFalse
Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask);
// Check if the task was the input task and if it has failed.
if (completedTask == _inputTask)
{
if (_inputTask.IsFaulted)
{
throw _inputTask.Exception;
}
Logging.Critical ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
}
Logging.Trace ("Booting complete");
}
///
public void RunIteration ()
{
lock (_oLockInitialization)
{
_loop.Iteration ();
}
}
///
public void Stop ()
{
// Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
if (_stopCalled)
{
return;
}
_stopCalled = true;
_runCancellationTokenSource.Cancel ();
_output?.Dispose ();
// Wait for input infinite loop to exit
_inputTask?.Wait ();
}
private void BootMainLoop (IApplication? app)
{
//Logging.Trace ($"_inputProcessor: {_inputProcessor}, _output: {_output}, _componentFactory: {_componentFactory}");
lock (_oLockInitialization)
{
// Instance must be constructed on the thread in which it is used.
_output = _componentFactory.CreateOutput ();
_loop.Initialize (_timedEvents, _inputQueue, _inputProcessor, _output, _componentFactory, app);
BuildDriverIfPossible (app);
}
}
private void BuildDriverIfPossible (IApplication? app)
{
if (_input != null && _output != null)
{
_driver = new (
_inputProcessor,
_loop.OutputBuffer,
_output,
_loop.AnsiRequestScheduler,
_loop.SizeMonitor);
app!.Driver = _driver;
_startupSemaphore.Release ();
Logging.Trace ($"Driver: _input: {_input}, _output: {_output}");
}
}
///
/// INTERNAL: Runs the IInput read loop on a new thread called the "Input Thread".
///
///
private void RunInput (IApplication? app)
{
try
{
lock (_oLockInitialization)
{
// Instance must be constructed on the thread in which it is used.
_input = _componentFactory.CreateInput ();
_input.Initialize (_inputQueue);
// Wire up InputImpl reference for ITestableInput support
if (_inputProcessor is InputProcessorImpl impl)
{
impl.InputImpl = _input;
}
BuildDriverIfPossible (app);
}
try
{
_input.Run (_runCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{ }
_input.Dispose ();
}
catch (Exception e)
{
Logging.Critical ($"Input loop crashed: {e}");
throw;
}
if (_stopCalled)
{
Logging.Information ("Input loop exited cleanly");
}
else
{
Logging.Critical ("Input loop exited early (stop not called)");
}
}
}