
Although removing a USB stick using the Windows shell is quite easy, it tends to be quite difficult programmatically. There are a lot of underlying notions, at the frontier of kernel driver development, one has to understand to achieve what may seem a simple task. When I started this work, I really did not think where it would lead me to. I sure did not think I would have to switch between kernel driver control codes, the Windows setup and Configuration Manager APIs, WMI, etc...
Well, this is the main reason for this article, I think the subject really deserves one. The result is a demo .NET Winforms project that includes reusable source code.
And by the way, as a side benefit, I will also explain how to read the hardware serial number of those disks, a recurring question on newsgroups and forums.
First of all, let's talk about the various Windows API involved here, apart from Win32, and there are many:
Setup API: An underused and not very well known Windows API. One of the reasons it is underused is because it is part of the DDK, but in fact, these are general Setup routines, which are used in installation applications, and there are many very interesting gems in there, like the CM_Request_Device_Eject function, which is the heart of the UsbEject project. DeviceIoControl function: The "Open, Sesame" user mode function to the depths of kernel mode cave. Tons of things (a large part of it is undocumented, or not well document) can be done using this. UsbEject project does not actually uses WMI because WMI does not handle device ejection, as far as I know. I could have used a hybrid approach using WMI for disk management and the rest for device Ejection. I will leave this as an exercise to the reader. WM_DEVICECHANGE Window message is used in this sample GUI application to visually refresh the tree when something happens to the device manager tree. Now, let's introduce a few terms. The definitions here are my own, not official ones (it is actually quite hard to find official definitions for all these).
The project is a Visual Studio 2005 (.NET 2.0) Windows form project. There is a folder called Library that contains the heart of it, an object oriented API to handle USB disks. The classes there are described in the following class diagram:

As you can see, there are 5 main classes (each class is defined in its own respective .cs file):
DeviceClass: An abstract class that represents a physical device class. It has a list of devices in this class DiskDeviceClass: Represents all disk devices in the system VolumeDeviceClass: Represents all volume devices in the system Device: Represents a generic device of any type (disk, volume, etc...). Note there is no Disk class, because in this project, a Disk has no specific property, compared to a Device. Note also the code has been designed so it could be extended to other devices, not only volumes and disks Volume: Represents a Windows volume. A volume has a LogicalDrive property which is filled if it has a drive letter assigned This is how you can eject all USB volumes for example:
VolumeDeviceClass volumeDeviceClass = new VolumeDeviceClass();
foreach (Volume device in volumeDeviceClass.Devices)
{
// is this volume on USB disks?
if (!device.IsUsb)
continue;
// is this volume a logical disk?
if ((device.LogicalDrive == null) || (device.LogicalDrive.Length == 0))
continue;
device.Eject(true); // allow Windows to display any relevant UI
}
This is the SetupApi function that ejects a device (any device that can be ejected). It takes a device instance handle (or devInst) as input. The function behaves differently if the second argument pVetoType is provided or not. If it is provided, the Windows shell will not present the end user with any dialog, and the eject operation will either succeed or fail silently (with an error code, called the Veto). If not, the Windows shell may display the traditional messages or dialog boxes, or balloon windows to inform the end user (Note: Windows 2000 may always display a message under certain circumstances, even when the second argument is not provided) that something interesting has happened.
The main problem is therefore "For a given disk letter, what is the device to eject?"
The device to eject must be a Disk device, not a Volume device (at least for USB disks). So the general algorithm is the following:
Environment.GetLogicalDrives. GetVolumeNameForVolumeMountPoint. IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS IO control code. CM_DEVCAP_REMOVABLE capability. To match Windows volumes with logical disks (algorithm step 3), there is a trick to know. First of all, let me say that all Windows volume can be uniquely addressed with a specific syntax of the form "\\?\Volume{GUID}\" where GUID is the GUID that identifies the volume.
When enumerating volumes devices (using the Volume Device Class Guid GUID_DEVINTERFACE_VOLUME, do not worry, the VolumeDeviceClass wraps it), the volume device path can be used directly in a call to GetVolumeNameForVolumeMountPoint, if "\" is appended to it, although it is not strictly speaking a volume name.
I have reproduced here a code snippet that shows how .NET P/Invoke interop is used to enumerate all physical devices for a given type.
int index = 0;
while (true)
{
// enumerate device interface for a given index
SP_DEVICE_INTERFACE_DATA interfaceData = new SP_DEVICE_INTERFACE_DATA();
if (!SetupDiEnumDeviceInterfaces(
_deviceInfoSet, null, ref _classGuid, index, interfaceData))
{
int error = Marshal.GetLastWin32Error();
// this is not really an error...
if (error != Native.ERROR_NO_MORE_ITEMS)
throw new Win32Exception(error);
break;
}
SP_DEVINFO_DATA devData = new SP_DEVINFO_DATA();
int size = 0;
// get detail for all the device interface
if (!SetupDiGetDeviceInterfaceDetail(
_deviceInfoSet, interfaceData, IntPtr.Zero, 0, ref size, devData))
{
int error = Marshal.GetLastWin32Error();
if (error != Native.ERROR_INSUFFICIENT_BUFFER)
throw new Win32Exception(error);
}
// allocate unmanaged Win32 buffer
IntPtr buffer = Marshal.AllocHGlobal(size);
SP_DEVICE_INTERFACE_DETAIL_DATA detailData =
new SP_DEVICE_INTERFACE_DETAIL_DATA();
detailData.cbSize = Marshal.SizeOf(
typeof(Native.SP_DEVICE_INTERFACE_DETAIL_DATA));
// copy managed struct buffer into unmanager win32 buffer
Marshal.StructureToPtr(detailData, buffer, false);
if (!SetupDiGetDeviceInterfaceDetail(
_deviceInfoSet, interfaceData, buffer, size, ref size, devData))
{
Marshal.FreeHGlobal(buffer); // don't forget to free memory
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// a bit of voodoo magic. This code is not 64 bits portable :-)
IntPtr pDevicePath = (IntPtr)((int)buffer + Marshal.SizeOf(typeof(int)));
string devicePath = Marshal.PtrToStringAuto(pDevicePath);
Marshal.FreeHGlobal(buffer);
index++;
}
There are multiple ways of doing this. I have chosen the most simple one: capture the WM_DEVICECHANGE Windows message. You just have to override the default Window procedure of any Winform in your application, like this:
protected override void WndProc(ref Message m)
{
if (m.Msg == Native.WM_DEVICECHANGE)
{
if (!_loading)
{
LoadItems(); // do the refresh work here
}
}
base.WndProc(ref m);
}
This is not strictly speaking related to the Eject subject, but I have seen many questions about this too, so I will talk a bit about hard disk's "serial numbers". When speaking about Windows volumes and disks, there are actually (at least) two serial numbers:
GetVolumeInformation regular Win32 function. Ok, so the question is "how do I read the hardware serial number?". There are at least two ways:
// browse all USB WMI physical disks
foreach(ManagementObject drive in new ManagementObjectSearcher(
"select * from Win32_DiskDrive where InterfaceType='USB'").Get())
{
// associate physical disks with partitions
foreach(ManagementObject partition in new ManagementObjectSearcher(
"ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" + drive["DeviceID"]
+ "'} WHERE AssocClass =
Win32_DiskDriveToDiskPartition").Get())
{
Console.WriteLine("Partition=" + partition["Name"]);
// associate partitions with logical disks (drive letter volumes)
foreach(ManagementObject disk in new ManagementObjectSearcher(
"ASSOCIATORS OF {Win32_DiskPartition.DeviceID='"
+ partition["DeviceID"]
+ "'} WHERE AssocClass =
Win32_LogicalDiskToPartition").Get())
{
Console.WriteLine("Disk=" + disk["Name"]);
}
}
// this may display nothing if the physical disk
// does not have a hardware serial number
Console.WriteLine("Serial="
+ new ManagementObject("Win32_PhysicalMedia.Tag='"
+ drive["DeviceID"] + "'")["SerialNumber"]);
}