Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

How to Prepare a USB Drive for Safe Removal

4.90/5 (95 votes)
9 Mar 2012CPOL8 min read 49   11.7K  
Shows the link between a drive letter, its disk number, and the disk's device instance

Introduction

Removing a USB drive using the Windows tray icon is easy, especially if you single left-click it, but sometimes, it's useful to do it from your program.

Background

There are some samples around, but the ones I saw were searching for the volume and then calling CM_Get_Parent twice to get the USB device to eject. This approach works only with drives which claim to have removable media. Such drives (drive type: DRIVE_REMOVABLE) are handled differently from basic disks (DRIVE_FIXED) under W2K and XP. Removable drives have a one-to-one relation between the volume and the disk, where the disk is the parent device of the volume. This is true for CDROM drives too.

USB drives without removable media are handled like basic disks, so they can have multiple partitions, and the volume's parent device is not the disk! Under Vista, this is the case for removable drives too, but multiple partitions are still not allowed. By the way, there are more differences resulting from the type of the USB disk. Here is some information.

The magic link between storage volumes and their disk is the device number. You can get it via DeviceIoControl called with IOCTL_STORAGE_GET_DEVICE_NUMBER. This call works with handles to storage volumes on one side, and disk, floppy, and CDROM drives on the other side.

Storage volumes can be spread over multiple disks. So, it is possible that for safe removal of a storage volume, more than one disk device has to be prepared for safe removal. Getting the list of device numbers of such a storage volume is done by IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS. I have ignored this because I think that using a RAID on external drives is a strange idea.

The device number is unique within a device class only. When dealing with drive letters, we have to distinguish between the device interfaces GUID_DEVINTERFACE_DISK, GUID_DEVINTERFACE_FLOPPY, and GUID_DEVINTERFACE_CDROM. The floppies were not considered here until end of October 2006, so a USB floppy screwed up everything, in theory. In real life, a USB floppy has usually device number 0, while any other USB drive has a higher number, so there were no real problems.

By the way: in W2K and XP, legacy floppies are not part of the GUID_DEVINTERFACE_FLOPPY enumeration.

The Sample

This sample is a simplified version of my command-line tool RemoveDrive. It expects the drive letter as a parameter to prepare for safe removal. It opens the volume and gets its device number:

C++
// "X:\"    -> for GetDriveType
char szRootPath[] = "X:\\";
szRootPath[0] = DriveLetter;

// "X:"     -> for QueryDosDevice
char szDevicePath[] = "X:";
szDevicePath[0] = DriveLetter;

// "\\.\X:" -> to open the volume
char szVolumeAccessPath[] = "\\\\.\\X:";
szVolumeAccessPath[4] = DriveLetter;

long DeviceNumber = -1;

HANDLE hVolume = CreateFile(szVolumeAccessPath, 0,
                    FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL, OPEN_EXISTING, 0, NULL);
if (hVolume == INVALID_HANDLE_VALUE) {
  return 1;
}

STORAGE_DEVICE_NUMBER sdn;
DWORD dwBytesReturned = 0;
long res = DeviceIoControl(hVolume,
                    IOCTL_STORAGE_GET_DEVICE_NUMBER,
                    NULL, 0, &sdn, sizeof(sdn),
                    &dwBytesReturned, NULL);
if ( res ) {
  DeviceNumber = sdn.DeviceNumber;
}
CloseHandle(hVolume);

if ( DeviceNumber == -1 ) {
  return 1;
}

UINT DriveType = GetDriveType(szRootPath);

// get the dos device name (like \device\floppy0)
// to decide if it's a floppy or not
char szKernelName[MAX_PATH];
res = QueryDosDevice(szDevicePath, szKernelName, MAX_PATH);
if ( !res ) {
  return 1;
}

DEVINST DevInst = GetDrivesDevInstByDeviceNumber(DeviceNumber,
                  DriveType, szKernelName);
if ( ! DevInst ) {
  return 1;
}

Depending on the volume's drive type and the kernel name, it then enumerates either all disks, floppies, or CD-ROMs, using the setup API. The device numbers of the drives are matched with the device number mentioned above in order to get the device instance of the correct drive:

