• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

luttje / Key2Joy / 6557152774

18 Oct 2023 06:36AM UTC coverage: 52.519% (+39.8%) from 12.718%
6557152774

push

github

web-flow
Adding more tests, fixing bugs in the process (#48)

* Add config manager tests

* legacy config and mapping profile tests (should fix #42)

* remove comment, problem was caused by transfer across appdomain (or to/fro scripting environment)

* Test core functionality #48 + includes minor refactoring to be able to test + added docs

* Add interop tests + implement and test async test utility (refactors away from singletons)

* fix not all tests running in workflow

* config and interop tests

* Refactor and allow mocking global input hook class

* add capture action test

* Make Execute override optional for script only methods

* add dependency injection + refactor and try test gamepad service

* Refactor config singleton to using dependency injection

* add tests for scripting

* add tests for plugin set + fix plugin showing as loaded even if checksum match failed

* fix tests failing because it relied on config exist (I guess the test order was accidentally correct earlier, this means we should really fix cleanup so we catch this sooner)

* refactor docs code + fix wrong enum summary

* refactor docs builder and start testing it a bit

* fix cmd project structure

* ignore designer files in tests

* cleanup and refactor UI code + show latest version in help

* truncate listview action column

* allow user config to minimize app when pressing X (defaults to shut down app) resolves #45

696 of 1757 branches covered (0.0%)

Branch coverage included in aggregate %.

4597 of 4597 new or added lines in 138 files covered. (100.0%)

3619 of 6459 relevant lines covered (56.03%)

17089.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.6
/Core/Key2Joy.Core/LowLevelInput/GlobalInputHook.cs
1
using System;
2
using System.ComponentModel;
3
using System.Diagnostics;
4
using System.Runtime.InteropServices;
5

6
namespace Key2Joy.LowLevelInput;
7

8
// Based on these sources:
9
// - https://gist.github.com/Stasonix/3181083
10
// - https://stackoverflow.com/a/34384189
11
public class GlobalInputHook : IDisposable
12
{
13
    public const int WH_KEYBOARD_LL = 13;
14
    public const int WH_MOUSE_LL = 14;
15

16
    public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardInputEvent;
17

18
    public event EventHandler<GlobalMouseHookEventArgs> MouseInputEvent;
19

20
    public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
21

22
    private readonly IntPtr[] windowsHookHandles;
23
    private IntPtr user32LibraryHandle;
24
    private HookProc hookProc;
25
    private readonly IHookManager hookManager;
26

27
    public GlobalInputHook(IHookManager hookManager = null)
4✔
28
    {
29
        this.hookManager = hookManager ?? new NativeHookManager();
4!
30
        this.windowsHookHandles = new IntPtr[2];
4✔
31
        this.user32LibraryHandle = IntPtr.Zero;
4✔
32

33
        this.Initialize();
4✔
34
    }
4✔
35

36
    private void Initialize()
37
    {
38
        this.hookProc = this.LowLevelInputHook; // keep alive hookProc
4✔
39

40
        this.user32LibraryHandle = this.hookManager.LoadLibrary("User32");
4✔
41
        if (this.user32LibraryHandle == IntPtr.Zero)
4!
42
        {
43
            var errorCode = Marshal.GetLastWin32Error();
×
44
            throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
×
45
        }
46

47
        var windowsHooks = new int[] { WH_KEYBOARD_LL, WH_MOUSE_LL };
4✔
48

49
        for (var i = 0; i < windowsHooks.Length; i++)
24✔
50
        {
51
            var windowsHook = windowsHooks[i];
8✔
52

53
            this.windowsHookHandles[i] = this.hookManager.SetWindowsHookEx(windowsHook, this.hookProc, this.user32LibraryHandle, 0);
8✔
54

55
            if (this.windowsHookHandles[i] == IntPtr.Zero)
8!
56
            {
57
                var errorCode = Marshal.GetLastWin32Error();
×
58
                throw new Win32Exception(errorCode, $"Failed to adjust input hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
×
59
            }
60
        }
61
    }
4✔
62

63
    protected virtual void Dispose(bool disposing)
64
    {
65
        if (disposing)
4!
66
        {
67
            // Because we can unhook only in the same thread, not in garbage collector thread
68
            for (var i = 0; i < this.windowsHookHandles.Length; i++)
×
69
            {
70
                var windowsHookHandle = this.windowsHookHandles[i];
×
71

72
                if (windowsHookHandle != IntPtr.Zero)
×
73
                {
74
                    if (!this.hookManager.UnhookWindowsHookEx(windowsHookHandle))
×
75
                    {
76
                        var errorCode = Marshal.GetLastWin32Error();
×
77
                        throw new Win32Exception(errorCode, $"Failed to remove input hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
×
78
                    }
79
                    this.windowsHookHandles[i] = IntPtr.Zero;
×
80

81
                    // ReSharper disable once DelegateSubtraction
82
                    this.hookProc -= this.LowLevelInputHook;
×
83
                }
84
            }
85
        }
86

87
        if (this.user32LibraryHandle != IntPtr.Zero)
4✔
88
        {
89
            if (!this.hookManager.FreeLibrary(this.user32LibraryHandle)) // reduces reference to library by 1.
4!
90
            {
91
                var errorCode = Marshal.GetLastWin32Error();
×
92
                throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
×
93
            }
94
            this.user32LibraryHandle = IntPtr.Zero;
4✔
95
        }
96
    }
4✔
97

98
    public IntPtr LowLevelInputHook(int nCode, IntPtr wParam, IntPtr lParam)
99
    {
100
        var isInputHandled = false;
4✔
101
        var wparamTyped = wParam.ToInt32();
4✔
102

103
        if (Enum.IsDefined(typeof(KeyboardState), wparamTyped))
4✔
104
        {
105
            var o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
2✔
106
            var p = (LowLevelKeyboardInputEvent)o;
2✔
107

108
            GlobalKeyboardHookEventArgs eventArguments = new(p, (KeyboardState)wparamTyped);
2✔
109

110
            var handler = KeyboardInputEvent;
2✔
111
            handler?.Invoke(this, eventArguments);
2✔
112

113
            isInputHandled = eventArguments.Handled;
2✔
114
        }
115
        else if (Enum.IsDefined(typeof(MouseState), wparamTyped))
2✔
116
        {
117
            var o = Marshal.PtrToStructure(lParam, typeof(LowLevelMouseInputEvent));
2✔
118
            var p = (LowLevelMouseInputEvent)o;
2✔
119

120
            GlobalMouseHookEventArgs eventArguments = new(p, (MouseState)wparamTyped);
2✔
121

122
            var handler = MouseInputEvent;
2✔
123
            handler?.Invoke(this, eventArguments);
2✔
124

125
            isInputHandled = eventArguments.Handled;
2✔
126
        }
127

128
        return isInputHandled ? (IntPtr)1 : this.hookManager.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
4✔
129
    }
130

131
    ~GlobalInputHook()
132
    {
133
        this.Dispose(false);
4✔
134
    }
8✔
135

136
    public void Dispose()
137
    {
138
        this.Dispose(true);
×
139
        GC.SuppressFinalize(this);
×
140
    }
×
141
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc