//
// MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui
//
// Authors:
// Miguel de Icaza (miguel@gnome.org)
//
using System.Collections.ObjectModel;
namespace Terminal.Gui;
/// Interface to create a platform specific driver.
internal interface IMainLoopDriver
{
/// Must report whether there are any events pending, or even block waiting for events.
/// true, if there were pending events, false otherwise.
bool EventsPending ();
/// The iteration function.
void Iteration ();
/// Initializes the , gets the calling main loop for the initialization.
/// Call to release resources.
/// Main loop.
void Setup (MainLoop mainLoop);
/// Tears down the driver. Releases resources created in .
void TearDown ();
/// Wakes up the that might be waiting on input, must be thread safe.
void Wakeup ();
}
/// The MainLoop monitors timers and idle handlers.
///
/// Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this
/// on Windows.
///
internal class MainLoop : IDisposable
{
internal List> _idleHandlers = new ();
internal SortedList _timeouts = new ();
/// The idle handlers and lock that must be held while manipulating them
private readonly object _idleHandlersLock = new ();
private readonly object _timeoutsLockToken = new ();
/// Creates a new MainLoop.
/// Use to release resources.
///
/// The instance (one of the implementations FakeMainLoop, UnixMainLoop,
/// NetMainLoop or WindowsMainLoop).
///
internal MainLoop (IMainLoopDriver driver)
{
MainLoopDriver = driver;
driver.Setup (this);
}
/// Gets a copy of the list of all idle handlers.
internal ReadOnlyCollection> IdleHandlers
{
get
{
lock (_idleHandlersLock)
{
return new List> (_idleHandlers).AsReadOnly ();
}
}
}
/// The current in use.
/// The main loop driver.
internal IMainLoopDriver MainLoopDriver { get; private set; }
/// Used for unit tests.
internal bool Running { get; set; }
///
/// Gets the list of all timeouts sorted by the time ticks. A shorter limit time can be
/// added at the end, but it will be called before an earlier addition that has a longer limit time.
///
internal SortedList Timeouts => _timeouts;
///
public void Dispose ()
{
GC.SuppressFinalize (this);
Stop ();
Running = false;
MainLoopDriver?.TearDown ();
MainLoopDriver = null;
}
///
/// Adds specified idle handler function to processing. The handler function will be called
/// once per iteration of the main loop after other events have been handled.
///
///
/// Remove an idle handler by calling with the token this method returns.
///
/// If the returns it will be removed and not called
/// subsequently.
///
///
/// Token that can be used to remove the idle handler with .
// QUESTION: Why are we re-inventing the event wheel here?
// PERF: This is heavy.
// CONCURRENCY: Race conditions exist here.
// CONCURRENCY: null delegates will hose this.
//
internal Func AddIdle (Func idleHandler)
{
lock (_idleHandlersLock)
{
_idleHandlers.Add (idleHandler);
}
MainLoopDriver.Wakeup ();
return idleHandler;
}
/// Adds a timeout to the .
///
/// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
/// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
/// token that can be used to stop the timeout by calling .
///
internal object AddTimeout (TimeSpan time, Func callback)
{
if (callback is null)
{
throw new ArgumentNullException (nameof (callback));
}
var timeout = new Timeout { Span = time, Callback = callback };
AddTimeout (time, timeout);
return timeout;
}
///
/// Called from to check if there are any outstanding timers or idle
/// handlers.
///
///
/// Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if
/// there are no active timers.
///
/// if there is a timer or idle handler active.
internal bool CheckTimersAndIdleHandlers (out int waitTimeout)
{
long now = DateTime.UtcNow.Ticks;
waitTimeout = 0;
lock (_timeouts)
{
if (_timeouts.Count > 0)
{
waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
if (waitTimeout < 0)
{
// This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
// This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
// and no event occurred in elapsed time when the 'poll' is start running again.
waitTimeout = 0;
}
return true;
}
// ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
// the timeout is -1.
waitTimeout = -1;
}
// There are no timers set, check if there are any idle handlers
lock (_idleHandlers)
{
return _idleHandlers.Count > 0;
}
}
/// Determines whether there are pending events to be processed.
///
/// You can use this method if you want to probe if events are pending. Typically used if you need to flush the
/// input queue while still running some of your own code in your main thread.
///
internal bool EventsPending () { return MainLoopDriver.EventsPending (); }
/// Removes an idle handler added with from processing.
/// A token returned by
/// Returns
/// true
/// if the idle handler is successfully removed; otherwise,
/// false
/// .
/// This method also returns
/// false
/// if the idle handler is not found.
internal bool RemoveIdle (Func token)
{
lock (_idleHandlersLock)
{
return _idleHandlers.Remove (token);
}
}
/// Removes a previously scheduled timeout
/// The token parameter is the value returned by AddTimeout.
/// Returns
/// true
/// if the timeout is successfully removed; otherwise,
/// false
/// .
/// This method also returns
/// false
/// if the timeout is not found.
internal bool RemoveTimeout (object token)
{
lock (_timeoutsLockToken)
{
int idx = _timeouts.IndexOfValue (token as Timeout);
if (idx == -1)
{
return false;
}
_timeouts.RemoveAt (idx);
}
return true;
}
/// Runs the . Used only for unit tests.
internal void Run ()
{
bool prev = Running;
Running = true;
while (Running)
{
EventsPending ();
RunIteration ();
}
Running = prev;
}
/// Runs one iteration of timers and file watches
///
/// Use this to process all pending events (timers, idle handlers and file watches).
///
/// while (main.EventsPending ()) RunIteration ();
///
///
internal void RunIteration ()
{
lock (_timeouts)
{
if (_timeouts.Count > 0)
{
RunTimers ();
}
}
MainLoopDriver.Iteration ();
var runIdle = false;
lock (_idleHandlersLock)
{
runIdle = _idleHandlers.Count > 0;
}
if (runIdle)
{
RunIdle ();
}
}
/// Stops the main loop driver and calls . Used only for unit tests.
internal void Stop ()
{
Running = false;
Wakeup ();
}
///
/// Invoked when a new timeout is added. To be used in the case when
/// is .
///
internal event EventHandler TimeoutAdded;
/// Wakes up the that might be waiting on input.
internal void Wakeup () { MainLoopDriver?.Wakeup (); }
private void AddTimeout (TimeSpan time, Timeout timeout)
{
lock (_timeoutsLockToken)
{
long k = (DateTime.UtcNow + time).Ticks;
_timeouts.Add (NudgeToUniqueKey (k), timeout);
TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k));
}
}
///
/// Finds the closest number to that is not present in
/// (incrementally).
///
///
///
private long NudgeToUniqueKey (long k)
{
lock (_timeoutsLockToken)
{
while (_timeouts.ContainsKey (k))
{
k++;
}
}
return k;
}
// PERF: This is heavier than it looks.
// CONCURRENCY: Potential deadlock city here.
// CONCURRENCY: Multiple concurrency pitfalls on the delegates themselves.
// INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern.
private void RunIdle ()
{
List> iterate;
lock (_idleHandlersLock)
{
iterate = _idleHandlers;
_idleHandlers = new List> ();
}
foreach (Func idle in iterate)
{
if (idle ())
{
lock (_idleHandlersLock)
{
_idleHandlers.Add (idle);
}
}
}
}
private void RunTimers ()
{
long now = DateTime.UtcNow.Ticks;
SortedList copy;
// lock prevents new timeouts being added
// after we have taken the copy but before
// we have allocated a new list (which would
// result in lost timeouts or errors during enumeration)
lock (_timeoutsLockToken)
{
copy = _timeouts;
_timeouts = new SortedList ();
}
foreach ((long k, Timeout timeout) in copy)
{
if (k < now)
{
if (timeout.Callback ())
{
AddTimeout (timeout.Span, timeout);
}
}
else
{
lock (_timeoutsLockToken)
{
_timeouts.Add (NudgeToUniqueKey (k), timeout);
}
}
}
}
}