How can i remove a USB flash disk programmatically using delphi?

12,101

Solution 1

This is a quick and dirty translation of this sample code to remove a drive, from support.microsoft.com. It does however work only for users with admin permissions on my system.

For more information on working with USB devices in general follow the link in this answer by concept03.

function OpenVolume(ADrive: char): THandle;
var
  RootName, VolumeName: string;
  AccessFlags: DWORD;
begin
  RootName := ADrive + ':\'; (* '\'' // keep SO syntax highlighting working *)
  case GetDriveType(PChar(RootName)) of
    DRIVE_REMOVABLE:
      AccessFlags := GENERIC_READ or GENERIC_WRITE;
    DRIVE_CDROM:
      AccessFlags := GENERIC_READ;
  else
    Result := INVALID_HANDLE_VALUE;
    exit;
  end;
  VolumeName := Format('\\.\%s:', [ADrive]);
  Result := CreateFile(PChar(VolumeName), AccessFlags,
    FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
  if Result = INVALID_HANDLE_VALUE then
    RaiseLastWin32Error;
end;

function LockVolume(AVolumeHandle: THandle): boolean;
const
  LOCK_TIMEOUT = 10 * 1000; // 10 Seconds
  LOCK_RETRIES = 20;
  LOCK_SLEEP = LOCK_TIMEOUT div LOCK_RETRIES;

// #define FSCTL_LOCK_VOLUME CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 6, METHOD_BUFFERED, FILE_ANY_ACCESS)
  FSCTL_LOCK_VOLUME = (9 shl 16) or (0 shl 14) or (6 shl 2) or 0;
var
  Retries: integer;
  BytesReturned: Cardinal;
begin
  for Retries := 1 to LOCK_RETRIES do begin
    Result := DeviceIoControl(AVolumeHandle, FSCTL_LOCK_VOLUME, nil, 0,
      nil, 0, BytesReturned, nil);
    if Result then
      break;
    Sleep(LOCK_SLEEP);
  end;
end;

function DismountVolume(AVolumeHandle: THandle): boolean;
const
// #define FSCTL_DISMOUNT_VOLUME CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 8, METHOD_BUFFERED, FILE_ANY_ACCESS)
  FSCTL_DISMOUNT_VOLUME = (9 shl 16) or (0 shl 14) or (8 shl 2) or 0;
var
  BytesReturned: Cardinal;
begin
  Result := DeviceIoControl(AVolumeHandle, FSCTL_DISMOUNT_VOLUME, nil, 0,
    nil, 0, BytesReturned, nil);
  if not Result then
    RaiseLastWin32Error;
end;

function PreventRemovalOfVolume(AVolumeHandle: THandle;
  APreventRemoval: boolean): boolean;
const
// #define IOCTL_STORAGE_MEDIA_REMOVAL CTL_CODE(IOCTL_STORAGE_BASE, 0x0201, METHOD_BUFFERED, FILE_READ_ACCESS)
  IOCTL_STORAGE_MEDIA_REMOVAL = ($2d shl 16) or (1 shl 14) or ($201 shl 2) or 0;
type
  TPreventMediaRemoval = record
    PreventMediaRemoval: BOOL;
  end;
var
  BytesReturned: Cardinal;
  PMRBuffer: TPreventMediaRemoval;
begin
  PMRBuffer.PreventMediaRemoval := APreventRemoval;
  Result := DeviceIoControl(AVolumeHandle, IOCTL_STORAGE_MEDIA_REMOVAL,
    @PMRBuffer, SizeOf(TPreventMediaRemoval), nil, 0, BytesReturned, nil);
  if not Result then
    RaiseLastWin32Error;
end;

function AutoEjectVolume(AVolumeHandle: THandle): boolean;
const
// #define IOCTL_STORAGE_EJECT_MEDIA CTL_CODE(IOCTL_STORAGE_BASE, 0x0202, METHOD_BUFFERED, FILE_READ_ACCESS)
  IOCTL_STORAGE_EJECT_MEDIA = ($2d shl 16) or (1 shl 14) or ($202 shl 2) or 0;
var
  BytesReturned: Cardinal;
begin
  Result := DeviceIoControl(AVolumeHandle, IOCTL_STORAGE_EJECT_MEDIA, nil, 0,
    nil, 0, BytesReturned, nil);
  if not Result then
    RaiseLastWin32Error;
end;

function EjectVolume(ADrive: char): boolean;
var
  VolumeHandle: THandle;
begin
  Result := FALSE;
  // Open the volume
  VolumeHandle := OpenVolume(ADrive);
  if VolumeHandle = INVALID_HANDLE_VALUE then
    exit;
  try
    // Lock and dismount the volume
    if LockVolume(VolumeHandle) and DismountVolume(VolumeHandle) then begin
      // Set prevent removal to false and eject the volume
      if PreventRemovalOfVolume(VolumeHandle, FALSE) then
        AutoEjectVolume(VolumeHandle);
    end;
  finally
    // Close the volume so other processes can use the drive
    CloseHandle(VolumeHandle);
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  EjectVolume('E');
end;

Solution 2

The key for remove a USB drive is use the CM_Request_Device_Eject function,

Check this sample delphi app, based on this article How to Prepare a USB Drive for Safe Removal and which uses the JEDI API Library & Security Code Library

{$APPTYPE CONSOLE}

{$R *.res}

uses
  JwaWinIoctl,
  Cfg,
  CfgMgr32,
  SetupApi,
  Windows,
  SysUtils;


function GetDrivesDevInstByDeviceNumber(DeviceNumber : LONG; DriveType : UINT; szDosDeviceName: PChar) : DEVINST;
var
 StorageGUID : TGUID;
 IsFloppy : Boolean;
 hDevInfo : SetupApi.HDEVINFO;
 dwIndex  : DWORD;
 res      : BOOL;
 pspdidd  : PSPDeviceInterfaceDetailData;
 spdid    : SP_DEVICE_INTERFACE_DATA;
 spdd     : SP_DEVINFO_DATA;
 dwSize   : DWORD;
 hDrive   : THandle;
 sdn      : STORAGE_DEVICE_NUMBER;
 dwBytesReturned : DWORD;
begin
  Result:=0;
    IsFloppy := pos('\\Floppy', szDosDeviceName)>0; // who knows a better way?
    case DriveType of
    DRIVE_REMOVABLE:
                if ( IsFloppy ) then
                  StorageGUID := GUID_DEVINTERFACE_FLOPPY
                else
                  StorageGUID := GUID_DEVINTERFACE_DISK;

    DRIVE_FIXED:  StorageGUID := GUID_DEVINTERFACE_DISK;
    DRIVE_CDROM:    StorageGUID := GUID_DEVINTERFACE_CDROM;
    else
        exit
  end;

    // Get device interface info set handle for all devices attached to system
    hDevInfo := SetupDiGetClassDevs(@StorageGUID, nil, 0, DIGCF_PRESENT OR DIGCF_DEVICEINTERFACE);
  if (NativeUInt(hDevInfo) <> INVALID_HANDLE_VALUE) then
  try
    // Retrieve a context structure for a device interface of a device information set
    dwIndex := 0;
    //PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd = (PSP_DEVICE_INTERFACE_DETAIL_DATA)Buf;
    spdid.cbSize := SizeOf(spdid);

    while true do
    begin
      res := SetupDiEnumDeviceInterfaces(hDevInfo, nil, StorageGUID, dwIndex, spdid);
      if not res then
        break;

      dwSize := 0;
      SetupDiGetDeviceInterfaceDetail(hDevInfo, @spdid, nil, 0, dwSize, nil); // check the buffer size

      if ( dwSize<>0) then
      begin
       pspdidd := AllocMem(dwSize);
       try
        pspdidd.cbSize := SizeOf(TSPDeviceInterfaceDetailData);
        ZeroMemory(@spdd, sizeof(spdd));
        spdd.cbSize := SizeOf(spdd);
        res := SetupDiGetDeviceInterfaceDetail(hDevInfo, @spdid, pspdidd, dwSize, dwSize, @spdd);
        if res then
        begin
          // open the disk or cdrom or floppy
          hDrive := CreateFile(pspdidd.DevicePath, 0, FILE_SHARE_READ OR FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
         if ( hDrive <> INVALID_HANDLE_VALUE ) then
          try
              // get its device number
              dwBytesReturned := 0;
              res := DeviceIoControl(hDrive, IOCTL_STORAGE_GET_DEVICE_NUMBER, nil, 0, @sdn, sizeof(sdn), dwBytesReturned, nil);
              if res  then
              begin
                if ( DeviceNumber = sdn.DeviceNumber) then
                begin  // match the given device number with the one of the current device
                  Result:= spdd.DevInst;
                  exit;
                end;
              end;
          finally
            CloseHandle(hDrive);
          end;
        end;
       finally
         FreeMem(pspdidd);
       end;
      end;
      Inc(dwIndex);
    end;
  finally
   SetupDiDestroyDeviceInfoList(hDevInfo);
  end;
end;

procedure EjectUSB(const DriveLetter:char);
var
  szRootPath, szDevicePath : PChar;
  szVolumeAccessPath : PChar;
  hVolume : THandle;
  DeviceNumber : LONG;
  sdn  : STORAGE_DEVICE_NUMBER;
  dwBytesReturned : DWORD;
  res : BOOL;
  resCM : Cardinal;
  DriveType : UINT;
  szDosDeviceName : array [0..MAX_PATH-1] of Char;
  DevInst  : CfgMgr32.DEVINST;
  VetoType : PNP_VETO_TYPE;
  VetoName : array [0..MAX_PATH-1] of WCHAR;
  bSuccess : Boolean;
  DevInstParent : CfgMgr32.DEVINST;
  tries :  Integer;
begin
    szRootPath := PChar(DriveLetter+':\');
    szDevicePath := PChar(DriveLetter+':');
  szVolumeAccessPath := PChar(Format('\\.\%s:',[DriveLetter]));

  DeviceNumber:=-1;
    // open the storage volume
  hVolume := CreateFile(szVolumeAccessPath, 0, FILE_SHARE_READ OR FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
    if (hVolume <> INVALID_HANDLE_VALUE) then
   try
    //get the volume's device number
    dwBytesReturned := 0;
    res := DeviceIoControl(hVolume, IOCTL_STORAGE_GET_DEVICE_NUMBER, nil, 0, @sdn, SizeOf(sdn), dwBytesReturned, nil);
    if res then
      DeviceNumber := sdn.DeviceNumber;
   finally
     CloseHandle(hVolume);
   end;
   if DeviceNumber=-1 then exit;

    // get the drive type which is required to match the device numbers correctely
    DriveType := GetDriveType(szRootPath);

    // get the dos device name (like \device\floppy0) to decide if it's a floppy or not - who knows a better way?
    QueryDosDevice(szDevicePath, szDosDeviceName, MAX_PATH);

    // get the device instance handle of the storage volume by means of a SetupDi enum and matching the device number
    DevInst := GetDrivesDevInstByDeviceNumber(DeviceNumber, DriveType, szDosDeviceName);

    if ( DevInst = 0 ) then
   exit;

  VetoType := PNP_VetoTypeUnknown;
    bSuccess := false;

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

    for tries:=0 to 3 do // sometimes we need some tries...
  begin
        FillChar(VetoName[0], SizeOf(VetoName), 0);

        // CM_Query_And_Remove_SubTree doesn't work for restricted users
        //resCM = CM_Query_And_Remove_SubTree(DevInstParent, &VetoType, VetoNameW, MAX_PATH, CM_REMOVE_NO_RESTART); // CM_Query_And_Remove_SubTreeA is not implemented under W2K!
        //resCM = CM_Query_And_Remove_SubTree(DevInstParent, NULL, NULL, 0, CM_REMOVE_NO_RESTART);  // with messagebox (W2K, Vista) or balloon (XP)

        resCM := CM_Request_Device_Eject(DevInstParent, @VetoType, @VetoName[0], Length(VetoName), 0);
        resCM := CM_Request_Device_Eject(DevInstParent,nil, nil, 0, 0); // optional -> shows messagebox (W2K, Vista) or balloon (XP)

        bSuccess := (resCM=CR_SUCCESS) and (VetoType=PNP_VetoTypeUnknown);
        if ( bSuccess )  then
            break;

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

    if ( bSuccess ) then
        Writeln('Success')
  else
      Writeln('Failed');
end;

begin
  try
    LoadSetupApi;
    LoadConfigManagerApi;
    EjectUSB('F');
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;

end.

Solution 3

This doesn't eject the drive, but it flushes the drive buffers and makes it safe to remove. It requires administrative rights under Vista and higher (and XP if running as a limited rights user, IIRC). It probably should have a try..finally to make sure that CloseHandle gets called; I leave that as an exercise to the reader, since code formattig is tight here without horizontal scrolling. :-)

unit USBDriveFlush;

interface

  uses Windows;

type
  // Taken from JEDI JwaWinIoctl
  PSTORAGE_HOTPLUG_INFO = ^STORAGE_HOTPLUG_INFO;
  {$EXTERNALSYM PSTORAGE_HOTPLUG_INFO}
  _STORAGE_HOTPLUG_INFO = record
    Size: DWORD; // version
    MediaRemovable: BOOLEAN; // ie. zip, jaz, cdrom, mo, etc. vs hdd
    MediaHotplug: BOOLEAN;   // ie. does the device succeed a lock 
                             // even though its not lockable media?
    DeviceHotplug: BOOLEAN;  // ie. 1394, USB, etc.
    WriteCacheEnableOverride: BOOLEAN; // This field should not be 
                             // relied upon because it is no longer used
  end;
  {$EXTERNALSYM _STORAGE_HOTPLUG_INFO}
  STORAGE_HOTPLUG_INFO = _STORAGE_HOTPLUG_INFO;
  {$EXTERNALSYM STORAGE_HOTPLUG_INFO}
  TStorageHotplugInfo = STORAGE_HOTPLUG_INFO;
  PStorageHotplugInfo = PSTORAGE_HOTPLUG_INFO;    

  function FlushUSBDrive(const Drive: string): Boolean;

implementation

function FlushUSBDrive(const Drive: string): Boolean;
var
  shpi : TStorageHotplugInfo;
  retlen : DWORD; //unneeded, but deviceiocontrol expects it
  h : THandle;
begin
  Result := False;
  h := CreateFile(PChar('\\.\' + Drive),
                  0,
                  FILE_SHARE_READ or FILE_SHARE_WRITE,
                  nil,
                  OPEN_EXISTING,
                  0,
                  0);
  if h <> INVALID_HANDLE_VALUE then
  begin
    shpi.Size := SizeOf(shpi);

    if DeviceIoControl(h,
                       IOCTL_STORAGE_GET_HOTPLUG_INFO,
                       nil,
                       0,
                       @shpi,
                       SizeOf(shpi),
                       retlen,
                       nil) then
    begin
      //shpi now has the existing values, so you can check to
      //see if the device is already hot-pluggable
      if not shpi.DeviceHotplug then
      begin
        shpi.DeviceHotplug:= True;

        //Need to use correct administrator security privilages here
        //otherwise it'll just give 'access is denied' error
        Result := DeviceIoControl(h,
                                  IOCTL_STORAGE_SET_HOTPLUG_INFO,
                                   @shpi,
                                   SizeOf(shpi),
                                   nil,
                                   0,
                                   retlen,
                                   nil);
      end;
    end;
    CloseHandle(h);
  end;
end;

Sample use:

if FlushUSBDrive('G:') then
  ShowMessage('Safe to remove USB drive G:')
else
  ShowMessage('Flush of drive G: failed!' +
    SysErrorMessage(GetLastError()));
Share:
12,101
Gath
Author by

Gath

Updated on June 14, 2022

Comments

  • Gath
    Gath almost 2 years

    How can I detect and remove a USB flash disk programatically using delphi?

    I have seen some of the examples in this website, but they lack clear explanation on how to go about it!

    Please examples will really help!

  • MSalters
    MSalters over 15 years
    It would be quite bad if a non-user admin could remove a USB disk that's in use by an admin (or even determine whether it's in use). So the limitation seems logical.
  • mghie
    mghie over 15 years
    Not really. This code does not work for me (non-admin, member of "Power Users") even though I can remove the USB stick via the "Safely Remove Hardware" icon in the notification area. It works with "Run As..." and the Administrator account, though...
  • Jerry Dodge
    Jerry Dodge almost 12 years
    It keeps failing, returning false on a flash stick... Checking administrative privileges...
  • Ken White
    Ken White almost 12 years
    Works on my thumb drive and an external USB 1TB drive. Are you getting an error? ("It keeps failing" and "didn't work either" isn't a lot of info.)
  • Jerry Dodge
    Jerry Dodge almost 12 years
    It doesn't pass if not shpi.DeviceHotplug then begin
  • Ken White
    Ken White almost 12 years
    If you're not getting an error, and shpi.DeviceHotplug is returning true, then the drive is already safe to eject. I know nothing about the capabilities of your device, or what state it's in. :-) I can only tell you that the code works, and has been used more than once since I originally found and modified it a few years ago.
  • Jerry Dodge
    Jerry Dodge almost 12 years
    Yes there is a massive variety of possible reasons it's returning false. I guess I'll have to test it on a number of different machines in different scenarios.
  • mghie
    mghie almost 12 years
    @Jerry: If you try to edit the post you will notice that the source is complete, but the SO engine somehow can't handle it. I've flagged it for moderator attention.
  • Ken White
    Ken White almost 12 years
    @mghie, if you get your post fixed where the code is complete, let me know and I'll delete my answer (only fair, since you did most of the work - I just used the C code to fill in what wasn't there).
  • Ken White
    Ken White almost 12 years
    As promised, mine deleted (and +1 for the one I got instead).
  • Jerry Dodge
    Jerry Dodge almost 12 years
    AHH I see instead of G:\ it's returning G:T or some other weird character... Changing to RootName := ADrive + ':\'; worked :D
  • mghie
    mghie almost 12 years
    @Jerry: I used a different trick to make the SO syntax highlighting happy. It still looks a little weird, but copy and paste into the Unicode IDE should simply work now.
  • mghie
    mghie almost 12 years
    @Ken: Thanks, much appreciated.
  • Jerry Dodge
    Jerry Dodge almost 12 years
    Thanks for all the help, I'm up and working fine now. This will be used on a timer which will automatically eject a backup disk at 5:00 every afternoon, or on any given schedule. I have 2 portable HDD's which I switch back and forth every night for performing backups and taking a backup off-site (I know other solutions exist but I'm making this one customized).