【Win7】弹出U盘

概述:Win7 弹出U盘的方式汇总

参考文章:

问题描述

用户环境:Windows 7 x64 旗舰版

客户反馈使用 [U盘小助手] 弹出U盘时报错。弹出失败,每次都需要强制弹出才能弹出U盘,且强制弹出后 [Explorer] 界面仍会显示 U盘 对应的磁盘(没有容量)。但是使用系统的弹出是可以弹出的,且U盘图标弹出逻辑也正常。

异常情况就是上述两者发生:

排查思路

句柄未关闭

排查是否有未关闭的句柄导致弹出失败。具体就是使用 [Process Explorer] 查找 [U盘] 句柄。

被拦截

  1. 查看系统日志
  2. 查看 API 调用

查看代码

如果以上均排查不出来问题,那就只能看是不是代码写的有问题了。

以上只是常见的排查思路,具体问题具体分析。

弹出调用失败

代码中调用弹出的接口是 CM_Request_Device_Eject_Ex

问题原因

驱动问题,调用 CM_Request_Device_Eject_Ex 弹出时 pVetoType 返回 PNP_VetoIllegalDeviceRequest,表示当前设备不支持。这个在别的机器上是无法复现,截止目前我也不知道这个问题的根源是什么。只能通过别的方式弹出。

目前看只有 CM_Request_Device_Eject_Ex 才能从设备管理器界面弹出U盘。

1
2
3
4
5
6
7
8
9
10
11
CMAPI
CONFIGRET
WINAPI
CM_Request_Device_Eject_ExW(
_In_ DEVINST dnDevInst,
_Out_opt_ PPNP_VETO_TYPE pVetoType,
_Out_writes_opt_(ulNameLength) LPWSTR pszVetoName,
_In_ ULONG ulNameLength,
_In_ ULONG ulFlags,
_In_opt_ HMACHINE hMachine
);

CM_Request_Device_Eject_Ex 接口不支持调用时,常见的方式就是调用 DeviceIOControl 来强制弹出U盘。示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
STDAPI_(BOOL) EjectVolumeForce(DWORD dwVolLetter)
{
MYTRACE(L"Enter EjectVolumeForce");

WCHAR wszVolume[20] = {};
WCHAR wszVolShort[20] = {};
DWORD dwReturn = 0;
HANDLE hVolume = INVALID_HANDLE_VALUE;
BOOL bRet = FALSE;

StringCchPrintf(wszVolShort, ARRAYSIZE(wszVolShort), L"%c:\\", dwVolLetter);
StringCchPrintf(wszVolume, ARRAYSIZE(wszVolume), L"\\\\.\\%c:", dwVolLetter);

hVolume = CreateFile(wszVolume, GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hVolume == INVALID_HANDLE_VALUE)
{
MYTRACE(L"ERROR! Can not open volume!\n");
return FALSE;
}
do
{
// 硬盘强制弹出失败率大
if (DRIVE_FIXED == GetDriveType(wszVolShort))
{
MYTRACE(L"DRIVE_FIXED == GetDriveType(wszVolShort)");
bRet = FALSE;
break;
}
MYTRACE(L"GetDriveType(wszVolShort) == %d", GetDriveType(wszVolShort));

// 开始暴力弹出
if (DeviceIoControl(hVolume, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL, 0, &dwReturn, NULL))
{
DWORD dwTmp = 0;
MYTRACE(L"IOCTL_STORAGE_MEDIA_REMOVAL");
bRet = DeviceIoControl(hVolume, IOCTL_STORAGE_MEDIA_REMOVAL, &dwTmp, sizeof(dwTmp), NULL, 0, &dwReturn, NULL);
MYTRACE(L"IOCTL_STORAGE_MEDIA_REMOVAL, bRet==%d", bRet);
bRet = DeviceIoControl(hVolume, IOCTL_DISK_EJECT_MEDIA, NULL, 0, NULL, 0, &dwReturn, NULL);
MYTRACE(L"IOCTL_DISK_EJECT_MEDIA bRet==%d", bRet);
bRet = DeviceIoControl(hVolume, IOCTL_STORAGE_EJECT_MEDIA, NULL, 0, NULL, 0, &dwReturn, NULL);
MYTRACE(L"IOCTL_STORAGE_EJECT_MEDIA bRet==%d", bRet);

}
// 弹出是否成功
if (!PathFileExists(wszVolShort))
{
DeleteJunctionPoint(dwVolLetter);
bRet = TRUE;
break;
}
else
{
bRet = FALSE;
break;
}
} while (0);

if (INVALID_HANDLE_VALUE != hVolume)
{
CloseHandle(hVolume);
hVolume = INVALID_HANDLE_VALUE;
}

return bRet;
}