C++
//---------------------------------------------------------
DEVINST GetDrivesDevInstByDeviceNumber(long DeviceNumber,
          UINT DriveType, char* szKernelName)
{
  bool IsFloppy = (strstr(szKernelName,
       "\\Floppy") != NULL); // is there a better way?

  GUID* guid;

  switch (DriveType) {
  case DRIVE_REMOVABLE:
    if ( IsFloppy ) {
      guid = (GUID*)&GUID_DEVINTERFACE_FLOPPY;
    } else {
      guid = (GUID*)&GUID_DEVINTERFACE_DISK;
    }
    break;
  case DRIVE_FIXED:
    guid = (GUID*)&GUID_DEVINTERFACE_DISK;
    break;
  case DRIVE_CDROM:
    guid = (GUID*)&GUID_DEVINTERFACE_CDROM;
    break;
  default:
    return 0;
  }

  // Get device interface info set handle
  // for all devices attached to system
  HDEVINFO hDevInfo = SetupDiGetClassDevs(guid, NULL, NULL,
                    DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);

  if ( hDevInfo == INVALID_HANDLE_VALUE )  {
    return 0;
  }

  // Retrieve a context structure for a device interface
  // of a device information set.
  DWORD dwIndex = 0;
  BOOL bRet = FALSE;

  BYTE Buf[1024];
  PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd =
     (PSP_DEVICE_INTERFACE_DETAIL_DATA)Buf;
  SP_DEVICE_INTERFACE_DATA         spdid;
  SP_DEVINFO_DATA                  spdd;
  DWORD                            dwSize;

  spdid.cbSize = sizeof(spdid);

  while ( true )  {
    bRet = SetupDiEnumDeviceInterfaces(hDevInfo, NULL,
           guid, dwIndex, &spdid);
    if ( !bRet ) {
      break;
    }

    dwSize = 0;
    SetupDiGetDeviceInterfaceDetail(hDevInfo,
      &spdid, NULL, 0, &dwSize, NULL);

    if ( dwSize!=0 && dwSize<=sizeof(Buf) ) {
      pspdidd->cbSize = sizeof(*pspdidd); // 5 Bytes!

      ZeroMemory((PVOID)&spdd, sizeof(spdd));
      spdd.cbSize = sizeof(spdd);

      long res =
        SetupDiGetDeviceInterfaceDetail(hDevInfo, &
                                        spdid, pspdidd,
                                        dwSize, &dwSize,
                                        &spdd);
      if ( res ) {
        HANDLE hDrive = CreateFile(pspdidd->DevicePath,0,
                      FILE_SHARE_READ | FILE_SHARE_WRITE,
                      NULL, OPEN_EXISTING, 0, NULL);
        if ( hDrive != INVALID_HANDLE_VALUE ) {
          STORAGE_DEVICE_NUMBER sdn;
          DWORD dwBytesReturned = 0;
          res = DeviceIoControl(hDrive,
                        IOCTL_STORAGE_GET_DEVICE_NUMBER,
                        NULL, 0, &sdn, sizeof(sdn),
                        &dwBytesReturned, NULL);
          if ( res ) {
            if ( DeviceNumber == (long)sdn.DeviceNumber ) {
              CloseHandle(hDrive);
              SetupDiDestroyDeviceInfoList(hDevInfo);
              return spdd.DevInst;
            }
          }
          CloseHandle(hDrive);
        }
      }
    }
    dwIndex++;
  }

  SetupDiDestroyDeviceInfoList(hDevInfo);

  return 0;
}
//---------------------------------------------------------

The parent device of the disk, floppy, or CD-ROM is the USB device to eject. CM_Request_Device_Eject shall be used for devices which have the SurpriseRemovalOK flag only. Otherwise, CM_Query_And_Remove_SubTree shall be used. See MSDN here and here.

As a result, the removed device is still present but has a problem code which is usually 47 (CM_PROB_HELD_FOR_EJECT). But if CM_Query_And_Remove_SubTree is used for a USB device (which has the SurpriseRemovalOK flag) then the problem code is 21 (CM_PROB_WILL_BE_REMOVED). This seems to be "safe" too since the USB device's drive and volume are gone then. The big difference here is that such a device can be reactivated! I have made a tool for this: RestartSrDev.

However, CM_Query_And_Remove_SubTree doesn't work for restricted users; it returns CR_ACCESS_DENIED in these cases, while the non-suggested CM_Request_Device_Eject works fine for restricted users. Surprisingly, CM_Request_Device_Eject does not work in a service or a GINA, here CM_Query_And_Remove_SubTree is the right choice.

When using CM_Query_And_Remove_SubTree, we have to add the flag CM_REMOVE_NO_RESTART because otherwise the just-removed device may be immediately redetected. This happens under Vista, but is also reported to happen under W2K and XP sometimes. It is documented as "Beginning with Windows XP" but the flag works (and is required) under W2K with SP4 too.

I take the easy way, and now use CM_Request_Device_Eject exclusively in this sample.

Discussion

If you use it for PATA drives, then both master and slave drives are removed! However, both can be brought back with a DEVCON RESCAN.

If the functions are called with NULL/0 for the veto parameters, then XP shows the "it's safe now" balloon tip, W2K shows a message box, and Vista shows nothing. As McCoy once said: "I know engineers. They love to change things."

I remember that I've seen CM_Query_And_Remove_SubTree and CM_Request_Device_Eject returning CR_SUCCESS even when the call failed with a veto under XP. I cannot reproduce it, but I'm sure I've seen this, maybe it was under XP RTM or SP1. Therefore, it seems to be better to check the veto values the functions return.

Under Windows 2000, the ANSI versions of both functions are not implemented. They return CR_CALL_NOT_IMPLEMENTED, so we use the Unicode versions instead.

Both functions usually take several seconds until they return, so it's a good idea to put them into their own thread. In fact, the delay can be up to 30 seconds under XP, and up to 15 seconds since Vista. This happens when a process has registered for receiving the removal request and does not return it. To keep this sample simple, there is no extra thread created for calling CM_Request_Device_Eject:

