问题描述
我从 RIDI_DEVICENAME
那里得到了荒谬的行为。根据文档,
返回值
类型:UINT
如果成功,此函数返回一个非负数,表示复制到 pData
的字节数。
如果 pData
对数据来说不够大,函数返回 -1
。如果 pData
为NULL
,则函数返回零值。在这两种情况下,pcbSize
都设置为 pData
缓冲区所需的最小大小。
忽略 -1
不是 UINT
返回类型中可表示的值的明显问题,似乎该函数应该告诉我所需的缓冲区大小,如果我提供一个缓冲区这个大小,函数要么成功,要么至少遵循自己的失败规则。
但是,我根本没有看到这一点。在 Windows 10 上,当 pcbSize
为 null 时,函数的 Unicode 版本将 1
设置为 pData
,否则将其置之不理,在所有情况下都会失败。当 pcbSize
为 null 时,函数的 ANSI 版本将 2
设置为 pData
,否则将传入的任何值加倍,但仍然失败。
用于任一版本测试代码的标头:
#define WIN32_EXTRA_LEAN 1
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include <windows.h>
ANSI 测试代码:
std::string GetRawInputDeviceName( HANDLE hRaw )
{
UINT numChars = 0u;
INT validChars;
validChars = static_cast<INT>(::GetRawInputDeviceInfoA(hRaw,RIDI_DEVICENAME,nullptr,&numChars));
auto lasterror = ::GetLastError();
if (lasterror != ERROR_INSUFFICIENT_BUFFER) {
std::wcerr << L"Failed to get length of name of raw input device,retcode = " << validChars << L",last error = " << lasterror << L"\n";
return {};
}
std::string name;
name.resize(numChars);
validChars = static_cast<INT>(::GetRawInputDeviceInfoA(hRaw,&name[0],&numChars));
lasterror = ::GetLastError();
if (validChars > 0) {
name.resize(validChars);
return name;
}
else {
std::wcerr << L"Failed to get name of raw input device,last error = " << lasterror << L"\n";
return {};
}
}
Unicode 测试代码:
std::wstring GetRawInputDeviceName( HANDLE hRaw )
{
UINT numChars = 0u;
INT validChars;
validChars = static_cast<INT>(::GetRawInputDeviceInfoW(hRaw,last error = " << lasterror << L"\n";
return {};
}
std::wstring name;
name.resize(numChars);
validChars = static_cast<INT>(::GetRawInputDeviceInfoW(hRaw,last error = " << lasterror << L"\n";
return {};
}
}
在通过 RDP 的 Windows 10 上,我始终获得 ERROR_INSUFFICIENT_BUFFER
。
在以本地用户身份运行的 Windows 8.1 上,如果 ERROR_INSUFFICIENT_BUFFER
为空,我得到 pData
,当我提供缓冲区时,我得到失败 ((UINT)-1
) 和 {{1} } 返回零。
我也刚刚尝试提出一个可能足够大的缓冲区大小,但也失败了。
这是怎么回事,获取接口路径名的正确方法是什么,我需要管理权限还是先调用一些其他 API?我似乎在调用 GetLastError()
或使用 GetRawInputDeviceList
的 RIDI_DEVICEINFO
模式时没有任何问题...但我需要接口路径才能走得更远。
解决方法
GetRawInputDeviceName
在声明/实现/文档中有几个错误
事实上更正确的是将返回值声明为有符号(LONG
或 INT
)而不是 UINT
存在3种情况:
1. 函数返回负值(或者如果想要 -1
):这是错误
情况下,并且按照设计 - 必须设置最后一个错误。但真的不是
始终设置(实现错误)。
最常见的错误:
-
pcbSize 或 pData 指向无效或只读的内存位置。这种情况下的常见错误
ERROR_NOACCESS
(翻译自STATUS_ACCESS_VIOLATION
) -
hDevice 无效句柄 - 返回
ERROR_INVALID_HANDLE
-
uiCommand 无效的 RIDI_XXX 常量 -
ERROR_INVALID_PARAMETER
-
*pcbSize 对于数据来说不够大 - 在这种情况下,*pcbSize 设置为 pData 缓冲区所需的最小大小。
ERROR_INSUFFICIENT_BUFFER
再次 - 仅在这种情况下 (
-1
) 存在感调用GetLastError();
2. 函数返回 0 仅当 pData 为 NULL 时才有可能。 *pcbSize 设置为 pData 缓冲区所需的最小大小。
3. 函数返回正值 ( > 0) 这意味着这个计数
字节(在 RIDI_PREPARSEDDATA
或 RIDI_DEVICEINFO
的情况下)或
字符(如果RIDI_DEVICENAME
)写入缓冲区
所以这里的文档是错误的:
pcbSize [进,出]
指向包含数据大小的变量的指针,以字节为单位 pData。
在字符
中的情况下RIDI_DEVICENAME
因此在设计(返回值的类型 - 无符号)和混合字节/字符方面已经可见非常严重的问题。许多不同的情况。
但随后在实施中存在严重错误。在函数句柄的开头 hDevice 转换为指针。
PDEVICEINFO pDeviceInfo = HMValidateHandle(hDevice,TYPE_DEVICEINFO);
(如果返回 0 - 我们在使用 ERROR_INVALID_HANDLE
退出时得到 -1)。
在 DEVICEINFO
中存在 UNICODE_STRING ustrName
- 此名称并复制到用户模式
switch (uiCommand) {
case RIDI_DEVICENAME:
/*
* N.b. UNICODE_STRING counts the length by the BYTE count,not by the character count.
* Our APIs always treat the strings by the character count. Thus,for RIDI_DEVICNAME
* only,cbOutSize holds the character count,not the byte count,in spite of its
* name. Confusing,but cch is the way to be consistent.
*/
cbOutSize = pDeviceInfo->ustrName.Length / sizeof(WCHAR) + 1; // for Null terminator
break;
//...
}
与cbOutSize
相比需要cbBufferSize = *pcbSize;
和if (cbBufferSize >= cbOutSize)
api开始复制操作
存在下一个代码
case RIDI_DEVICENAME:
if (cbOutSize <= 2) { // !!!! error !!!!
retval = -1;
goto leave;
}
RtlCopyMemory(pData,pDeviceInfo->ustrName.Buffer,pDeviceInfo->ustrName.Length);
((WCHAR*)pData)[1] = '\\'; // convert nt prefix ( \??\ ) to win32 ( \\?\ )
((WCHAR*)pData)[cbOutSize - 1] = 0; // make it null terminated
break;
cbOutSize 在这里 - 是设备名称的 (len + 1)(我们无法控制)。因此,如果 name 长度为零 - 始终返回 -1
(error #1) 但未设置最后一个错误 ( error #2 )
当然存在并且错误 #3 - 为什么设备名称的长度为 0?这一定不是。但如果终端服务设备 - (在 UMB 总线上创建的虚拟鼠标/键盘设备) - 存在此结果。
api 的完整代码(在内核中)
UINT NtUserGetRawInputDeviceInfo(
HANDLE hDevice,UINT uiCommand,LPVOID pData,PUINT pcbSize)
{
UINT cbOutSize = 0;
UINT cbBufferSize;
int retval = 0;
EnterCrit(0,UserMode);
UserAtomicCheck uac;
try {
ProbeForRead(pcbSize,sizeof(UINT),sizeof(DWORD));
cbBufferSize = *pcbSize;
} except (EXCEPTION_EXECUTE_HANDLER) {
UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS
retval = -1;
goto leave1;
}
EnterDeviceInfoListCrit_();
PDEVICEINFO pDeviceInfo = HMValidateHandle(hDevice,TYPE_DEVICEINFO);
if (pDeviceInfo == NULL) {
UserSetLastError(ERROR_INVALID_HANDLE);
retval = -1;
goto leave;
}
/*
* Compute the size of the output and evaluate the uiCommand.
*/
switch (uiCommand) {
case RIDI_PREPARSEDDATA:
if (pDeviceInfo->type == DEVICE_TYPE_HID) {
cbOutSize = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.DescriptorSize;
} else {
cbOutSize = 0;
}
break;
case RIDI_DEVICENAME:
/*
* N.b. UNICODE_STRING counts the length by the BYTE count,not by the character count.
* Our APIs always treat the strings by the character count. Thus,for RIDI_DEVICNAME
* only,in spite of its
* name. Confusing,but cch is the way to be consistent.
*/
cbOutSize = pDeviceInfo->ustrName.Length / sizeof(WCHAR) + 1; // for Null terminator
break;
case RIDI_DEVICEINFO:
cbOutSize = sizeof(RID_DEVICE_INFO);
break;
default:
UserSetLastError(ERROR_INVALID_PARAMETER);
retval = -1;
goto leave;
}
if (pData == NULL) {
/*
* The app wants to get the required size.
*/
try {
ProbeForWrite(pcbSize,sizeof(DWORD));
*pcbSize = cbOutSize;
} except (EXCEPTION_EXECUTE_HANDLER) {
UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS
retval = -1;
goto leave;
}
retval = 0;
} else {
if (cbBufferSize >= cbOutSize) {
try {
ProbeForWrite(pData,cbBufferSize,sizeof(DWORD));
switch (uiCommand) {
case RIDI_PREPARSEDDATA:
if (pDeviceInfo->type == DEVICE_TYPE_HID) {
RtlCopyMemory(pData,pDeviceInfo->hid.pHidDesc->pPreparsedData,cbOutSize);
}
break;
case RIDI_DEVICENAME:
if (cbOutSize <= 2) { // !!!!
retval = -1;
goto leave;
}
RtlCopyMemory(pData,pDeviceInfo->ustrName.Length);
((WCHAR*)pData)[1] = '\\'; // make it null terminated
((WCHAR*)pData)[cbOutSize - 1] = 0; // make it null terminated
break;
case RIDI_DEVICEINFO:
{
PRID_DEVICE_INFO prdi = (PRID_DEVICE_INFO)pData;
ProbeForRead(prdi,sizeof(DWORD));
if (prdi->cbSize != cbOutSize) {
MSGERRORCLEANUP(ERROR_INVALID_PARAMETER);
}
ProbeForWrite(prdi,sizeof(RID_DEVICE_INFO),sizeof(DWORD));
RtlZeroMemory(prdi,sizeof(RID_DEVICE_INFO));
prdi->cbSize = cbOutSize;
switch (pDeviceInfo->type) {
case DEVICE_TYPE_HID:
prdi->dwType = RIM_TYPEHID;
prdi->hid.dwVendorId = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.VendorID;
prdi->hid.dwProductId = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.ProductID;
prdi->hid.dwVersionNumber = pDeviceInfo->hid.pHidDesc->hidCollectionInfo.VersionNumber;
prdi->hid.usUsagePage = pDeviceInfo->hid.pHidDesc->hidpCaps.UsagePage;
prdi->hid.usUsage = pDeviceInfo->hid.pHidDesc->hidpCaps.Usage;
break;
case DEVICE_TYPE_MOUSE:
prdi->dwType = RIM_TYPEMOUSE;
prdi->mouse.dwId = pDeviceInfo->mouse.Attr.MouseIdentifier;
prdi->mouse.dwNumberOfButtons = pDeviceInfo->mouse.Attr.NumberOfButtons;
prdi->mouse.dwSampleRate = pDeviceInfo->mouse.Attr.SampleRate;
break;
case DEVICE_TYPE_KEYBOARD:
prdi->dwType = RIM_TYPEKEYBOARD;
prdi->keyboard.dwType = GET_KEYBOARD_DEVINFO_TYPE(pDeviceInfo);
prdi->keyboard.dwSubType = GET_KEYBOARD_DEVINFO_SUBTYPE(pDeviceInfo);
prdi->keyboard.dwKeyboardMode = pDeviceInfo->keyboard.Attr.KeyboardMode;
prdi->keyboard.dwNumberOfFunctionKeys = pDeviceInfo->keyboard.Attr.NumberOfFunctionKeys;
prdi->keyboard.dwNumberOfIndicators = pDeviceInfo->keyboard.Attr.NumberOfIndicators;
prdi->keyboard.dwNumberOfKeysTotal = pDeviceInfo->keyboard.Attr.NumberOfKeysTotal;
break;
}
}
break;
default:
__assume(false);
}
} except (EXCEPTION_EXECUTE_HANDLER) {
UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS
retval = -1;
goto leave;
}
retval = cbOutSize;
} else {
/*
* The buffer size is too small.
* Returns error,storing the required size in *pcbSize.
*/
retval = -1;
try {
ProbeForWrite(pcbSize,sizeof(DWORD));
*pcbSize = cbOutSize;
UserSetLastError(ERROR_INSUFFICIENT_BUFFER);
} except (EXCEPTION_EXECUTE_HANDLER) {
UserSetLastError(RtlNtStatusToDosError(GetExceptionCode()));// ERROR_NOACCESS
retval = -1;
goto leave;
}
}
}
leave:
LeaveDeviceInfoListCrit_();
leave1:
UserSessionSwitchLeaveCrit();
return retval;
}
然后 GetRawInputDeviceInfoA
添加额外的错误比较 GetRawInputDeviceInfoW
- 来自 *pcbSize 的值由于某种原因乘以 2。但同样 - 在所有情况下都会出现此错误。
请注意 DeviceName(从 IRP_MN_QUERY_ID
上的驱动程序返回的字符串格式化有非常严格的限制:
如果驱动程序返回带有非法字符的 ID,系统将 错误检查。具有以下值的字符在 ID 中是非法的 对于此 IRP:
- 小于或等于 0x20 (' ')
- 大于 0x7F
- 等于 0x2C (',')
因此,即使在将 unicode 转换为 ansi 之后 - 设备名称的长度也将相同(所有符号 0x80 )。所以 Ansi 版本不需要 *2
缓冲区大小。
然后我已经查看了您代码中的错误 - 您在 ::GetLastError();
之后无条件调用 GetRawInputDeviceInfoW
- 但返回值仅在 api return -1
的情况下有意义
解释观察到的行为:
对于本地设备 api 一般工作正确(如果我们的代码没有错误)
对于终端服务设备 - 长度为 0 ustrName
。结果,如果我们在 pData 中传递 NULL - 返回值将是
pDeviceInfo->ustrName.Length / sizeof(WCHAR) + 1;
因为 pDeviceInfo->ustrName.Length == 0
- 1 将在 *pcbSize 内返回
如果 A 版本 - - 错误 - 2*1==2
将被返回。
但是当 e 在 pData 中不传递 NULL 时 - 我们陷入了这个
if (cbOutSize <= 2) { // !!!! error !!!!
retval = -1;
goto leave;
}
所以你可以通过任何大小的缓冲区,无论如何,因为 (cbOutSize <= 2)
- -1
将被返回并且最后一个错误未设置
可能的解决方案 - 首先 - 永远不要使用 ansi 版本 - GetRawInputDeviceInfoA
使用这个包装函数。
ULONG GetRawInputDeviceInfoExW(_In_opt_ HANDLE hDevice,_In_ UINT uiCommand,_Inout_updates_bytes_to_opt_(*pcbSize,*pcbSize) LPVOID pData,_Inout_ PUINT pcbSize)
{
switch (int i = GetRawInputDeviceInfoW(hDevice,uiCommand,pData,pcbSize))
{
case 0:
return ERROR_INSUFFICIENT_BUFFER;
case 1:
return ERROR_INVALID_NAME;
default:
if (0 > i)
{
return GetLastError();
}
*pcbSize = i;
return NOERROR;
}
}
用法示例:(/RTCs
必须禁用)
void Demo()
{
PRAWINPUTDEVICELIST pRawInputDeviceList = 0;
UINT uiNumDevices = 0;
UINT cch,cchAllocated = 0;
union {
PVOID buf;
PWSTR name;
};
buf = 0;
while (0 <= (int)GetRawInputDeviceList(pRawInputDeviceList,&uiNumDevices,sizeof(RAWINPUTDEVICELIST)))
{
if (pRawInputDeviceList)
{
do
{
HANDLE hDevice = pRawInputDeviceList->hDevice;
ULONG dwError;
while (ERROR_INSUFFICIENT_BUFFER == (dwError =
GetRawInputDeviceInfoExW(hDevice,RIDI_DEVICENAME,name,&(cch = cchAllocated))))
{
if (cch > cchAllocated)
{
cchAllocated = RtlPointerToOffset(buf = alloca((cch - cchAllocated) * sizeof(WCHAR)),pRawInputDeviceList) / sizeof(WCHAR);
}
else
{
__debugbreak();
}
}
if (dwError == NOERROR)
{
DbgPrint("[%p,%x %S]\n",hDevice,pRawInputDeviceList->dwType,name);
}
else
{
DbgPrint("error = %u\n",dwError);
}
} while (pRawInputDeviceList++,--uiNumDevices);
break;
}
pRawInputDeviceList = (PRAWINPUTDEVICELIST)alloca(uiNumDevices * sizeof(RAWINPUTDEVICELIST));
}
}
,
此代码在我的 PC 上运行良好。不确定,但确实可能是 RDP 问题。
UINT result = ::GetRawInputDeviceInfoW(m_Handle,nullptr,&size);
if (result == static_cast<UINT>(-1))
{
//PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
return false;
}
DCHECK_EQ(0u,result);
std::wstring buffer(size,0);
result = ::GetRawInputDeviceInfoW(m_Handle,buffer.data(),&size);
if (result == static_cast<UINT>(-1))
{
//PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
return false;
}
DCHECK_EQ(size,result);