在大多是机器都是正常的,但是在该客户的机器,调用上述代码后,虽然U盘设备已经移除,但是explorer 仍会有U盘图标显示。对此,我的解决方案就是刷新一下 explorer 界面,让已弹出的U盘不显示。

解决思路

总共试了以下多个 API 尝试通知到 explorer。

API 1 DeleteVolumeMountPointW (不要用)

由于尝试了多个 API 无果后,就尝试了调用了 以下接口,导致了后续一系列问题,在此避坑,建议了解当前函数的使用场景后再决定是否调用。

1
DeleteVolumeMountPointW

API 2 DefineDosDevice

DefineDosDeviceA 函数 (winbase.h) - Win32 apps | Microsoft Learn
DefineDosDevice 会删除驱动器号。但是在。

1
2
3
4
5
6
7
8
9
10
void RemoveDrive(char driveLetter) 
{
MYTRACE(L"RemoveDrive");

wchar_t deviceName[] = L"\\??\\X:";
deviceName[4] = driveLetter;

// 移除设备挂载点
DefineDosDevice(DDD_REMOVE_DEFINITION, deviceName, NULL);
}

API 3 SendMessage

通过 SendMessage 发送 WM_DEVICECHANGE 消息给所有窗口。

1
2
3
4
5
6
7
8
9
10
11
void NotifyDeviceRemoval(DWORD dwVolLetter) 
{
MYTRACE(L"NotifyDeviceRemoval");
DEV_BROADCAST_VOLUME dbv = { 0 };
dbv.dbcv_size = sizeof(DEV_BROADCAST_VOLUME);
dbv.dbcv_devicetype = DBT_DEVTYP_VOLUME;
dbv.dbcv_unitmask = 1 << (dwVolLetter - L'A');

SendMessage(HWND_BROADCAST, WM_DEVICECHANGE, DBT_DEVICEREMOVECOMPLETE, (LPARAM)&dbv); // 这一步其实已经是可以了,但是这个客户机器还是不行,调用后设备管理器界面会有延迟,但是 explorer 界面会立即刷新。
SendMessage(HWND_BROADCAST, WM_DEVICECHANGE, DBT_DEVICEREMOVECOMPLETE, 0);
}

API 4 RunDLL32.EXE

调用 RunDLL32.EXE 来实现弹出。以下两种方法均不可行,有需求可以深入研究下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void EjectDriveWithShell(char driveLetter) 
{
MYTRACE(L"EjectDriveWithShell");
wchar_t command[] = L"RunDLL32.EXE shell32.dll,Control_RunDLL hotplug.dll";
ShellExecute(NULL, L"open", command, NULL, NULL, SW_SHOWNORMAL);
}

// RunDLL32.EXE shell32.dll,SHInvokeCommand 0 "EjectPC" "%c:\\"
void EjectDriveWithShell(char driveLetter)
{
MYTRACE(L"EjectDriveWithShell");
wchar_t command[MAX_PATH] = {0};
wsprintf(command, L"RunDLL32.EXE shell32.dll,SHInvokeCommand 0 \"EjectPC\" \"%c:\\\\\"", driveLetter);
ShellExecute(NULL, L"open", command, NULL, NULL, SW_SHOWNORMAL);
}

API 5 重新挂载达到刷新目的

