564 lines
22 KiB
C#
564 lines
22 KiB
C#
using EonaCat.HID.EventArguments;
|
|
using Microsoft.Win32.SafeHandles;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using static EonaCat.HID.Managers.Windows.NativeMethods;
|
|
|
|
namespace EonaCat.HID.Managers.Windows
|
|
{
|
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
|
|
|
internal sealed class HidManagerWindows : IHidManager, IDisposable
|
|
{
|
|
private const int DIGCF_PRESENT = 0x00000002;
|
|
private const int DIGCF_DEVICEINTERFACE = 0x00000010;
|
|
private const ushort HID_USAGE_PAGE_GENERIC = 0x01;
|
|
private const ushort HID_USAGE_GENERIC_MOUSE = 0x02;
|
|
|
|
private static Guid GUID_DEVINTERFACE_HID = new Guid("4d1e55b2-f16f-11cf-88cb-001111000030");
|
|
|
|
private readonly object _lock = new object();
|
|
|
|
// Monitor devices for connect/disconnect
|
|
private IntPtr _deviceNotificationHandle;
|
|
private WndProc _windowProcDelegate;
|
|
private IntPtr _messageWindowHandle;
|
|
private readonly Dictionary<string, IHid> _knownDevices = new();
|
|
|
|
public event EventHandler<HidEventArgs> OnDeviceInserted;
|
|
public event EventHandler<HidEventArgs> OnDeviceRemoved;
|
|
|
|
public HidManagerWindows()
|
|
{
|
|
InitializeMessageWindow();
|
|
RegisterForDeviceNotifications();
|
|
}
|
|
|
|
private void InitializeMessageWindow()
|
|
{
|
|
// Create hidden window to receive device change messages for insert/remove events
|
|
_windowProcDelegate = new WndProc(WindowProc);
|
|
WNDCLASS wc = new WNDCLASS()
|
|
{
|
|
lpfnWndProc = _windowProcDelegate,
|
|
lpszClassName = "HidDeviceNotificationWindow_" + Guid.NewGuid(),
|
|
style = 0,
|
|
cbClsExtra = 0,
|
|
cbWndExtra = 0,
|
|
hInstance = GetModuleHandle(null),
|
|
hbrBackground = IntPtr.Zero,
|
|
hCursor = IntPtr.Zero,
|
|
hIcon = IntPtr.Zero,
|
|
lpszMenuName = null
|
|
};
|
|
|
|
ushort classAtom = RegisterClass(ref wc);
|
|
if (classAtom == 0)
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register window class");
|
|
}
|
|
|
|
_messageWindowHandle = CreateWindowEx(
|
|
0,
|
|
wc.lpszClassName,
|
|
"",
|
|
0,
|
|
0, 0, 0, 0,
|
|
IntPtr.Zero,
|
|
IntPtr.Zero,
|
|
wc.hInstance,
|
|
IntPtr.Zero);
|
|
|
|
if (_messageWindowHandle == IntPtr.Zero)
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create message-only window");
|
|
}
|
|
}
|
|
|
|
private void RegisterForDeviceNotifications()
|
|
{
|
|
DEV_BROADCAST_DEVICEINTERFACE devInterface = new DEV_BROADCAST_DEVICEINTERFACE
|
|
{
|
|
dbcc_size = Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>(),
|
|
dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
|
|
dbcc_classguid = GUID_DEVINTERFACE_HID
|
|
};
|
|
|
|
IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf(devInterface));
|
|
Marshal.StructureToPtr(devInterface, buffer, false);
|
|
|
|
_deviceNotificationHandle = RegisterDeviceNotification(_messageWindowHandle, buffer, DEVICE_NOTIFY_WINDOW_HANDLE);
|
|
|
|
Marshal.FreeHGlobal(buffer);
|
|
|
|
if (_deviceNotificationHandle == IntPtr.Zero)
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register for device notifications");
|
|
}
|
|
}
|
|
|
|
private IntPtr WindowProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
|
|
{
|
|
if (msg == WM_DEVICECHANGE)
|
|
{
|
|
int eventType = wParam.ToInt32();
|
|
|
|
if (eventType == DBT_DEVICEARRIVAL || eventType == DBT_DEVICEREMOVECOMPLETE)
|
|
{
|
|
var hdr = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
|
|
if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
|
|
{
|
|
var devInterface = Marshal.PtrToStructure<DEV_BROADCAST_DEVICEINTERFACE>(lParam);
|
|
|
|
// Calculate pointer to string
|
|
IntPtr stringPtr = IntPtr.Add(lParam, Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>());
|
|
|
|
// Read null-terminated string from unmanaged memory
|
|
string devicePath = Marshal.PtrToStringUni(stringPtr);
|
|
if (!string.IsNullOrEmpty(devicePath))
|
|
{
|
|
try
|
|
{
|
|
if (eventType == DBT_DEVICEARRIVAL)
|
|
{
|
|
using (var testHandle = CreateFile(devicePath, 0,
|
|
FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
|
|
{
|
|
if (testHandle.IsInvalid)
|
|
return DefWindowProc(hwnd, msg, wParam, lParam);
|
|
}
|
|
|
|
var device = new HidWindows(devicePath);
|
|
device.Setup();
|
|
DeviceInsertedInternal(device);
|
|
}
|
|
else if (eventType == DBT_DEVICEREMOVECOMPLETE)
|
|
{
|
|
var device = new HidWindows(devicePath);
|
|
DeviceRemovedInternal(device);
|
|
}
|
|
|
|
return IntPtr.Zero;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[WindowProc] HID device change error: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return DefWindowProc(hwnd, msg, wParam, lParam);
|
|
}
|
|
|
|
private void DeviceInsertedInternal(IHid device)
|
|
{
|
|
if (!_knownDevices.ContainsKey(device.DevicePath))
|
|
{
|
|
_knownDevices[device.DevicePath] = device;
|
|
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
|
|
}
|
|
}
|
|
|
|
private void DeviceRemovedInternal(IHid device)
|
|
{
|
|
if (_knownDevices.ContainsKey(device.DevicePath))
|
|
{
|
|
device = _knownDevices[device.DevicePath];
|
|
_knownDevices.Remove(device.DevicePath);
|
|
OnDeviceRemoved?.Invoke(this, new HidEventArgs(device));
|
|
}
|
|
}
|
|
|
|
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
|
|
{
|
|
var list = new List<IHid>();
|
|
IntPtr devInfo = SetupDiGetClassDevs(
|
|
ref GUID_DEVINTERFACE_HID,
|
|
null,
|
|
IntPtr.Zero,
|
|
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
|
|
|
if (devInfo == IntPtr.Zero || devInfo.ToInt64() == -1)
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed");
|
|
}
|
|
|
|
try
|
|
{
|
|
var iface = new SP_DEVICE_INTERFACE_DATA
|
|
{
|
|
cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA))
|
|
};
|
|
|
|
for (uint index = 0; ; index++)
|
|
{
|
|
bool ok = SetupDiEnumDeviceInterfaces(
|
|
devInfo, IntPtr.Zero, ref GUID_DEVINTERFACE_HID, index, ref iface);
|
|
|
|
if (!ok)
|
|
{
|
|
int error = Marshal.GetLastWin32Error();
|
|
if (error == ERROR_NO_MORE_ITEMS)
|
|
{
|
|
break;
|
|
}
|
|
|
|
throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed");
|
|
}
|
|
|
|
// Step 1: Get required size
|
|
uint requiredSize = 0;
|
|
bool sizeResult = SetupDiGetDeviceInterfaceDetail(
|
|
devInfo,
|
|
ref iface,
|
|
IntPtr.Zero,
|
|
0,
|
|
ref requiredSize,
|
|
IntPtr.Zero);
|
|
|
|
// This call should fail with ERROR_INSUFFICIENT_BUFFER
|
|
int sizeError = Marshal.GetLastWin32Error();
|
|
if (sizeError != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Step 2: Allocate buffer for detail data
|
|
IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize);
|
|
try
|
|
{
|
|
// Step 3: Set cbSize at start of allocated memory
|
|
// CRITICAL: cbSize must be size of SP_DEVICE_INTERFACE_DETAIL_DATA structure
|
|
// On x86: 6 bytes (4 for cbSize + 2 for alignment)
|
|
// On x64: 8 bytes (4 for cbSize + 4 for alignment)
|
|
int cbSize = IntPtr.Size == 8 ? 8 : 6;
|
|
Marshal.WriteInt32(detailDataBuffer, cbSize);
|
|
|
|
// Step 4: Now get the device interface detail
|
|
bool success = SetupDiGetDeviceInterfaceDetail(
|
|
devInfo,
|
|
ref iface,
|
|
detailDataBuffer,
|
|
requiredSize,
|
|
ref requiredSize,
|
|
IntPtr.Zero);
|
|
|
|
if (!success)
|
|
{
|
|
int detailError = Marshal.GetLastWin32Error();
|
|
throw new Win32Exception(detailError, $"SetupDiGetDeviceInterfaceDetail failed with error {detailError}");
|
|
}
|
|
|
|
// Step 5: Read device path string (starts at offset 4)
|
|
// The DevicePath is a null-terminated string that starts at offset 4
|
|
IntPtr pDevicePathName = IntPtr.Add(detailDataBuffer, 4);
|
|
string devicePath = Marshal.PtrToStringAuto(pDevicePathName);
|
|
|
|
if (string.IsNullOrEmpty(devicePath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Step 6: Create device, filter and add
|
|
try
|
|
{
|
|
// First try to open the device with minimal access to check if it's accessible
|
|
using (var testHandle = CreateFile(devicePath, 0, // No access requested
|
|
FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
|
|
{
|
|
if (testHandle.IsInvalid)
|
|
{
|
|
// Device not accessible, skip it
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var device = new HidWindows(devicePath);
|
|
device.Setup();
|
|
|
|
if (vendorId.HasValue && device.VendorId != vendorId.Value ||
|
|
productId.HasValue && device.ProductId != productId.Value)
|
|
{
|
|
device.Dispose();
|
|
continue;
|
|
}
|
|
|
|
list.Add(device);
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
// Device is in use or access denied - skip silently
|
|
continue;
|
|
}
|
|
catch (Exception ex) when (ex.Message.Contains("HidP_GetCaps") ||
|
|
ex.Message.Contains("The parameter is incorrect") ||
|
|
ex.Message.Contains("Access is denied"))
|
|
{
|
|
// Common HID access failures - skip these devices
|
|
System.Diagnostics.Debug.WriteLine($"Skipping inaccessible HID device {devicePath}: {ex.Message}");
|
|
continue;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Other unexpected errors - log but continue
|
|
System.Diagnostics.Debug.WriteLine($"Failed to create device for path {devicePath}: {ex.Message}");
|
|
continue;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(detailDataBuffer);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
SetupDiDestroyDeviceInfoList(devInfo);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_deviceNotificationHandle != IntPtr.Zero)
|
|
{
|
|
UnregisterDeviceNotification(_deviceNotificationHandle);
|
|
_deviceNotificationHandle = IntPtr.Zero;
|
|
}
|
|
|
|
if (_messageWindowHandle != IntPtr.Zero)
|
|
{
|
|
DestroyWindow(_messageWindowHandle);
|
|
_messageWindowHandle = IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class NativeMethods
|
|
{
|
|
public const int ERROR_INSUFFICIENT_BUFFER = 122;
|
|
public const int ERROR_NO_MORE_ITEMS = 259;
|
|
|
|
public const int FILE_FLAG_OVERLAPPED = 0x40000000;
|
|
public const int FILE_SHARE_READ = 0x00000001;
|
|
public const int FILE_SHARE_WRITE = 0x00000002;
|
|
|
|
public const int OPEN_EXISTING = 3;
|
|
public const int GENERIC_READ = unchecked((int)0x80000000);
|
|
public const int GENERIC_WRITE = 0x40000000;
|
|
public const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;
|
|
public const int HIDP_STATUS_SUCCESS = 0x110000;
|
|
|
|
public const int WM_DEVICECHANGE = 0x0219;
|
|
public const int DBT_DEVICEARRIVAL = 0x8000;
|
|
public const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
|
|
public const int DBT_DEVTYP_DEVICEINTERFACE = 5;
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct WNDCLASS
|
|
{
|
|
public uint style;
|
|
[MarshalAs(UnmanagedType.FunctionPtr)]
|
|
public WndProc lpfnWndProc;
|
|
public int cbClsExtra;
|
|
public int cbWndExtra;
|
|
public IntPtr hInstance;
|
|
public IntPtr hIcon;
|
|
public IntPtr hCursor;
|
|
public IntPtr hbrBackground;
|
|
[MarshalAs(UnmanagedType.LPTStr)]
|
|
public string lpszMenuName;
|
|
[MarshalAs(UnmanagedType.LPTStr)]
|
|
public string lpszClassName;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct DEV_BROADCAST_HDR
|
|
{
|
|
public int dbch_size;
|
|
public int dbch_devicetype;
|
|
public int dbch_reserved;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct DEV_BROADCAST_DEVICEINTERFACE
|
|
{
|
|
public int dbcc_size;
|
|
public int dbcc_devicetype;
|
|
public int dbcc_reserved;
|
|
public Guid dbcc_classguid;
|
|
}
|
|
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct SP_DEVICE_INTERFACE_DATA
|
|
{
|
|
public int cbSize;
|
|
public Guid InterfaceClassGuid;
|
|
public int Flags;
|
|
public IntPtr Reserved;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
|
public struct SP_DEVICE_INTERFACE_DETAIL_DATA
|
|
{
|
|
public int cbSize;
|
|
|
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
|
public string DevicePath;
|
|
}
|
|
|
|
|
|
// Declare delegate for WndProc
|
|
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
public static extern ushort RegisterClass(ref WNDCLASS lpWndClass);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
public static extern IntPtr CreateWindowEx(
|
|
int dwExStyle,
|
|
string lpClassName,
|
|
string lpWindowName,
|
|
int dwStyle,
|
|
int x, int y, int nWidth, int nHeight,
|
|
IntPtr hWndParent,
|
|
IntPtr hMenu,
|
|
IntPtr hInstance,
|
|
IntPtr lpParam);
|
|
|
|
[DllImport("user32.dll")]
|
|
public static extern bool DestroyWindow(IntPtr hWnd);
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
|
|
public static extern IntPtr GetModuleHandle(string lpModuleName);
|
|
|
|
[DllImport("user32.dll")]
|
|
public static extern IntPtr DefWindowProc(IntPtr hwnd, uint uMsg, IntPtr wParam, IntPtr lParam);
|
|
|
|
[DllImport("setupapi.dll", SetLastError = true)]
|
|
public static extern IntPtr SetupDiGetClassDevs(
|
|
ref Guid ClassGuid,
|
|
[MarshalAs(UnmanagedType.LPTStr)] string Enumerator,
|
|
IntPtr hwndParent,
|
|
uint Flags);
|
|
|
|
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
|
public static extern bool SetupDiEnumDeviceInterfaces(
|
|
IntPtr DeviceInfoSet,
|
|
IntPtr DeviceInfoData,
|
|
ref Guid InterfaceClassGuid,
|
|
uint MemberIndex,
|
|
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);
|
|
|
|
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
|
public static extern bool SetupDiGetDeviceInterfaceDetail(
|
|
IntPtr DeviceInfoSet,
|
|
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
|
|
IntPtr DeviceInterfaceDetailData,
|
|
uint DeviceInterfaceDetailDataSize,
|
|
ref uint RequiredSize,
|
|
IntPtr DeviceInfoData);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
|
public static extern SafeFileHandle CreateFile(
|
|
string lpFileName,
|
|
uint dwDesiredAccess,
|
|
FileShare dwShareMode,
|
|
IntPtr lpSecurityAttributes,
|
|
FileMode dwCreationDisposition,
|
|
uint dwFlagsAndAttributes,
|
|
IntPtr hTemplateFile);
|
|
|
|
[DllImport("setupapi.dll", SetLastError = true)]
|
|
public static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
public static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, IntPtr NotificationFilter, uint Flags);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
public static extern bool UnregisterDeviceNotification(IntPtr Handle);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
|
public static extern SafeFileHandle CreateFile(
|
|
string lpFileName,
|
|
int dwDesiredAccess,
|
|
int dwShareMode,
|
|
IntPtr lpSecurityAttributes,
|
|
int dwCreationDisposition,
|
|
int dwFlagsAndAttributes,
|
|
IntPtr hTemplateFile);
|
|
|
|
// HID APIs
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_GetAttributes(IntPtr hidDeviceObject, ref HidDeviceAttributes attributes);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_GetManufacturerString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_GetProductString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_GetSerialNumberString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr preparsedData);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_FreePreparsedData(IntPtr preparsedData);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_SetFeature(IntPtr hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_GetFeature(IntPtr hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
|
|
|
|
[DllImport("hid.dll", SetLastError = true)]
|
|
public static extern bool HidD_GetInputReport(IntPtr hidDeviceObject, byte[] buffer, int bufferLength);
|
|
|
|
// HIDP status
|
|
[DllImport("hid.dll")]
|
|
public static extern int HidP_GetCaps(IntPtr preparsedData, out HIDP_CAPS capabilities);
|
|
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct HIDP_CAPS
|
|
{
|
|
public ushort Usage;
|
|
public ushort UsagePage;
|
|
public ushort InputReportByteLength;
|
|
public ushort OutputReportByteLength;
|
|
public ushort FeatureReportByteLength;
|
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
|
|
public ushort[] Reserved;
|
|
public ushort NumberLinkCollectionNodes;
|
|
public ushort NumberInputButtonCaps;
|
|
public ushort NumberInputValueCaps;
|
|
public ushort NumberInputDataIndices;
|
|
public ushort NumberOutputButtonCaps;
|
|
public ushort NumberOutputValueCaps;
|
|
public ushort NumberOutputDataIndices;
|
|
public ushort NumberFeatureButtonCaps;
|
|
public ushort NumberFeatureValueCaps;
|
|
public ushort NumberFeatureDataIndices;
|
|
}
|
|
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
internal struct HidDeviceAttributes
|
|
{
|
|
public int Size;
|
|
public ushort VendorID;
|
|
public ushort ProductID;
|
|
public ushort VersionNumber;
|
|
}
|
|
} |