如何使用Delphi编程以程序化方式移除USB闪存盘?

12

如何使用Delphi编程检测并删除USB闪存驱动器程序?

我看过这个网站上的一些示例,但它们缺乏清晰的解释来指导应该如何操作!

请提供示例,这将非常有帮助!

3个回答

9
这是从support.microsoft.com的示例代码快速而简单地翻译出来的移除驱动器内容。但是,它仅适用于具有管理员权限的用户。
有关一般情况下使用USB设备的更多信息,请参阅此答案,作者为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;

如果非用户管理员可以移除管理员正在使用的USB磁盘(甚至确定它是否在使用),那将是相当糟糕的。因此,这种限制似乎是合理的。 - MSalters
不是真的。即使我可以通过通知区域中的“安全删除硬件”图标移除USB驱动器,但这段代码对我(非管理员,“Power Users”的成员)无效。不过,使用“以管理员身份运行”和管理员帐户可以解决问题... - mghie
1
@mghie,如果您修复了帖子并完整了代码,请告诉我,我会删除我的答案(这是公平的,因为您做了大部分工作 - 我只是使用C代码填补了缺失的部分)。 - Ken White
1
如承诺,我删除了我的(并且+1替代了我得到的那个)。 - Ken White
1
@Jerry:我使用了另一种技巧来让SO语法高亮变得更好。虽然看起来还是有点奇怪,但现在复制粘贴到Unicode IDE中应该就可以正常工作了。 - mghie
显示剩余3条评论

4
使用 CM_Request_Device_Eject 函数来拔出 USB 驱动器,
请参考这篇文章如何准备 USB 驱动器以进行安全拔出,并查看基于此的 Delphi 应用程序,使用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.

2

这并不会弹出驱动器,但它会刷新驱动器缓存并使其可以安全地拆卸。在Vista及更高版本下需要管理员权限(如果作为受限制的用户运行XP,则也需要),最好使用try..finally确保调用CloseHandle;我将其留给读者练习,因为代码格式紧凑,没有水平滚动条。 :-)

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;

示例用法:

if FlushUSBDrive('G:') then
  ShowMessage('Safe to remove USB drive G:')
else
  ShowMessage('Flush of drive G: failed!' +
    SysErrorMessage(GetLastError()));

它一直失败,闪存上返回false...检查管理员权限... - Jerry Dodge
可以在我的闪存驱动器和外置的1TB USB驱动器上运行。你是否遇到了错误?(“它一直失败”和“也没有起作用”并不是很多信息。) - Ken White
它不通过 if not shpi.DeviceHotplug then begin - Jerry Dodge
如果您没有收到错误消息,并且shpi.DeviceHotplug返回true,则该驱动器已经可以安全地弹出。我不了解您的设备能力或其状态。 :-) 我只能告诉您代码是有效的,并且自几年前我最初发现并修改它以来已经被使用多次。 - Ken White
是的,它返回false的可能原因非常多。我想我必须在不同的场景和机器上进行测试。 - Jerry Dodge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接