结果:同样不生效,U盘设备移除,但是 Expolorer 图标仍在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void RemountDrive(char driveLetter) 
{
MYTRACE(L"RemountDrive");
wchar_t deviceName[] = L"\\\\.\\X:";
deviceName[4] = driveLetter;

HANDLE hDevice = CreateFile(deviceName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hDevice != INVALID_HANDLE_VALUE) {
DWORD bytesReturned;
DeviceIoControl(hDevice, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL, 0, &bytesReturned, NULL);
DeviceIoControl(hDevice, IOCTL_STORAGE_EJECT_MEDIA, NULL, 0, NULL, 0, &bytesReturned, NULL);
CloseHandle(hDevice);
}

// 重新挂载驱动器
hDevice = CreateFile(deviceName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hDevice != INVALID_HANDLE_VALUE) {
CloseHandle(hDevice);
}
}

API 5 BroadcastSystemMessage

调用 BroadcastSystemMessage 广播通知设备改动
结果:不生效,图标在、设备也在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void BroadcastDeviceChange(DWORD dwVolLetter) 
{
// 模拟设备移除消息
DEV_BROADCAST_VOLUME dbv;
dbv.dbcv_size = sizeof(DEV_BROADCAST_VOLUME);
dbv.dbcv_devicetype = DBT_DEVTYP_VOLUME;
dbv.dbcv_reserved = 0;
dbv.dbcv_unitmask = 1 << (dwVolLetter - L'A'); // 假设 E: 是 U 盘的盘符
dbv.dbcv_flags = DBTF_MEDIA;

// 向系统广播设备变更消息
BroadcastSystemMessage(BSF_IGNORECURRENTTASK | BSF_POSTMESSAGE, BSM_ALLCOMPONENTS, WM_DEVICECHANGE, DBT_DEVICEREMOVECOMPLETE, (LPARAM)&dbv);

}

API 6 刷新设备列表

调用 SetupDI* 接口刷新设备列表。
结果:无效。因为设备并没有实际弹出,所有刷新后设备列表还是会有便携设备存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void RefreshDevice(LPCWSTR deviceInstanceId)
{
MYTRACE(L"RefreshDevice");
HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, deviceInstanceId, NULL, DIGCF_ALLCLASSES | DIGCF_PRESENT);
if (deviceInfoSet == INVALID_HANDLE_VALUE)
{
return;
}

SP_DEVINFO_DATA deviceInfoData;
deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);

if (SetupDiEnumDeviceInfo(deviceInfoSet, 0, &deviceInfoData))
{
SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, deviceInfoSet, &deviceInfoData);
}

SetupDiDestroyDeviceInfoList(deviceInfoSet);
}

API 7 刷新 Explorer 界面

1
2
3
4
5
6
7
8
9
10
VOID RefreshFolderViews(UINT csidl, LONG wEventId)
{
LPITEMIDLIST pidl;
if (SUCCEEDED(SHGetSpecialFolderLocation(NULL, csidl, &pidl)))
{
// SHChangeNotify(SHCNE_UPDATEDIR, SHCNF_IDLIST, pidl, 0);
SHChangeNotify(wEventId, SHCNF_IDLIST|SHCNF_FLUSH, pidl, 0);
CoTaskMemFree(pidl);
}
}

最终解决方案

调用 DeivceIOControl 强制弹出,然后通知界面刷新。

界面刷新调用了 SendMessageSHChangeNotify,代码如下所示:

1
2
3
NotifyDeviceRemoval(dwVolLetter);
SHChangeNotify(SHCNE_DRIVEREMOVED, SHCNF_PATH, wszVolShort, NULL);
SHChangeNotify(SHCNE_DRIVEADD, SHCNF_PATH, wszVolShort, NULL);

事实上,只要 CM_Request_Device_Eject_Ex 调用成功,那图标缓存和设备列表都会更新,但是调用失败的时候就比较复杂,尤其是返回 PNP_VetoIllegalDeviceRequest 时,没有合适的方法能从设备列表移除设备。

备忘

1
2
3
4
5
6
WCHAR wszDrivePoint[3] = L" :";
wszDrivePoint[0] = dwVolLetter;

if (!DefineDosDevice(DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE, wszDrivePoint, NULL))
MYTRACE(L"DDD_REMOVE_DEFINITION bRet==%d", GetLastError());


【Win7】弹出U盘
https://hodlyounger.github.io/A_OS/Windows/API/【winapi】【Win7】弹出U盘/
作者
mingming
发布于
2024年8月12日
许可协议