5,316,870 members and growing! (15,966 online)
Email Password   helpLost your password?
General Reading » Hardware & System » Cryptography     Intermediate License: The Code Project Open License (CPOL)

How to Prepare a USB Drive for Safe Removal

By Uwe_Sieber

Shows the link between a drive letter, its disk number and the disk's device instance
VC6, C++Windows, Win2K, WinXP, Win2003, VistaVS6, Visual Studio, Dev

Posted: 18 Apr 2006
Updated: 7 Nov 2007
Views: 117,044
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
41 votes for this Article.
Popularity: 7.45 Rating: 4.62 out of 5
1 vote, 2.4%
1
1 vote, 2.4%
2
2 votes, 4.9%
3
7 votes, 17.1%
4
30 votes, 73.2%
5

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.

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 the one side, and disks, floppies and CD-ROMs on the other side. The device number is unique within a device class only. 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 device number 0 while any other USB drive has a higher number, so there were no real problems.

By the way: 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 to prepare for safe removal as a parameter. It opens the volume and gets its device number:

// "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 szDosDeviceName[MAX_PATH];
res =QueryDosDevice(szDevicePath,szDosDeviceName,MAX_PATH);
if ( !res ) {
  return 1;
}

DEVINST DevInst=GetDrivesDevInstByDeviceNumber(DeviceNumber,
                  DriveType, szDosDeviceName);
if ( DeviceNumber == -1 ) {
  return 1;
}

Then it enumerates all disks, floppies or CD-ROMs — depending on the drive type and the DOS device name — using the setup API. The disk's device numbers are matched with the device number mentioned above in order to get the device instance:

//---------------------------------------------------------
DEVINST GetDrivesDevInstByDeviceNumber(long DeviceNumber,
          UINT DriveType, char* szDosDeviceName)
{

  bool IsFloppy = (strstr(szDosDeviceName,
       "\\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, NULL, 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.

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. Under Vista, we have to add the flag CM_REMOVE_NO_RESTART because otherwise the just-removed device is immediately redetected. I take the easy way and now use CM_Request_Device_Eject exclusively.

Discussion

Here are some interesting links:

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 messagebox 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 with SP1. Therefore it seems to be better to check the veto values that 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 may take several seconds until they return, so it's a good idea to put them into their own thread. However, I didn't do that in this simple example of removal:

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 prepartion 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 it. 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:. I've often seen that it cannot resolve drive letters, so you have to search for the DOS device 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 occasion, however, even the driver-driven ProcessExplorer isn't able to find the nasty handle.

The Demo Project

The demo project is made with VS6. It requires LIBs and headers from the Microsoft Windows Platform SDK. The latest version that integrates and works perfectly with VS6 is from February 2003. It's called Platform SDK for Windows Server 2003 Build 3780.0 and is still available for download:
here

Unfortunately, the CFG.h file is still missing in pre Vista SDKs. You can take it from the Vista Platform SDK or from any DDK/WDK since build 2600.

If you don't want to download a whole DDK for a single file then you can use the CFG.h from the ReactOS project, which is at least compatible for this demo. Change the include line in CFGMGR32.h to this file in that case.

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

License

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

About the Author

Uwe_Sieber



Occupation: Software Developer
Location: Germany Germany

Other popular Hardware & System articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 82 (Total in Forum: 82) (Refresh)FirstPrevNext
Subject  Author Date 
GeneralAvoid the Windows XP popup on inserting a USB devicememberAlexEvans22:13 14 Jul '08  
Generalflush to usb diskmemberJimmyO14:03 30 Dec '07  
Questionlink error "LNK2001" when build release versionmembertungshan hsieh16:19 6 Nov '07  
AnswerRe: link error "LNK2001" when build release versionmemberUwe_Sieber22:08 6 Nov '07  
GeneralRe: link error "LNK2001" when build release versionmembertungshan hsieh23:30 6 Nov '07  
QuestionHow to avoid CR_ACCESS_DENIED in GINAmemberMasa Doi18:41 29 Oct '07  
AnswerRe: How to avoid CR_ACCESS_DENIED in GINAmemberUwe_Sieber23:41 29 Oct '07  
GeneralRe: How to avoid CR_ACCESS_DENIED in GINAmemberMasa Doi20:35 30 Oct '07  
GeneralThanks! Packed in one funktion.memberMr. S7:28 2 Sep '07  
GeneralRe: Thanks! Packed in one funktion.memberUwe_Sieber8:03 2 Sep '07  
GeneralRe: Thanks! Packed in one funktion.memberMr. S9:52 2 Sep '07  
GeneralThanks!memberSabuncu12:04 29 Jul '07  
GeneralNo Notify winprocmemberShiNoOb8:55 17 Jul '07  
Generalcan this offer a workaround to GetDriveType?memberumeca746:29 6 Jun '07  
GeneralRe: can this offer a workaround to GetDriveType?memberUwe_Sieber7:17 6 Jun '07  
GeneralRe: can this offer a workaround to GetDriveType?memberumeca743:12 7 Jun '07  
Questionremove fail in guest accountmemberSwanek3:31 4 Jun '07  
AnswerRe: remove fail in guest account [modified]memberUwe_Sieber22:13 4 Jun '07  
GeneralDetect USB devicemember4:32 20 Feb '07  
GeneralRe: Detect USB devicememberUwe_Sieber5:18 20 Feb '07  
GeneralSave removal, or safe?memberHamed Mosavi17:45 5 Feb '07  
GeneralRe: Save removal, or safe?memberUwe_Sieber19:55 5 Feb '07  
GeneralProblem?memberkiranin20:38 23 Jan '07  
GeneralRe: Problem?memberUwe_Sieber20:59 23 Jan '07  
GeneralRe: Problem? [modified]memberkiranin21:21 23 Jan '07  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 7 Nov 2007
Editor: Sean Ewington
Copyright 2006 by Uwe_Sieber
Everything else Copyright © CodeProject, 1999-2008
Web17 | Advertise on the Code Project