// // MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui // // Authors: // Miguel de Icaza (miguel@gnome.org) // using System; using System.Collections.Generic; using System.Collections.ObjectModel; namespace Terminal.Gui { /// /// Public interface to create a platform specific driver. /// internal interface IMainLoopDriver { /// /// Initializes the , gets the calling main loop for the initialization. /// /// /// Call to release resources. /// /// Main loop. void Setup (MainLoop mainLoop); /// /// Wakes up the that might be waiting on input, must be thread safe. /// void Wakeup (); /// /// 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 (); /// /// Tears down the driver. Releases resources created in . /// void TearDown (); } /// /// 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 SortedList _timeouts = new SortedList (); readonly object _timeoutsLockToken = new object (); /// /// The idle handlers and lock that must be held while manipulating them /// readonly object _idleHandlersLock = new object (); internal List> _idleHandlers = new List> (); /// /// 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; /// /// 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; } /// /// Invoked when a new timeout is added. To be used in the case /// when is . /// internal event EventHandler TimeoutAdded; /// /// 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); } /// /// 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 . internal Func AddIdle (Func idleHandler) { lock (_idleHandlersLock) { _idleHandlers.Add (idleHandler); } MainLoopDriver.Wakeup (); return idleHandler; } /// /// Removes an idle handler added with from processing. /// /// A token returned by /// Returns trueif 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); } } void AddTimeout (TimeSpan time, Timeout timeout) { lock (_timeoutsLockToken) { var k = (DateTime.UtcNow + time).Ticks; _timeouts.Add (NudgeToUniqueKey (k), timeout); TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k)); } } /// /// 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 == null) { throw new ArgumentNullException (nameof (callback)); } var timeout = new Timeout () { Span = time, Callback = callback }; AddTimeout (time, timeout); return timeout; } /// /// Removes a previously scheduled timeout /// /// /// The token parameter is the value returned by AddTimeout. /// /// Returns trueif 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) { var idx = _timeouts.IndexOfValue (token as Timeout); if (idx == -1) { return false; } _timeouts.RemoveAt (idx); } return true; } void RunTimers () { var 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 ((var k, var timeout) in copy) { if (k < now) { if (timeout.Callback ()) { AddTimeout (timeout.Span, timeout); } } else { lock (_timeoutsLockToken) { _timeouts.Add (NudgeToUniqueKey (k), 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) { var 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; } } /// /// Finds the closest number to that is not /// present in (incrementally). /// /// /// long NudgeToUniqueKey (long k) { lock (_timeoutsLockToken) { while (_timeouts.ContainsKey (k)) { k++; } } return k; } void RunIdle () { List> iterate; lock (_idleHandlersLock) { iterate = _idleHandlers; _idleHandlers = new List> (); } foreach (var idle in iterate) { if (idle ()) { lock (_idleHandlersLock) { _idleHandlers.Add (idle); } } } } /// /// Used for unit tests. /// internal bool Running { get; private set; } /// /// 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 (); } /// /// 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 (); } } /// /// Runs the . Used only for unit tests. /// internal void Run () { var prev = Running; Running = true; while (Running) { EventsPending (); RunIteration (); } Running = prev; } /// /// Wakes up the that might be waiting on input. /// internal void Wakeup () => MainLoopDriver?.Wakeup (); /// /// Stops the main loop driver and calls . Used only for unit tests. /// internal void Stop () { Running = false; Wakeup (); } /// public void Dispose () { GC.SuppressFinalize (this); Stop (); Running = false; MainLoopDriver?.TearDown (); MainLoopDriver = null; } } }