C++
ULONG Status = 0;
ULONG ProblemNumber = 0;
PNP_VETO_TYPE VetoType = PNP_VetoTypeUnknown;
WCHAR VetoNameW[MAX_PATH];
bool bSuccess = false;

// get drives's parent, e.g. the USB bridge,
// the SATA port, an IDE channel with two drives!
DEVINST DevInstParent = 0;
res = CM_Get_Parent(&DevInstParent, DevInst, 0);

for ( long tries=1; tries>=3; tries++ ) {
// sometimes we need some tries...

  VetoNameW[0] = 0;

  res = CM_Request_Device_EjectW(DevInstParent,
          &VetoType, VetoNameW, MAX_PATH, 0);

  bSuccess = ( res==CR_SUCCESS &&
                    VetoType==PNP_VetoTypeUnknown );
  if ( bSuccess )  {
    break;
  }

  Sleep(500); // required to give the next tries a chance!
}

It's often seen that the removal fails on the first attempt but works on the second attempt. Therefore, I just try it three times.

What Makes the Removal Fail

The preparation for safe removal fails as long as there is one open handle to the disk or to the storage volume. And, of course, you cannot run this EXE from the drive to remove. To do that, you would need a temporary copy on another drive. ProcessExplorer is great for discovering which process holds an open handle to a drive. Press Ctrl+F and enter the drive letter, like U:. It's often seen that it cannot resolve drive letters, so you have to search for the kernel name of the drive. It should be something like \Device\Harddisk4\DP(1)0-0+11. A significant part, such as 'disk4', is usually good enough. On occasions, however, even the driver-driven ProcessExplorer isn't able to find the nasty handle.

Reactivate a USB Drive after Safe Removal

When prepared for safe removal and having the problem code 47 (CM_PROB_HELD_FOR_EJECT), a USB device cannot be reactivated. The only way out is to deactivate and then reactivate the USB hub which it is connected to. This works with both standard hubs and root hubs. Of course, this cycles all USB devices attached to this hub.

The Demo Project

The demo project is made with VS6. It requires LIBs and headers from the Microsoft Windows Platform SDK and DDK/WDK. If you are using a newer Visual Studio, just try to compile it. If it complains about missing includes or LIBs, just get the latest SDK. If someting is still missing, it might come with the Windows Driver Kit (WDK): here.

Using Visual Studio 6.0

Many users still love to use VS6, me too. This is because it is slim, snappy, and installed in no time. In fact, for simple Win32 applications it's good enough to copy its folder and import its Registry settings. The disadvantages are the old compiler, missing x64 support, and the incompatibility with the new SDKs and DDKs/WDKs.

The latest SDK version that integrates and works perfectly with VS6 is from February 2003. It's called "Platform SDK for Windows Server 2003" (February 2003 Edition, Build 3790.0), and is still available for download at Microsoft, see here. Unfortunately, cfg.h and cfgmgr32.lib do not come with this SDK, even cfgmgr32.h has an include for cfg.h. cfg.h and the also missing cfgmgr32.lib are found in the Windows DDK. Both can be used from any DDK or WDK, even new versions, e.g., from WDK Build 6000. When building more complex projects, you will run into trouble since other header and lib files from newer SDKs and DDKs/WDKs will not work with VS6! The oldest DDK available for download is the "Windows Server 2003 SP1 DDK" (Build 3790.1830): 1830_usa_ddk.iso. Most files are compatible with VS6, but some lib files are not when compiling debug versions, for instance, uuid.lib. uuid.lib is no problem, since a compatible one comes with VS6. In such cases, just rename the new file to make VS6 use the old one from its own LIB folder. The right DDK for VS6 is the "Windows XP SP1 DDK" (Build 2600.1106), but this is not available for download at Microsoft.

If you don't want to download a whole DDK for a single file, then you can use CFG.h from the ReactOS project, which is at least compatible for this demo. Just put the downloaded cfg.h into the folder where the cfgmgr32.h is found.

The integration of the SDK+DDK headers and libs is usually done manually by entering them into the include and lib folder list.

Screenshots from a German VS6: Includes and Libs.

History

  • 15 Jan 2007 - Updated download
  • 27 Jan 2007 - Updated article and download; CM_Query_And_Remove_SubTree isn't used anymore
  • 16 May 2007 - Updated article and download; removed SetupDiEnumInterfaceDevice because it's just a define for SetupDiEnumDeviceInterfaces which has been called some lines before
  • 7 Nov 2007 - Fixed the hints about the required SDK for using VS6
  • 21 Jan 2009 - Some minor changes to the article
  • 30 March 2009 - Fixed the hints about the required SDK and DDK for using VS6
  • 15 Feb 2010 - Some minor changes to the article
  • 13 May 2010 - Some minor changes to the article
  • 16 May 2010 - Added some information about the resulting device problem codes
  • 6 April 2011 - Updated links to old SDK and DDK

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)