@ -1,13 +1,6 @@
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
using System.Runtime.InteropServices ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
using AutoHotInterception.Helpers ;
using static AutoHotInterception . Helpers . HelperFunctions ;
@ -15,22 +8,41 @@ namespace AutoHotInterception
{
public class Manager : IDisposable
{
private readonly IntPtr _deviceContext ;
private Thread _pollThread ;
private bool _pollThreadRunning = false ;
private readonly ConcurrentDictionary < int , dynamic >
_contextCallbacks = new ConcurrentDictionary < int , dynamic > ( ) ;
private bool _filterState = false ;
private readonly ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > _keyboardMappings = new ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > ( ) ;
private readonly ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > _mouseButtonMappings = new ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > ( ) ;
private readonly ConcurrentDictionary < int , MappingOptions > _mouseMoveRelativeMappings = new ConcurrentDictionary < int , MappingOptions > ( ) ;
private readonly ConcurrentDictionary < int , MappingOptions > _mouseMoveAbsoluteMappings = new ConcurrentDictionary < int , MappingOptions > ( ) ;
private readonly ConcurrentDictionary < int , dynamic > _contextCallbacks = new ConcurrentDictionary < int , dynamic > ( ) ;
private readonly IntPtr _deviceContext ;
// If a the ID of a device exists as a key in this Dictionary, then that device is filtered.
// If a device ID exists as a key in this Dictionary then that device is filtered.
// Used by IsMonitoredDevice, which is handed to Interception as a "Predicate".
private readonly ConcurrentDictionary < int , bool > _filteredDevices = new ConcurrentDictionary < int , bool > ( ) ;
private readonly ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > _keyboardMappings =
new ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > ( ) ;
private readonly ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > _mouseButtonMappings =
new ConcurrentDictionary < int , ConcurrentDictionary < ushort , MappingOptions > > ( ) ;
private readonly ConcurrentDictionary < int , MappingOptions > _mouseMoveAbsoluteMappings =
new ConcurrentDictionary < int , MappingOptions > ( ) ;
private readonly ConcurrentDictionary < int , MappingOptions > _mouseMoveRelativeMappings =
new ConcurrentDictionary < int , MappingOptions > ( ) ;
// If an event is subscribed to with concurrent set to false then use a single worker thread to process each event.
// Makes sure the events are handled synchronously and with a FIFO order.
private readonly ConcurrentDictionary < int , ConcurrentDictionary < ushort , WorkerThread > > _workers =
new ConcurrentDictionary < int , ConcurrentDictionary < ushort , WorkerThread > > ( ) ;
private bool _filterState ;
private Thread _pollThread ;
private bool _pollThreadRunning ;
public void Dispose ( )
{
SetThreadState ( false ) ;
}
#region Public
#region Initialization
@ -50,90 +62,131 @@ namespace AutoHotInterception
#region Subscription Mode
/// <summary>
/// Subscribes to a Keyboard key
/// Subscribes to a Keyboard key
/// </summary>
/// <param name="id">The ID of the Keyboard</param>
/// <param name="code">The ScanCode of the key</param>
/// <param name="block">Whether or not to block the key</param>
/// <param name="callback">The callback to fire when the key changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeKey ( int id , ushort code , bool block , dynamic callback )
public void SubscribeKey ( int id , ushort code , bool block , dynamic callback , bool concurrent = true )
{
SetFilterState ( false ) ;
IsValidDeviceId ( false , id ) ;
if ( ! _keyboardMappings . ContainsKey ( id ) )
{
_keyboardMappings . TryAdd ( id , new ConcurrentDictionary < ushort , MappingOptions > ( ) ) ;
}
_keyboardMappings [ id ] . TryAdd ( code , new MappingOptions ( ) { Block = block , Callback = callback } ) ;
_keyboardMappings [ id ] . TryAdd ( code ,
new MappingOptions { Block = block , Concurrent = concurrent , Callback = callback } ) ;
_filteredDevices [ id ] = true ;
if ( ! concurrent )
{
if ( ! _workers . ContainsKey ( id ) ) _workers . TryAdd ( id , new ConcurrentDictionary < ushort , WorkerThread > ( ) ) ;
var worker = new WorkerThread ( ) ;
_workers [ id ] . TryAdd ( code , worker ) ;
worker . Start ( ) ;
}
SetFilterState ( true ) ;
SetThreadState ( true ) ;
}
/// <summary>
/// Subscribe to a Mouse button
/// Subscribe to a Mouse button
/// </summary>
/// <param name="id">The ID of the mouse</param>
/// <param name="btn">The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4</param>
/// <param name="btn">The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4 , WV = 5, WH = 6) </param>
/// <param name="block">Whether or not to block the button</param>
/// <param name="callback">The callback to fire when the button changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeMouseButton ( int id , ushort btn , bool block , dynamic callback )
public void SubscribeMouseButton ( int id , ushort btn , bool block , dynamic callback , bool concurrent = true )
{
IsValidDeviceId ( true , id ) ;
if ( ! _mouseButtonMappings . ContainsKey ( id ) )
{
_mouseButtonMappings . TryAdd ( id , new ConcurrentDictionary < ushort , MappingOptions > ( ) ) ;
}
_mouseButtonMappings [ id ] . TryAdd ( btn , new MappingOptions ( ) { Block = block , C allback = callback } ) ;
_mouseButtonMappings [ id ] . TryAdd ( btn ,
new MappingOptions { Block = block , C oncurrent = concurrent , C allback = callback } ) ;
_filteredDevices [ id ] = true ;
if ( ! concurrent )
{
if ( ! _workers . ContainsKey ( id ) ) _workers . TryAdd ( id , new ConcurrentDictionary < ushort , WorkerThread > ( ) ) ;
var worker = new WorkerThread ( ) ;
_workers [ id ] . TryAdd ( btn , worker ) ;
worker . Start ( ) ;
}
SetFilterState ( true ) ;
SetThreadState ( true ) ;
}
//Shorthand for SubscribeMouseMoveRelative
public void SubscribeMouseMove ( int id , bool block , dynamic callback )
{
SubscribeMouseMoveRelative ( id , block , callback ) ;
}
/// <summary>
/// Subscribes to Relativ e mouse movement
/// Subscribes to Absolute mouse movement
/// </summary>
/// <param name="id">The id of the Mouse</param>
/// <param name="block">Whether or not to block the movement</param>
/// <param name="callback">The callback to fire when the mouse moves</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeMouseMove Relativ e( int id , bool block , dynamic callback )
public void SubscribeMouseMove Absolut e( int id , bool block , dynamic callback , bool concurrent = true )
{
IsValidDeviceId ( true , id ) ;
_mouseMoveRelativeMappings [ id ] = new MappingOptions ( ) { Block = block , Callback = callback } ;
_mouseMoveAbsoluteMappings [ id ] = new MappingOptions
{ Block = block , Concurrent = concurrent , Callback = callback } ;
_filteredDevices [ id ] = true ;
if ( ! concurrent )
{
if ( ! _workers . ContainsKey ( id ) ) _workers . TryAdd ( id , new ConcurrentDictionary < ushort , WorkerThread > ( ) ) ;
var worker = new WorkerThread ( ) ;
_workers [ id ] . TryAdd ( 7 , worker ) ; // Use 7 as second index for MouseMoveAbsolute
worker . Start ( ) ;
}
SetFilterState ( true ) ;
SetThreadState ( true ) ;
}
//Shorthand for SubscribeMouseMoveRelative
public void SubscribeMouseMove ( int id , bool block , dynamic callback , bool concurrent = true )
{
SubscribeMouseMoveRelative ( id , block , callback , concurrent ) ;
}
/// <summary>
///
/// Subscribes to Absolute mouse movement
/// Subscribes to Relative mouse movement
/// </summary>
/// <param name="id">The id of the Mouse</param>
/// <param name="block">Whether or not to block the movement</param>
/// <param name="callback">The callback to fire when the mouse moves</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeMouseMove Absolut e( int id , bool block , dynamic callback )
public void SubscribeMouseMove Relativ e( int id , bool block , dynamic callback , bool concurrent = true )
{
IsValidDeviceId ( true , id ) ;
_mouseMoveAbsoluteMappings [ id ] = new MappingOptions ( ) { Block = block , Callback = callback } ;
_mouseMoveRelativeMappings [ id ] = new MappingOptions
{ Block = block , Concurrent = concurrent , Callback = callback } ;
_filteredDevices [ id ] = true ;
if ( ! concurrent )
{
if ( ! _workers . ContainsKey ( id ) ) _workers . TryAdd ( id , new ConcurrentDictionary < ushort , WorkerThread > ( ) ) ;
var worker = new WorkerThread ( ) ;
_workers [ id ] . TryAdd ( 8 , worker ) ; // Use 8 as second index for MouseMoveRelative
worker . Start ( ) ;
}
SetFilterState ( true ) ;
SetThreadState ( true ) ;
}
@ -143,7 +196,7 @@ namespace AutoHotInterception
#region Context Mode
/// <summary>
/// Sets a callback for Context Mode for a given device
/// Sets a callback for Context Mode for a given device
/// </summary>
/// <param name="id">The ID of the device</param>
/// <param name="callback">The callback to fire before and after each key or button press</param>
@ -152,9 +205,7 @@ namespace AutoHotInterception
{
SetFilterState ( false ) ;
if ( id < 1 | | id > 20 )
{
throw new ArgumentOutOfRangeException ( nameof ( id ) , "DeviceIds must be between 1 and 20" ) ;
}
_contextCallbacks [ id ] = callback ;
_filteredDevices [ id ] = true ;
@ -168,7 +219,7 @@ namespace AutoHotInterception
#region Input Synthesis
/// <summary>
/// Sends a keyboard key event
/// Sends a keyboard key event
/// </summary>
/// <param name="id">The ID of the Keyboard to send as</param>
/// <param name="code">The ScanCode to send</param>
@ -182,17 +233,16 @@ namespace AutoHotInterception
{
code - = 256 ;
if ( code ! = 54 ) // RShift has > 256 code, but state is 0/1
{
st + = 2 ;
}
}
stroke . key . code = code ;
stroke . key . state = ( ushort ) st ;
stroke . key . state = ( ushort ) st ;
ManagedWrapper . Send ( _deviceContext , id , ref stroke , 1 ) ;
}
/// <summary>
/// Sends Mouse button events
/// Sends Mouse button events
/// </summary>
/// <param name="id"></param>
/// <param name="btn"></param>
@ -207,7 +257,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Same as <see cref="SendMouseButtonEvent"/>, but sends button events in Absolute mode (with coordinates)
/// Same as <see cref="SendMouseButtonEvent" />, but sends button events in Absolute mode (with coordinates)
/// </summary>
/// <param name="id"></param>
/// <param name="btn"></param>
@ -229,7 +279,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Sends Relative Mouse Movement
/// Sends Relative Mouse Movement
/// </summary>
/// <param name="id"></param>
/// <param name="x"></param>
@ -239,14 +289,15 @@ namespace AutoHotInterception
{
IsValidDeviceId ( true , id ) ;
var stroke = new ManagedWrapper . Stroke { mouse = { x = x , y = y , flags = ( ushort ) ManagedWrapper . MouseFlag . MouseMoveRelative } } ;
var stroke = new ManagedWrapper . Stroke
{ mouse = { x = x , y = y , flags = ( ushort ) ManagedWrapper . MouseFlag . MouseMoveRelative } } ;
ManagedWrapper . Send ( _deviceContext , id , ref stroke , 1 ) ;
}
/// <summary>
/// Sends Absolute Mouse Movement
/// Note: Newing up a stroke seems to make Absolute input b e relative to main monitor
/// Calling Send on an actual stroke from an Absolute device results in input relative to all monitors
/// Sends Absolute Mouse Movement
/// Note: Creating a new stroke seems to make Absolute input becom e relative to main monitor
/// Calling Send on an actual stroke from an Absolute device results in input relative to all monitors
/// </summary>
/// <param name="id"></param>
/// <param name="x"></param>
@ -256,7 +307,8 @@ namespace AutoHotInterception
{
IsValidDeviceId ( true , id ) ;
var stroke = new ManagedWrapper . Stroke { mouse = { x = x , y = y , flags = ( ushort ) ManagedWrapper . MouseFlag . MouseMoveAbsolute } } ;
var stroke = new ManagedWrapper . Stroke
{ mouse = { x = x , y = y , flags = ( ushort ) ManagedWrapper . MouseFlag . MouseMoveAbsolute } } ;
ManagedWrapper . Send ( _deviceContext , id , ref stroke , 1 ) ;
}
@ -285,7 +337,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Tries to get Device ID from VID/PID
/// Tries to get Device ID from VID/PID
/// </summary>
/// <param name="isMouse">Whether the device is a mouse or a keyboard</param>
/// <param name="vid">The VID of the device</param>
@ -302,10 +354,7 @@ namespace AutoHotInterception
int foundVid = 0 , foundPid = 0 ;
GetVidPid ( hardwareStr , ref foundVid , ref foundPid ) ;
if ( foundVid ! = vid | | foundPid ! = pid ) continue ;
if ( instance = = 1 )
{
return i ;
}
if ( instance = = 1 ) return i ;
instance - - ;
}
@ -314,7 +363,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Tries to get Device ID from Hardware String
/// Tries to get Device ID from Hardware String
/// </summary>
/// <param name="isMouse">Whether the device is a mouse or a keyboard</param>
/// <param name="handle">The Hardware String (handle) of the device</param>
@ -329,10 +378,7 @@ namespace AutoHotInterception
var hardwareStr = ManagedWrapper . GetHardwareStr ( _deviceContext , i , 1000 ) ;
if ( hardwareStr ! = handle ) continue ;
if ( instance = = 1 )
{
return i ;
}
if ( instance = = 1 ) return i ;
instance - - ;
}
@ -341,9 +387,9 @@ namespace AutoHotInterception
}
/// <summary>
/// Gets a list of connected devices
/// Intended to be used called via the AHK wrapper...
/// ... so it can convert the return value into an AHK array
/// Gets a list of connected devices
/// Intended to be used called via the AHK wrapper...
/// ... so it can convert the return value into an AHK array
/// </summary>
/// <returns></returns>
public DeviceInfo [ ] GetDeviceList ( )
@ -370,17 +416,17 @@ namespace AutoHotInterception
}
else
{
_pollThread . Abor t( ) ;
_pollThread . Interrup t( ) ;
_pollThread . Join ( ) ;
_pollThread = null ;
}
}
/// <summary>
/// Predicate used by Interception to decide whether to filter this device or not.
/// WARNING! Setting this to always return true is RISKY, as you could lock yourself out of Windows...
/// ... requiring a reboot.
/// When working with AHI, it's generally best to keep this matching as little as possible....
/// Predicate used by Interception to decide whether to filter this device or not.
/// WARNING! Setting this to always return true is RISKY, as you could lock yourself out of Windows...
/// ... requiring a reboot.
/// When working with AHI, it's generally best to keep this matching as little as possible....
/// </summary>
/// <param name="device"></param>
/// <returns></returns>
@ -400,7 +446,7 @@ namespace AutoHotInterception
// ScanCode notes: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html
private void PollThread ( )
{
ManagedWrapper . Stroke stroke = new ManagedWrapper . Stroke ( ) ;
var stroke = new ManagedWrapper . Stroke ( ) ;
while ( true )
{
@ -425,16 +471,10 @@ namespace AutoHotInterception
var state = stroke . key . state ;
#region KeyCode, State, Extended Flag translation
// Begin translation of incoming key code, state, extended flag etc...
var processMappings = true ;
if ( code = = 54 )
{
// Interception seems to report Right Shift as 54 / 0x36 with state 0/1...
// ... this code is normally unused (Alt-SysRq according to linked page) ...
// ... and AHK uses 54 + 256 = 310 (0x36 + 0x100 = 0x136)...
// ... so change the code, but leave the state as 0/1
code = 310 ;
}
if ( code = = 54 ) code = 310 ;
// If state is shifted up by 2 (1 or 2 instead of 0 or 1), then this is an "Extended" key code
if ( state > 1 )
@ -445,7 +485,7 @@ namespace AutoHotInterception
// Example case is Delete (The one above the arrow keys, not on numpad)...
// ... this generates a stroke of 0x2a (Shift) with *extended flag set* (Normal shift does not do this)...
// ... followed by 0x53 with extended flag set.
// We do not want to fire subs riptions for the extended shift, but *do* want to let the key flow through...
// We do not want to fire subs c riptions for the extended shift, but *do* want to let the key flow through...
// ... so that is handled here.
// When the extended key (Delete in the above example) subsequently comes through...
// ... it will have code 0x53, which we shift to 0x153 (Adding 256 Dec) to signify extended version...
@ -464,6 +504,7 @@ namespace AutoHotInterception
state - = 2 ;
}
}
# endregion
// Code and state now normalized, proceed with checking for subscriptions...
@ -471,12 +512,16 @@ namespace AutoHotInterception
{
hasSubscription = true ;
var mapping = _keyboardMappings [ i ] [ code ] ;
if ( mapping . Block )
if ( mapping . Block ) block = true ;
if ( mapping . Concurrent )
{
block = true ;
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( 1 - state ) ) ;
}
else if ( _workers . ContainsKey ( i ) & & _workers [ i ] . ContainsKey ( code ) )
{
var worker = _workers [ i ] [ code ] ;
worker ? . Actions . Add ( ( ) = > mapping . Callback ( 1 - state ) ) ;
}
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( 1 - state ) ) ;
}
}
@ -485,19 +530,13 @@ namespace AutoHotInterception
// If this key had no subscriptions, but Context Mode is set for this keyboard...
// ... then set the Context before sending the key
if ( ! hasSubscription & & hasContext )
{
_contextCallbacks [ i ] ( 1 ) ;
}
if ( ! hasSubscription & & hasContext ) _contextCallbacks [ i ] ( 1 ) ;
// Pass the key through to the OS.
ManagedWrapper . Send ( _deviceContext , i , ref stroke , 1 ) ;
// If we are processing Context Mode, then Unset the context variable after sending the key
if ( ! hasSubscription & & hasContext )
{
_contextCallbacks [ i ] ( 0 ) ;
}
if ( ! hasSubscription & & hasContext ) _contextCallbacks [ i ] ( 0 ) ;
}
}
@ -523,14 +562,21 @@ namespace AutoHotInterception
{
hasSubscription = true ;
var mapping = _mouseButtonMappings [ i ] [ btnState . Button ] ;
if ( mapping . Block )
{
block = true ;
}
if ( mapping . Block ) block = true ;
var state = btnState ;
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( state . State ) ) ;
if ( mapping . Concurrent )
{
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( 1 - state . State ) ) ;
}
else if ( _workers . ContainsKey ( i ) & & _workers [ i ] . ContainsKey ( btnState . Button ) )
{
var worker = _workers [ i ] [ btnState . Button ] ;
worker ? . Actions . Add ( ( ) = > mapping . Callback ( 1 - state . State ) ) ;
}
}
//Console.WriteLine($"AHK| Mouse {i} seen - button {btnState.Button}, state: {stroke.mouse.state}, rolling: {stroke.mouse.rolling}");
}
else if ( ( stroke . mouse . flags & ( ushort ) ManagedWrapper . MouseFlag . MouseMoveAbsolute ) = =
@ -540,51 +586,52 @@ namespace AutoHotInterception
// Absolute Mouse Move
hasSubscription = true ;
var mapping = _mouseMoveAbsoluteMappings [ i ] ;
if ( mapping . Block )
{
block = true ;
}
if ( mapping . Block ) block = true ;
var x = stroke . mouse . x ;
var y = stroke . mouse . y ;
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( x , y ) ) ;
if ( mapping . Concurrent )
{
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( x , y ) ) ;
}
else if ( _workers . ContainsKey ( i ) & & _workers [ i ] . ContainsKey ( 7 ) )
{
var worker = _workers [ i ] [ 7 ] ;
worker ? . Actions . Add ( ( ) = > mapping . Callback ( x , y ) ) ;
}
}
else if ( ( stroke . mouse . flags & ( ushort ) ManagedWrapper . MouseFlag . MouseMoveRelative ) = =
( ushort ) ManagedWrapper . MouseFlag . MouseMoveRelative
else if ( ( stroke . mouse . flags & ( ushort ) ManagedWrapper . MouseFlag . MouseMoveRelative ) = =
( ushort ) ManagedWrapper . MouseFlag . MouseMoveRelative
& & _mouseMoveRelativeMappings . ContainsKey ( i ) )
{
// Relative Mouse Move
hasSubscription = true ;
var mapping = _mouseMoveRelativeMappings [ i ] ;
if ( mapping . Block )
{
block = true ;
}
if ( mapping . Block ) block = true ;
var x = stroke . mouse . x ;
var y = stroke . mouse . y ;
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( x , y ) ) ;
if ( mapping . Concurrent )
{
ThreadPool . QueueUserWorkItem ( threadProc = > mapping . Callback ( x , y ) ) ;
}
else if ( _workers . ContainsKey ( i ) & & _workers [ i ] . ContainsKey ( 8 ) )
{
var worker = _workers [ i ] [ 8 ] ;
worker ? . Actions . Add ( ( ) = > mapping . Callback ( x , y ) ) ;
}
}
}
// If this key had no subscriptions, but Context Mode is set for this mouse...
// ... then set the Context before sending the button
if ( ! hasSubscription & & hasContext )
{
// Set Context
_contextCallbacks [ i ] ( 1 ) ;
}
if ( ! ( block ) )
{
ManagedWrapper . Send ( _deviceContext , i , ref stroke , 1 ) ;
}
if ( ! hasSubscription & & hasContext ) _contextCallbacks [ i ] ( 1 ) ; // Set Context
if ( ! block ) ManagedWrapper . Send ( _deviceContext , i , ref stroke , 1 ) ;
// If we are processing Context Mode, then Unset the context variable after sending the button
if ( ! hasSubscription & & hasContext )
{
// Unset Context
_contextCallbacks [ i ] ( 0 ) ;
}
if ( ! hasSubscription & & hasContext ) _contextCallbacks [ i ] ( 0 ) ;
}
}
Thread . Sleep ( 10 ) ;
}
}
@ -592,15 +639,48 @@ namespace AutoHotInterception
private class MappingOptions
{
public bool Block { get ; set ; }
public bool Concurrent { get ; set ; }
public dynamic Callback { get ; set ; }
}
# endregion
public void Dispose ( )
private class WorkerThread : IDisposable
{
SetThreadState ( false ) ;
private readonly Thread _worker ;
private bool _running ;
public WorkerThread ( )
{
Actions = new BlockingCollection < Action > ( ) ;
_worker = new Thread ( Run ) ;
_running = false ;
}
public BlockingCollection < Action > Actions { get ; }
public void Dispose ( )
{
if ( ! _running ) return ;
_worker . Interrupt ( ) ;
_worker . Join ( ) ;
_running = false ;
}
public void Start ( )
{
_worker . Start ( ) ;
_running = true ;
}
private void Run ( )
{
while ( true )
{
var action = Actions . Take ( ) ;
action . Invoke ( ) ;
}
}
}
# endregion
}
}
}