134 lines
4.8 KiB
C#
134 lines
4.8 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Channels;
|
|
|
|
namespace Recordingtest.Recorder;
|
|
|
|
public sealed record RawEvent(long TimestampMs, string Kind, int X, int Y, uint Code, int WheelDelta, string? FocusedElementPath = null);
|
|
|
|
/// <summary>
|
|
/// Installs WH_KEYBOARD_LL and WH_MOUSE_LL hooks on a dedicated thread with its own message loop.
|
|
/// Pushes RawEvent into a Channel for the main loop to consume.
|
|
/// </summary>
|
|
public sealed class LowLevelHook : IDisposable
|
|
{
|
|
private readonly Channel<RawEvent> _channel;
|
|
private Thread? _thread;
|
|
private IntPtr _kbHook = IntPtr.Zero;
|
|
private IntPtr _mouseHook = IntPtr.Zero;
|
|
private NativeMethods.HookProc? _kbProc;
|
|
private NativeMethods.HookProc? _mouseProc;
|
|
private uint _threadId;
|
|
private volatile bool _running;
|
|
|
|
public LowLevelHook(Channel<RawEvent> channel)
|
|
{
|
|
_channel = channel;
|
|
}
|
|
|
|
public ChannelReader<RawEvent> Reader => _channel.Reader;
|
|
|
|
public void Start()
|
|
{
|
|
if (_running) return;
|
|
_running = true;
|
|
var ready = new ManualResetEventSlim(false);
|
|
_thread = new Thread(() => HookThreadMain(ready))
|
|
{
|
|
IsBackground = true,
|
|
Name = "LowLevelHookThread",
|
|
};
|
|
_thread.SetApartmentState(ApartmentState.STA);
|
|
_thread.Start();
|
|
ready.Wait();
|
|
}
|
|
|
|
private void HookThreadMain(ManualResetEventSlim ready)
|
|
{
|
|
_threadId = GetCurrentThreadId();
|
|
_kbProc = KeyboardProc;
|
|
_mouseProc = MouseProc;
|
|
var hMod = NativeMethods.GetModuleHandle(null);
|
|
_kbHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_KEYBOARD_LL, _kbProc, hMod, 0);
|
|
_mouseHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, _mouseProc, hMod, 0);
|
|
ready.Set();
|
|
|
|
while (_running && NativeMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0)
|
|
{
|
|
NativeMethods.TranslateMessage(ref msg);
|
|
NativeMethods.DispatchMessage(ref msg);
|
|
}
|
|
|
|
if (_kbHook != IntPtr.Zero) NativeMethods.UnhookWindowsHookEx(_kbHook);
|
|
if (_mouseHook != IntPtr.Zero) NativeMethods.UnhookWindowsHookEx(_mouseHook);
|
|
_kbHook = IntPtr.Zero;
|
|
_mouseHook = IntPtr.Zero;
|
|
}
|
|
|
|
private IntPtr KeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
|
|
{
|
|
if (nCode >= 0)
|
|
{
|
|
var data = System.Runtime.InteropServices.Marshal.PtrToStructure<NativeMethods.KBDLLHOOKSTRUCT>(lParam);
|
|
var msg = wParam.ToInt32();
|
|
var kind = msg switch
|
|
{
|
|
NativeMethods.WM_KEYDOWN or NativeMethods.WM_SYSKEYDOWN => "key_down",
|
|
NativeMethods.WM_KEYUP or NativeMethods.WM_SYSKEYUP => "key_up",
|
|
_ => "key",
|
|
};
|
|
_channel.Writer.TryWrite(new RawEvent(NowMs(), kind, 0, 0, data.vkCode, 0));
|
|
}
|
|
return NativeMethods.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
|
|
}
|
|
|
|
private IntPtr MouseProc(int nCode, IntPtr wParam, IntPtr lParam)
|
|
{
|
|
if (nCode >= 0)
|
|
{
|
|
var data = System.Runtime.InteropServices.Marshal.PtrToStructure<NativeMethods.MSLLHOOKSTRUCT>(lParam);
|
|
var msg = wParam.ToInt32();
|
|
var kind = msg switch
|
|
{
|
|
NativeMethods.WM_LBUTTONDOWN => "mouse_down_l",
|
|
NativeMethods.WM_LBUTTONUP => "mouse_up_l",
|
|
NativeMethods.WM_RBUTTONDOWN => "mouse_down_r",
|
|
NativeMethods.WM_RBUTTONUP => "mouse_up_r",
|
|
NativeMethods.WM_MBUTTONDOWN => "mouse_down_m",
|
|
NativeMethods.WM_MBUTTONUP => "mouse_up_m",
|
|
NativeMethods.WM_MOUSEWHEEL => "wheel",
|
|
NativeMethods.WM_MOUSEMOVE => "move",
|
|
_ => "mouse",
|
|
};
|
|
int wheel = 0;
|
|
if (msg == NativeMethods.WM_MOUSEWHEEL)
|
|
{
|
|
wheel = (short)((data.mouseData >> 16) & 0xFFFF);
|
|
}
|
|
_channel.Writer.TryWrite(new RawEvent(NowMs(), kind, data.pt.x, data.pt.y, 0, wheel));
|
|
}
|
|
return NativeMethods.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
|
|
}
|
|
|
|
private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
|
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
|
private static extern uint GetCurrentThreadId();
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_running) return;
|
|
_running = false;
|
|
if (_threadId != 0)
|
|
{
|
|
// Post a WM_QUIT to break GetMessage loop
|
|
PostThreadMessage(_threadId, 0x0012 /* WM_QUIT */, IntPtr.Zero, IntPtr.Zero);
|
|
}
|
|
_thread?.Join(1000);
|
|
_channel.Writer.TryComplete();
|
|
}
|
|
|
|
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam);
|
|
}
|