Skip to content

Commit c99e961

Browse files
Add EventLoopGlobalHook - a new global hook implementation
1 parent 1f93297 commit c99e961

File tree

8 files changed

+1222
-15
lines changed

8 files changed

+1222
-15
lines changed

SharpHook.Tests/EventLoopGlobalHookTests.cs

Lines changed: 977 additions & 0 deletions
Large diffs are not rendered by default.

SharpHook/BasicGlobalHookBase.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ public void Run()
8282
this.ThrowIfRunning();
8383
this.ThrowIfDisposed();
8484

85+
this.BeforeRun();
86+
8587
UioHookResult result;
8688

8789
try
@@ -121,6 +123,8 @@ public Task RunAsync()
121123
this.ThrowIfRunning();
122124
this.ThrowIfDisposed();
123125

126+
this.BeforeRun();
127+
124128
var source = new TaskCompletionSource<object?>();
125129

126130
var thread = new Thread(() =>
@@ -180,6 +184,8 @@ public void Stop()
180184
throw new HookException(result, this.FormatStopFailureMessage(result));
181185
}
182186
}
187+
188+
this.AfterStop();
183189
}
184190

185191
/// <summary>
@@ -192,6 +198,11 @@ public void Stop()
192198
/// </remarks>
193199
public void Dispose()
194200
{
201+
if (this.IsDisposed)
202+
{
203+
return;
204+
}
205+
195206
this.Dispose(true);
196207
GC.SuppressFinalize(this);
197208
}
@@ -203,7 +214,24 @@ public void Dispose()
203214
protected abstract void HandleHookEvent(ref UioHookEvent e);
204215

205216
/// <summary>
206-
/// Disposes of the global hook, stopping it if it is running.
217+
/// Defines actions to be done before the global hook is started. The default implementation does nothing, but
218+
/// it may change in a future version, so calling <see cref="BasicGlobalHookBase.BeforeRun" /> in an overriden
219+
/// method is recommended.
220+
/// </summary>
221+
protected virtual void BeforeRun()
222+
{ }
223+
224+
/// <summary>
225+
/// Defines actions to be done after the global hook is stopped. The default implementation does nothing, but
226+
/// it may change in a future version, so calling <see cref="BasicGlobalHookBase.BeforeRun" /> in an overriden
227+
/// method is recommended.
228+
/// </summary>
229+
protected virtual void AfterStop()
230+
{ }
231+
232+
/// <summary>
233+
/// Disposes of the global hook, stopping it if it is running. This method will not be called if the global hook is
234+
/// already disposed.
207235
/// </summary>
208236
/// <param name="disposing">
209237
/// <see langword="true" /> if the method is called from the <see cref="Dispose()" /> method.
@@ -212,11 +240,6 @@ public void Dispose()
212240
/// <exception cref="HookException">Stopping the hook has failed.</exception>
213241
protected virtual void Dispose(bool disposing)
214242
{
215-
if (this.IsDisposed)
216-
{
217-
return;
218-
}
219-
220243
this.IsDisposed = true;
221244

222245
if (this.IsRunning)

SharpHook/EventLoopGlobalHook.cs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System.Collections.Concurrent;
2+
3+
namespace SharpHook;
4+
5+
/// <summary>
6+
/// Represents an implementation of <see cref="IGlobalHook" /> which dispatches events to an event loop running on a
7+
/// separate dedicated thread.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// The event handlers will run on a separate thread. This way the hook itself will not be blocked if the handlers are
12+
/// long-running. The exception is the <see cref="IGlobalHook.HookDisabled" /> event which will run on the same thread
13+
/// on which the hook itself is running since at that point it doesn't matter anymore that the hook is not blocked.
14+
/// </para>
15+
/// <para>
16+
/// Setting <see cref="HookEventArgs.SuppressEvent" /> inside the handlers will have no effect as they are run
17+
/// on another thread.
18+
/// </para>
19+
/// </remarks>
20+
/// <seealso cref="IGlobalHook" />
21+
/// <seealso cref="GlobalHookBase" />
22+
/// <seealso cref="SimpleGlobalHook" />
23+
/// <seealso cref="TaskPoolGlobalHook" />
24+
public sealed class EventLoopGlobalHook : GlobalHookBase
25+
{
26+
#if NET9_0_OR_GREATER
27+
private readonly Lock syncRoot = new();
28+
#else
29+
private readonly object syncRoot = new();
30+
#endif
31+
32+
private readonly BlockingCollection<UioHookEvent> eventLoop = [];
33+
private bool eventLoopStarted = false;
34+
35+
/// <summary>
36+
/// Initializes a new instance of <see cref="EventLoopGlobalHook" />.
37+
/// </summary>
38+
/// <param name="globalHookType">The global hook type.</param>
39+
/// <param name="globalHookProvider">
40+
/// The underlying global hook provider, or <see langword="null" /> to use the default one.
41+
/// </param>
42+
/// <param name="runAsyncOnBackgroundThread">
43+
/// <see langword="true" /> if <see cref="IBasicGlobalHook.RunAsync" /> should run the hook on a background thread.
44+
/// Otherwise, <see langword="false" />.
45+
/// </param>
46+
[SuppressMessage(
47+
"Style", "IDE0290:Use primary constructor", Justification = "Primary constructors don't support XML comments")]
48+
public EventLoopGlobalHook(
49+
GlobalHookType globalHookType = GlobalHookType.All,
50+
IGlobalHookProvider? globalHookProvider = null,
51+
bool runAsyncOnBackgroundThread = false)
52+
: base(globalHookType, globalHookProvider, runAsyncOnBackgroundThread)
53+
{ }
54+
55+
/// <summary>
56+
/// Starts the event loop.
57+
/// </summary>
58+
protected override void BeforeRun()
59+
{
60+
if (!this.eventLoopStarted)
61+
{
62+
lock (this.syncRoot)
63+
{
64+
if (!this.eventLoopStarted)
65+
{
66+
new Thread(this.RunEventLoop).Start();
67+
this.eventLoopStarted = true;
68+
}
69+
}
70+
}
71+
}
72+
73+
/// <summary>
74+
/// Handles the hook event.
75+
/// </summary>
76+
/// <param name="e">The event to handle.</param>
77+
protected override void HandleHookEvent(ref UioHookEvent e)
78+
{
79+
if (!this.ShouldDispatchEvent(ref e))
80+
{
81+
return;
82+
}
83+
84+
if (e.Type != EventType.HookDisabled)
85+
{
86+
this.eventLoop.Add(e);
87+
} else
88+
{
89+
this.DispatchEvent(ref e);
90+
}
91+
}
92+
93+
/// <summary>
94+
/// Disposes of the global hook, stopping it if it is running, and completing the event loop.
95+
/// </summary>
96+
/// <param name="disposing">
97+
/// <see langword="true" /> if the method is called from the <see cref="BasicGlobalHookBase.Dispose()" /> method.
98+
/// Otherwise, <see langword="false" />.
99+
/// </param>
100+
/// <exception cref="HookException">Stopping the hook has failed.</exception>
101+
protected override void Dispose(bool disposing)
102+
{
103+
lock (this.syncRoot)
104+
{
105+
this.eventLoop.CompleteAdding();
106+
}
107+
108+
base.Dispose(disposing);
109+
}
110+
111+
private void RunEventLoop()
112+
{
113+
foreach (var @event in this.eventLoop.GetConsumingEnumerable())
114+
{
115+
var e = @event;
116+
this.DispatchEvent(ref e);
117+
}
118+
119+
lock (this.syncRoot)
120+
{
121+
this.eventLoop.Dispose();
122+
}
123+
}
124+
}

SharpHook/GlobalHookBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace SharpHook;
66
/// </summary>
77
/// <seealso cref="IGlobalHook" />
88
/// <seealso cref="SimpleGlobalHook" />
9+
/// <seealso cref="EventLoopGlobalHook" />
910
/// <seealso cref="TaskPoolGlobalHook" />
1011
/// <seealso cref="BasicGlobalHookBase" />
1112
public abstract class GlobalHookBase : BasicGlobalHookBase, IGlobalHook
@@ -36,7 +37,8 @@ protected GlobalHookBase(
3637
/// <param name="e">The event to handle.</param>
3738
/// <remarks>
3839
/// Derived classes should call <see cref="DispatchEvent(ref UioHookEvent)" /> inside this method to raise the
39-
/// appropriate event.
40+
/// appropriate event. They can also call <see cref="ShouldDispatchEvent(ref UioHookEvent)" /> to determine whether
41+
/// to attempt dispatching the event at all.
4042
/// </remarks>
4143
protected override abstract void HandleHookEvent(ref UioHookEvent e);
4244

SharpHook/README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ libuiohook also provides functions to get various system properties. The corresp
4343

4444
### Global Hooks
4545

46-
SharpHook provides the `IGlobalHook` interface along with two default implementations which you can use to control the
46+
SharpHook provides the `IGlobalHook` interface along with three default implementations which you can use to control the
4747
hook and subscribe to its events. Here's a basic usage example:
4848

4949
```c#
5050
using SharpHook;
5151

5252
// ...
5353
54-
var hook = new TaskPoolGlobalHook();
54+
var hook = new EventLoopGlobalHook();
5555

5656
hook.HookEnabled += OnHookEnabled; // EventHandler<HookEventArgs>
5757
hook.HookDisabled += OnHookDisabled; // EventHandler<HookEventArgs>
@@ -98,15 +98,20 @@ a real difference only on Windows where there are two different global hooks –
9898
macOS and Linux, there is one hook for all events, and this simply enables filtering keyboard or mouse events out on
9999
these OSes.
100100

101-
SharpHook provides two implementations of `IGlobalHook`:
101+
SharpHook provides three implementations of `IGlobalHook`:
102102

103103
- `SharpHook.SimpleGlobalHook` runs all of its event handlers on the same thread on which the hook itself runs. This
104104
means that the handlers should generally be fast since they will block the hook from handling the events that follow if
105105
they run for too long.
106106

107+
- `SharpHook.EventLoopGlobalHook` runs all of its event handlers on a separate dedicated thread. On backpressure it will
108+
queue the remaining events which means that the hook will be able to process all events. This implementation should be
109+
preferred to `SimpleGlobalHook` except for very simple use-cases. But it has a downside – suppressing event propagation
110+
will be ignored since event handlers are run on another thread.
111+
107112
- `SharpHook.TaskPoolGlobalHook` runs all of its event handlers on other threads inside the default thread pool for
108-
tasks. The parallelism level of the handlers can be configured. On backpressure it will queue the remaining handlers.
109-
This means that the hook will be able to process all events. This implementation should be preferred to
113+
tasks. The parallelism level of the handlers can be configured. On backpressure it will queue the remaining events which
114+
means that the hook will be able to process all events. This implementation should be preferred to
110115
`SimpleGlobalHook` except for very simple use-cases. But it has a downside – suppressing event propagation will be
111116
ignored since event handlers are run on other threads.
112117

SharpHook/SharpHook.xml

Lines changed: 77 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)