// // mainloop.cs: Simple managed mainloop implementation. // // Authors: // Miguel de Icaza (miguel.de.icaza@gmail.com) // // Copyright (C) 2011 Novell (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System.Collections.Generic; using System; using System.Runtime.InteropServices; using System.Threading; namespace Mono.Terminal { /// /// Public interface to create your own platform specific main loop driver. /// public interface IMainLoopDriver { /// /// Initializes the main loop driver, gets the calling main loop for the initialization. /// /// Main loop. void Setup (MainLoop mainLoop); /// /// Wakes up the mainloop 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. /// If set to true wait until an event is available, otherwise return immediately. bool EventsPending (bool wait); /// /// The interation function. /// void MainIteration (); } /// /// Unix main loop, suitable for using on Posix systems /// /// /// In addition to the general functions of the mainloop, the Unix version /// can watch file descriptors using the AddWatch methods. /// public class UnixMainLoop : IMainLoopDriver { [StructLayout (LayoutKind.Sequential)] struct Pollfd { public int fd; public short events, revents; } /// /// Condition on which to wake up from file descriptor activity. These match the Linux/BSD poll definitions. /// [Flags] public enum Condition : short { /// /// There is data to read /// PollIn = 1, /// /// Writing to the specified descriptor will not block /// PollOut = 4, /// /// There is urgent data to read /// PollPri = 2, /// /// Error condition on output /// PollErr = 8, /// /// Hang-up on output /// PollHup = 16, /// /// File descriptor is not open. /// PollNval = 32 } class Watch { public int File; public Condition Condition; public Func Callback; } Dictionary descriptorWatchers = new Dictionary (); [DllImport ("libc")] extern static int poll ([In, Out]Pollfd [] ufds, uint nfds, int timeout); [DllImport ("libc")] extern static int pipe ([In, Out]int [] pipes); [DllImport ("libc")] extern static int read (int fd, IntPtr buf, IntPtr n); [DllImport ("libc")] extern static int write (int fd, IntPtr buf, IntPtr n); Pollfd [] pollmap; bool poll_dirty = true; int [] wakeupPipes = new int [2]; static IntPtr ignore = Marshal.AllocHGlobal (1); MainLoop mainLoop; void IMainLoopDriver.Wakeup () { write (wakeupPipes [1], ignore, (IntPtr) 1); } void IMainLoopDriver.Setup (MainLoop mainLoop) { this.mainLoop = mainLoop; pipe (wakeupPipes); AddWatch (wakeupPipes [0], Condition.PollIn, ml => { read (wakeupPipes [0], ignore, (IntPtr)1); return true; }); } /// /// Removes an active watch from the mainloop. /// /// /// The token parameter is the value returned from AddWatch /// public void RemoveWatch (object token) { var watch = token as Watch; if (watch == null) return; descriptorWatchers.Remove (watch.File); } /// /// Watches a file descriptor for activity. /// /// /// When the condition is met, the provided callback /// is invoked. If the callback returns false, the /// watch is automatically removed. /// /// The return value is a token that represents this watch, you can /// use this token to remove the watch by calling RemoveWatch. /// public object AddWatch (int fileDescriptor, Condition condition, Func callback) { if (callback == null) throw new ArgumentNullException (nameof(callback)); var watch = new Watch () { Condition = condition, Callback = callback, File = fileDescriptor }; descriptorWatchers [fileDescriptor] = watch; poll_dirty = true; return watch; } void UpdatePollMap () { if (!poll_dirty) return; poll_dirty = false; pollmap = new Pollfd [descriptorWatchers.Count]; int i = 0; foreach (var fd in descriptorWatchers.Keys) { pollmap [i].fd = fd; pollmap [i].events = (short)descriptorWatchers [fd].Condition; i++; } } bool IMainLoopDriver.EventsPending (bool wait) { long now = DateTime.UtcNow.Ticks; int pollTimeout, n; if (mainLoop.timeouts.Count > 0) { pollTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); if (pollTimeout < 0) return true; } else pollTimeout = -1; if (!wait) pollTimeout = 0; UpdatePollMap (); while (true) { if (wait && pollTimeout == -1) { pollTimeout = 0; } n = poll (pollmap, (uint)pollmap.Length, pollTimeout); if (pollmap != null) { break; } if (mainLoop.timeouts.Count > 0 || mainLoop.idleHandlers.Count > 0) { return true; } } int ic; lock (mainLoop.idleHandlers) ic = mainLoop.idleHandlers.Count; return n > 0 || mainLoop.timeouts.Count > 0 && ((mainLoop.timeouts.Keys [0] - DateTime.UtcNow.Ticks) < 0) || ic > 0; } void IMainLoopDriver.MainIteration () { if (pollmap != null) { foreach (var p in pollmap) { Watch watch; if (p.revents == 0) continue; if (!descriptorWatchers.TryGetValue (p.fd, out watch)) continue; if (!watch.Callback (this.mainLoop)) descriptorWatchers.Remove (p.fd); } } } } /// /// Mainloop intended to be used with the .NET System.Console API, and can /// be used on Windows and Unix, it is cross platform but lacks things like /// file descriptor monitoring. /// class NetMainLoop : IMainLoopDriver { AutoResetEvent keyReady = new AutoResetEvent (false); AutoResetEvent waitForProbe = new AutoResetEvent (false); ConsoleKeyInfo? windowsKeyResult = null; public Action WindowsKeyPressed; MainLoop mainLoop; public NetMainLoop () { } void WindowsKeyReader () { while (true) { waitForProbe.WaitOne (); windowsKeyResult = Console.ReadKey (true); keyReady.Set (); } } void IMainLoopDriver.Setup (MainLoop mainLoop) { this.mainLoop = mainLoop; Thread readThread = new Thread (WindowsKeyReader); readThread.Start (); } void IMainLoopDriver.Wakeup () { } bool IMainLoopDriver.EventsPending (bool wait) { long now = DateTime.UtcNow.Ticks; int waitTimeout; if (mainLoop.timeouts.Count > 0) { waitTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); if (waitTimeout < 0) return true; } else waitTimeout = -1; if (!wait) waitTimeout = 0; windowsKeyResult = null; waitForProbe.Set (); keyReady.WaitOne (waitTimeout); return windowsKeyResult.HasValue; } void IMainLoopDriver.MainIteration () { if (windowsKeyResult.HasValue) { if (WindowsKeyPressed!= null) WindowsKeyPressed (windowsKeyResult.Value); windowsKeyResult = null; } } } /// /// Simple main loop implementation that can be used to monitor /// file descriptor, run 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. /// public class MainLoop { internal class Timeout { public TimeSpan Span; public Func Callback; } internal SortedList timeouts = new SortedList (); internal List> idleHandlers = new List> (); IMainLoopDriver driver; /// /// The current IMainLoopDriver in use. /// /// The driver. public IMainLoopDriver Driver => driver; /// /// Creates a new Mainloop, to run it you must provide a driver, and choose /// one of the implementations UnixMainLoop, NetMainLoop or WindowsMainLoop. /// public MainLoop (IMainLoopDriver driver) { this.driver = driver; driver.Setup (this); } /// /// Runs @action on the thread that is processing events /// public void Invoke (Action action) { AddIdle (()=> { action (); return false; }); } /// /// Executes the specified @idleHandler on the idle loop. The return value is a token to remove it. /// public Func AddIdle (Func idleHandler) { lock (idleHandlers) idleHandlers.Add (idleHandler); return idleHandler; } /// /// Removes the specified idleHandler from processing. /// public void RemoveIdle (Func idleHandler) { lock (idleHandler) idleHandlers.Remove (idleHandler); } void AddTimeout (TimeSpan time, Timeout timeout) { timeouts.Add ((DateTime.UtcNow + time).Ticks, timeout); } /// /// Adds a timeout to the mainloop. /// /// /// When time 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. /// /// The returned value is a token that can be used to stop the timeout /// by calling RemoveTimeout. /// public 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. /// public void RemoveTimeout (object token) { var idx = timeouts.IndexOfValue (token as Timeout); if (idx == -1) return; timeouts.RemoveAt (idx); } void RunTimers () { long now = DateTime.UtcNow.Ticks; var copy = timeouts; timeouts = new SortedList (); foreach (var k in copy.Keys){ var timeout = copy [k]; if (k < now) { if (timeout.Callback (this)) AddTimeout (timeout.Span, timeout); } else timeouts.Add (k, timeout); } } void RunIdle () { List> iterate; lock (idleHandlers){ iterate = idleHandlers; idleHandlers = new List> (); } foreach (var idle in iterate){ if (idle ()) lock (idleHandlers) idleHandlers.Add (idle); } } bool running; /// /// Stops the mainloop. /// public void Stop () { running = false; driver.Wakeup (); } /// /// 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. /// public bool EventsPending (bool wait = false) { return driver.EventsPending (wait); } /// /// Runs one iteration of timers and file watches /// /// /// You use this to process all pending events (timers, idle handlers and file watches). /// /// You can use it like this: /// while (main.EvensPending ()) MainIteration (); /// public void MainIteration () { if (timeouts.Count > 0) RunTimers (); driver.MainIteration (); lock (idleHandlers){ if (idleHandlers.Count > 0) RunIdle(); } } /// /// Runs the mainloop. /// public void Run () { bool prev = running; running = true; while (running){ EventsPending (true); MainIteration (); } running = prev; } } }