Implement recorder PoC (#6)
This commit is contained in:
133
src/Recordingtest.Recorder/LowLevelHook.cs
Normal file
133
src/Recordingtest.Recorder/LowLevelHook.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
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);
|
||||
|
||||
/// <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);
|
||||
}
|
||||
Reference in New Issue
Block a user