Reputation: 55720
Do you know what is the API, or sequence of API calls that windows uses to accomplish the "Eject" function which is available on the shell context menu for removable volumes?
So far I've tried two things:
using CM_Request_Device_Eject, I enumerate the removable disks (using the SetupDiXXX APIs), find the one that I'm interested in, walk the device manager hierarchy (using CM_XXX APIs) and finally call CM_Request_Device_Eject
on the devInst
of the device I'm interesed in. This works in the sense that it does remove the volumes from My Computer and makes the device "safe to remove" (ready to be removed) but it is not the same as the shell context menu "Eject" function. The way I know this is because the device that I'm trying to eject is supposed to do something when it is ejected and that something is not happening when I do the eject using CM_Request_Device_Eject
.
using DeviceIoControl with the IOCTL_STORAGE_EJECT_MEDIA control code. The sequence of events is:
This doesn't work at all. Each one of the DeviceIoControl
calls fails with ERROR_IVALID_FUNCTION
(0x00000001). I don't know why the calls fail. I've verified that other calls to DeviceIoControl work fine for the same file handle (such as IOCTL_STORAGE_GET_DEVICE_NUMBER)
Finally, my development machine is running Windows 7 x64, and in order to get the second method to work I've tried running my application with Administrator privileges and that did not change anything.
Any information is welcome. Even maybe how to invoke the shell "Eject" menu item as a last recourse.
Upvotes: 17
Views: 11777
Reputation: 11
Late to the party, however, I had to implement this functionality, and this page is where the direction headed. So, I’ve put the code here for anyone who is stuck on this topic.
It works based on the device path to handle the case where the USB is formatted, so you need to retrieve it using: wmic diskdrive get mediaType,model,deviceID,serialNumber,interfaceType,size,partitions,pnpdeviceid /format:csv
#include <iostream>
#include <string>
#include <vector>
#include <windows.h>
#include <Setupapi.h>
#include <cfgmgr32.h>
#include <winioctl.h>
#pragma comment(lib, "setupapi.lib")
LPCWSTR string_to_lpcwstr(const std::string &str);
std::string wchar_to_string(const WCHAR volume_name[MAX_PATH]);
DEVINST get_drives_dev_instance(long number, _MEDIA_TYPE type, const std::string &dos_device_name);
bool eject_drive(const std::string &device_path);
int main() {
// Replace this with the correct physical drive name
// Open terminal and type: wmic diskdrive get mediaType,model,deviceID,serialNumber,interfaceType,size,partitions,pnpdeviceid /format:table
std::string device_path = R"(\\.\PHYSICALDRIVE1)";
eject_drive(device_path);
return 0;
}
LPCWSTR string_to_lpcwstr(const std::string &str) {
int wide_char_length = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0);
if (wide_char_length == 0) {
return nullptr;
}
wchar_t *wide_string = new wchar_t[wide_char_length];
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, wide_string, wide_char_length);
return wide_string;
}
std::string wchar_to_string(const WCHAR volume_name[MAX_PATH]) {
int utf8_length = WideCharToMultiByte(CP_UTF8, 0, volume_name, -1, NULL, 0, NULL, NULL);
if (utf8_length == 0) {
return "";
}
// -1 to exclude the null terminator
std::string result(utf8_length - 1, 0);
WideCharToMultiByte(CP_UTF8, 0, volume_name, -1, &result[0], utf8_length, NULL, NULL);
return result;
}
DEVINST get_drives_dev_instance(long number, _MEDIA_TYPE type, const std::string &dos_device_name) {
bool is_floppy = strstr(dos_device_name.c_str(), "\\Floppy") != NULL;
const GUID *guid = NULL;
switch (type) {
case RemovableMedia:
if (is_floppy) {
guid = &GUID_DEVINTERFACE_FLOPPY;
}
else {
guid = &GUID_DEVINTERFACE_DISK;
}
break;
case FixedMedia:
guid = &GUID_DEVINTERFACE_DISK;
break;
default:
std::cerr << "Failed to get drive device instance. Incorrect device type.\n";
return 0;
}
HDEVINFO devinfo = SetupDiGetClassDevsA(guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (devinfo == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to SetupDiGetClassDevsA(). Last error code: 0x%08X.\n";
return 0;
}
DWORD index = 0;
long res;
BYTE buffer[1024];
PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd = (PSP_DEVICE_INTERFACE_DETAIL_DATA)buffer;
SP_DEVICE_INTERFACE_DATA spdid;
SP_DEVINFO_DATA spdd;
DWORD size;
spdid.cbSize = sizeof(spdid);
while (true) {
res = SetupDiEnumDeviceInterfaces(devinfo, NULL, guid, index, &spdid);
if (!res) {
break;
}
size = 0;
SetupDiGetDeviceInterfaceDetail(devinfo, &spdid, NULL, 0, &size, NULL); // check the buffer size
if (size != 0 && size <= sizeof(buffer)) {
pspdidd->cbSize = sizeof(*pspdidd); // 5 Bytes!
ZeroMemory(&spdd, sizeof(spdd));
spdd.cbSize = sizeof(spdd);
long res = SetupDiGetDeviceInterfaceDetail(devinfo, &spdid, pspdidd, size, &size, &spdd);
if (res) {
HANDLE drive_handle = CreateFile(pspdidd->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (drive_handle != INVALID_HANDLE_VALUE) {
STORAGE_DEVICE_NUMBER sdn;
DWORD bytes = 0;
res = DeviceIoControl(drive_handle, IOCTL_STORAGE_GET_DEVICE_NUMBER, NULL, 0, &sdn, sizeof(sdn), &bytes, NULL);
if (res) {
if (number == (long)sdn.DeviceNumber) {
CloseHandle(drive_handle);
SetupDiDestroyDeviceInfoList(devinfo);
return spdd.DevInst;
}
}
CloseHandle(drive_handle);
}
}
}
index++;
}
SetupDiDestroyDeviceInfoList(devinfo);
return 0;
}
bool eject_drive(const std::string &device_path) {
HANDLE device_handle = CreateFileW(string_to_lpcwstr(device_path), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (device_handle == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open physical drive. Last error: " << GetLastError() << std::endl;
return false;
}
STORAGE_DEVICE_NUMBER storage_device_number = { 0 };
DWORD bytes_returned = 0;
BOOL get_device_number_result = DeviceIoControl(
device_handle,
IOCTL_STORAGE_GET_DEVICE_NUMBER,
NULL,
0,
&storage_device_number,
sizeof(storage_device_number),
&bytes_returned,
NULL);
if (!get_device_number_result) {
std::cerr << "Failed to get device number. Last error: " << GetLastError() << std::endl;
CloseHandle(device_handle);
return false;
}
DISK_GEOMETRY disk_geometry = { 0 };
BOOL get_drive_geometry_result = DeviceIoControl(device_handle,
IOCTL_DISK_GET_DRIVE_GEOMETRY,
NULL,
0,
&disk_geometry,
sizeof(disk_geometry),
&bytes_returned, NULL);
if (!get_drive_geometry_result) {
std::cerr << "Failed to get drive geometry. Last error code: " << GetLastError() << std::endl;
CloseHandle(device_handle);
return false;
}
CloseHandle(device_handle);
WCHAR volume_name[MAX_PATH] = { 0 };
HANDLE handle_find_volume = FindFirstVolumeW(volume_name, ARRAYSIZE(volume_name));
if (handle_find_volume == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to enumerate volumes. Last error: " << GetLastError() << std::endl;
return false;
}
uint64_t device_number = 0;
std::string dos_name = "";
bool found = false;
do {
std::wstring formatted_volume_name = volume_name;
if (formatted_volume_name.back() == L'\\') {
formatted_volume_name.pop_back();
}
HANDLE handle_volume = CreateFileW(formatted_volume_name.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (handle_volume == INVALID_HANDLE_VALUE) {
continue;
}
VOLUME_DISK_EXTENTS disk_extents;
DWORD bytes_returned = 0;
if (DeviceIoControl(handle_volume, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, NULL, 0, &disk_extents, sizeof(disk_extents), &bytes_returned, NULL)) {
for (DWORD i = 0; i < disk_extents.NumberOfDiskExtents; ++i) {
if (disk_extents.Extents[i].DiskNumber == storage_device_number.DeviceNumber) {
std::wcout << L"Volume found: " << formatted_volume_name << std::endl;
// Map the volume GUID to DOS device name(s)
WCHAR dos_device_names[MAX_PATH] = { 0 };
if (QueryDosDeviceW(formatted_volume_name.c_str() + 4, dos_device_names, ARRAYSIZE(dos_device_names))) {
std::wcout << L"Associated DOS device(s): " << dos_device_names << std::endl;
found = true;
device_number = storage_device_number.DeviceNumber;
dos_name = wchar_to_string(dos_device_names);
CloseHandle(handle_volume);
break;
}
else {
std::cerr << "Failed to query DOS device name. Last error code: " << GetLastError() << std::endl;
}
return false;
}
}
}
} while (FindNextVolumeW(handle_find_volume, volume_name, ARRAYSIZE(volume_name)) && !found);
FindVolumeClose(handle_find_volume);
std::cout << "device_number: " << device_number << std::endl;
std::cout << "dos_name: " << dos_name << std::endl;
std::cout << "drive_type: " << disk_geometry.MediaType << std::endl;
DEVINST devinst = get_drives_dev_instance(device_number, disk_geometry.MediaType, dos_name);
if (devinst == 0) {
return false;
}
PNP_VETO_TYPE veto_type = PNP_VetoTypeUnknown;
DEVINST parent = 0;
CM_Get_Parent(&parent, devinst, 0);
constexpr int32_t MAX_RETRY = 3;
constexpr int32_t TIME_OUT = 500;
bool result = false;
for (DWORD i = 0; i < MAX_RETRY; ++i) {
char veto_name[MAX_PATH] = { 0 };
CONFIGRET res = CM_Request_Device_EjectA(parent, &veto_type, veto_name, MAX_PATH, 0);
result = res == CR_SUCCESS && veto_type == PNP_VetoTypeUnknown;
if (!result) {
Sleep(TIME_OUT);
}
}
return result;
}
Upvotes: 0
Reputation: 55720
Eventually, I found out where I was making a mistake with approach #2.
It turns out that for some reason I was not setting the desired access correctly when opening the handle to the volume using CreateFile.
The correct access mode is GENERIC_READ | GENERIC_WRITE
and I was passing 0
. After correcting my error I was able to successfully eject the device using DeviceIoControl - IOCTL_STORAGE_EJECT_MEDIA, as well as with method #1, using CM_Request_Device_Eject.
Finally, it turns out that method #2 is indeed the method used by the shell context menu's "Eject" function. Using this method the device reacts correctly.
Upvotes: 3
Reputation: 1561
I came here accidentally while doing a search on "CM_Request_Device_Eject", and saw that it was similar to a solution I'd recently done by pulling together similar pieces of a solution. Forgive the late answer.
I've summarized the steps I've done for this on my project in this SO answer.
Upvotes: 0