EonaCat.HID/EonaCat.HID/Managers/HidManagerWindows.cs

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;
}
}