USB drive safe removal by drive letter

Share your advanced PureBasic knowledge/code with the community.
breeze4me
Enthusiast
Enthusiast
Posts: 633
Joined: Thu Mar 09, 2006 9:24 am
Location: S. Kor

USB drive safe removal by drive letter

Post by breeze4me »

Reference: How to prepare a USB drive for safe removal (By Uwe_Sieber)
(http://www.codeproject.com/system/Remov ... Letter.asp)

Enable "Create Unicode executable" option, and then run.

It seems to work fine.

Tested on XP SP2, PB v4.02/4.10 beta 2

Code: Select all

;
; RemoveDriveByLetter.cpp by Uwe Sieber - www.uwe-sieber.de
;
; Simple demonstration how to prepare a disk drive for save removal
;
; Works with removable and fixed drives under W2K, XP, W2K3, Vista
;
; Console application - expects the drive letter of the drive to remove as parameter
;
; you are free to use this code in your projects
;

EnableExplicit


Macro CTL_CODE(DeviceType, Function, Method, Access)
  (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
EndMacro

#FILE_DEVICE_MASS_STORAGE = $0000002d
#IOCTL_STORAGE_BASE = #FILE_DEVICE_MASS_STORAGE
#FILE_ANY_ACCESS = 0
#METHOD_BUFFERED = 0
#IOCTL_STORAGE_GET_DEVICE_NUMBER = CTL_CODE(#IOCTL_STORAGE_BASE, $0420, #METHOD_BUFFERED, #FILE_ANY_ACCESS)

#CM_REMOVE_NO_RESTART = $00000002

#DIGCF_PRESENT         = $00000002
#DIGCF_ALLCLASSES      = $00000004
#DIGCF_PROFILE         = $00000008
#DIGCF_DEVICEINTERFACE = $00000010

#PNP_VetoTypeUnknown = 0
#CR_SUCCESS = 0

Structure PSP_DEVICE_INTERFACE_DETAIL_DATA
  cbSize.l
  DevicePath.s{1024}
EndStructure

Structure SP_DEVINFO_DATA
  cbSize.l
  ClassGuid.GUID
  DevInst.l
  Reserved.l
EndStructure

Structure SP_DEVICE_INTERFACE_DATA
  cbSize.l
  InterfaceClassGuid.GUID
  Flags.l
  Reserved.l
EndStructure

Structure STORAGE_DEVICE_NUMBER
  DeviceType.l      ;// The FILE_DEVICE_XXX type For this device.
  DeviceNumber.l    ;// The number of this device
  PartitionNumber.l ;// If the device is partitionable, the partition number of the device. Otherwise -1
EndStructure

Prototype.l CM_Get_Parent(*DevInst, DevInst, Flags)
; CM_Get_Parent(
;              OUT PDEVINST pdnDevInst,
;              IN  DEVINST  dnDevInst,
;              IN  ULONG    ulFlags
;              );
Prototype.l CM_Get_Device_IDW(DevInst, DeviceIdString.s, BufferLen, Flags)
; CM_Get_Device_IDW(
;              IN  DEVINST  dnDevInst,
;              OUT PWCHAR   Buffer,
;              IN  ULONG    BufferLen,
;              IN  ULONG    ulFlags
;              );
Prototype.l CM_Request_Device_EjectW(DevInst, *VetoType, VetoName.s, NameLength, Flags)
; CM_Request_Device_EjectW(
;             IN  DEVINST         dnDevInst,
;             OUT PPNP_VETO_TYPE  pVetoType,
;             OUT LPWSTR          pszVetoName,
;             IN  ULONG           ulNameLength,
;             IN  ULONG           ulFlags
;             );
Prototype.l CM_Query_And_Remove_SubTreeW(Ancestor, *VetoType, VetoName.s, NameLength, Flags)
; CM_Query_And_Remove_SubTreeW(
;              IN  DEVINST        dnAncestor,
;              OUT PPNP_VETO_TYPE pVetoType,
;              OUT LPWSTR         pszVetoName,
;              IN  ULONG          ulNameLength,
;              IN  ULONG          ulFlags
;              );
Prototype.l SetupDiEnumDeviceInterfaces(DeviceInfoSet, DeviceInfoData, *InterfaceClassGuid, MemberIndex, *DeviceInterfaceData)
; SetupDiEnumDeviceInterfaces(
;     IN  HDEVINFO                   DeviceInfoSet,
;     IN  PSP_DEVINFO_DATA           DeviceInfoData,     OPTIONAL
;     IN  CONST GUID                *InterfaceClassGuid,
;     IN  DWORD                      MemberIndex,
;     OUT PSP_DEVICE_INTERFACE_DATA  DeviceInterfaceData
;     );
Prototype.l SetupDiGetDeviceInterfaceDetailW(DeviceInfoSet, DeviceInterfaceData, *DeviceInterfaceDetailData, DeviceInterfaceDetailDataSize, *RequiredSize, *DeviceInfoData)
; SetupDiGetDeviceInterfaceDetailW(
;     IN  HDEVINFO                           DeviceInfoSet,
;     IN  PSP_DEVICE_INTERFACE_DATA          DeviceInterfaceData,
;     OUT PSP_DEVICE_INTERFACE_DETAIL_DATA_W DeviceInterfaceDetailData,     OPTIONAL
;     IN  DWORD                              DeviceInterfaceDetailDataSize,
;     OUT PDWORD                             RequiredSize,                  OPTIONAL
;     OUT PSP_DEVINFO_DATA                   DeviceInfoData                 OPTIONAL
;     );

Global CM_Get_Parent.CM_Get_Parent
Global CM_Get_Device_ID.CM_Get_Device_IDW
Global CM_Request_Device_EjectW.CM_Request_Device_EjectW
Global CM_Query_And_Remove_SubTreeW.CM_Query_And_Remove_SubTreeW
Global SetupDiEnumDeviceInterfaces.SetupDiEnumDeviceInterfaces
Global SetupDiGetDeviceInterfaceDetail.SetupDiGetDeviceInterfaceDetailW


; ----------------------------------------------------------------------
;  returns the device instance handle of a storage volume or 0 on error
; ----------------------------------------------------------------------
Procedure.l GetDrivesDevInstByDeviceNumber(DeviceNumber, DriveType, DosDeviceName.s)
	Protected IsFloppy, *guid, Index, res, Size, hDevInfo
	Protected pspdidd.PSP_DEVICE_INTERFACE_DETAIL_DATA, spdid.SP_DEVICE_INTERFACE_DATA, spdd.SP_DEVINFO_DATA
  Protected sdn.STORAGE_DEVICE_NUMBER
  Protected hDrive, BytesReturned
  
	IsFloppy = FindString(DosDeviceName, "\Floppy", 1)  ; // who knows a better way?
  
	Select DriveType
  	Case #DRIVE_REMOVABLE
  		If IsFloppy
  			*guid = ?GUID_DEVINTERFACE_FLOPPY
  		Else
  			*guid = ?GUID_DEVINTERFACE_DISK
      EndIf
  	Case #DRIVE_FIXED
  	  *guid = ?GUID_DEVINTERFACE_DISK
  	Case #DRIVE_CDROM
  		*guid = ?GUID_DEVINTERFACE_CDROM
  	Default
  		ProcedureReturn 0
	EndSelect

	; Get device interface info set handle for all devices attached to system
	hDevInfo = SetupDiGetClassDevs_(*guid, 0, 0, #DIGCF_PRESENT|#DIGCF_DEVICEINTERFACE)
  
	If hDevInfo = #INVALID_HANDLE_VALUE
		ProcedureReturn 0
	EndIf

	; Retrieve a context structure for a device interface of a device information set
  
	spdid\cbSize = SizeOf(SP_DEVICE_INTERFACE_DATA)
  
	Repeat
		res = SetupDiEnumDeviceInterfaces(hDevInfo, 0, *guid, Index, @spdid)

		If res = 0
			Break
		EndIf
		
		SetupDiGetDeviceInterfaceDetail(hDevInfo, @spdid, 0, 0, @Size, 0) ; check the buffer size
		If Size<>0 And Size<=SizeOf(PSP_DEVICE_INTERFACE_DETAIL_DATA)
			pspdidd\cbSize = 6    ;For Unicode. ASCII=5
      
			spdd\cbSize = SizeOf(SP_DEVINFO_DATA)
      
      ;res = SetupDiGetDeviceInterfaceDetail(hDevInfo, @spdid, @pspdidd, Size, @Size, @spdd)
			res = SetupDiGetDeviceInterfaceDetail(hDevInfo, @spdid, @pspdidd, Size, 0, @spdd)
			If res
				; in case you are interested in the USB serial number:
				; the device id string contains the serial number if the device has one,
				; otherwise a generated id that contains the '&' char...
;         Protected DevInstParent, DeviceIdString.s=Space(#MAX_PATH)
;  				CM_Get_Parent(@DevInstParent, spdd\DevInst, 0)
;  				CM_Get_Device_ID(DevInstParent, DeviceIdString, #MAX_PATH, 0)
;         Debug "DeviceId= " + DeviceIdString

				; open the disk or cdrom or floppy
				hDrive = CreateFile_(@pspdidd\DevicePath, 0, #FILE_SHARE_READ|#FILE_SHARE_WRITE, 0, #OPEN_EXISTING, 0, 0)
				If hDrive <> #INVALID_HANDLE_VALUE
					; get its device number
					BytesReturned = 0
					res = DeviceIoControl_(hDrive, #IOCTL_STORAGE_GET_DEVICE_NUMBER, 0, 0, @sdn, SizeOf(STORAGE_DEVICE_NUMBER), @BytesReturned, 0)
					If res
						If DeviceNumber = sdn\DeviceNumber  ; match the given device number with the one of the current device
							CloseHandle_(hDrive)
							SetupDiDestroyDeviceInfoList_(hDevInfo)
							ProcedureReturn spdd\DevInst
						EndIf
					EndIf
					CloseHandle_(hDrive)
				EndIf
			EndIf
		EndIf
		Index + 1
	ForEver

	SetupDiDestroyDeviceInfoList_(hDevInfo)

	ProcedureReturn 0
EndProcedure

Procedure RemoveDrive(drive$)
  Protected RootPath.s, DevicePath.s, VolumeAccessPath.s, hVolume
  Protected sdn.STORAGE_DEVICE_NUMBER, res, DriveType, DeviceNumber=-1
  Protected DosDeviceName.s
  Protected DevInst, BytesReturned, DevInstParent
  Protected VetoType, VetoNameW.s, tries, bSuccess=#False
  
  drive$ = Left(UCase(drive$), 1)
  If drive$<"A" Or drive$>"Z" : Goto exit : EndIf
  
  DevicePath = drive$ + ":"       ; "X:"   -> for QueryDosDevice
  RootPath = DevicePath + "\"     ; "X:\"  -> for GetDriveType
  VolumeAccessPath = "\\.\" + DevicePath  ; "\\.\X:"  -> to open the volume
  
  ; open the storage volume
  hVolume = CreateFile_(VolumeAccessPath, 0, #FILE_SHARE_READ|#FILE_SHARE_WRITE, 0, #OPEN_EXISTING, 0, 0)
  
  If hVolume <> #INVALID_HANDLE_VALUE
  	; get the volume's device number
  	res = DeviceIoControl_(hVolume, #IOCTL_STORAGE_GET_DEVICE_NUMBER, 0, 0, @sdn, SizeOf(STORAGE_DEVICE_NUMBER), @BytesReturned, 0)
  	If res
  		DeviceNumber = sdn\DeviceNumber
  	EndIf
  	CloseHandle_(hVolume)
  
  	If DeviceNumber = -1
  		Goto exit
  	EndIf
  
  	; get the drive type which is required To match the device numbers correctely
  	DriveType = GetDriveType_(RootPath)
  
  	; get the dos device name (like \device\floppy0) to decide if it's a floppy or not - who knows a better way?
  	DosDeviceName = Space(#MAX_PATH)
  	res = QueryDosDevice_(DevicePath, DosDeviceName, #MAX_PATH)
  	
  	If res = 0
  		Goto exit
  	EndIf
  
  	; get the device instance handle of the storage volume by means of a SetupDi enum and matching the device number
  	DevInst = GetDrivesDevInstByDeviceNumber(DeviceNumber, DriveType, DosDeviceName)
  
  	If DevInst = 0
  		Goto exit
  	EndIf
    
  	;VetoType = #PNP_VetoTypeUnknown  ;#PNP_VetoTypeUnknown=0
  	VetoNameW = Space(#MAX_PATH)
    
  	; get drives's parent, e.g. the USB bridge, the SATA port, an IDE channel with two drives!
  	res = CM_Get_Parent(@DevInstParent, DevInst, 0) 
  
  	For tries = 1 To 3  ; sometimes we need some tries...
  		
  
  		; CM_Query_And_Remove_SubTree doesn't work for restricted users
  		;res = CM_Query_And_Remove_SubTreeW(DevInstParent, @VetoType, VetoNameW, #MAX_PATH, #CM_REMOVE_NO_RESTART); ; CM_Query_And_Remove_SubTreeA is not implemented under W2K!
  		;res = CM_Query_And_Remove_SubTreeW(DevInstParent, 0, #NUL$, 0, #CM_REMOVE_NO_RESTART);  ; with messagebox (W2K, Vista) or balloon (XP)
  
  		res = CM_Request_Device_EjectW(DevInstParent, @VetoType, VetoNameW, #MAX_PATH, 0)
  		;res = CM_Request_Device_EjectW(DevInstParent, 0, #NUL$, 0, 0); ; with messagebox (W2K, Vista) or balloon (XP)
      
  		If res=#CR_SUCCESS And VetoType=#PNP_VetoTypeUnknown
  		  bSuccess = 1
  		  Break
  		Else
    		bSuccess = 0
  		EndIf
  
  		Sleep_(500) ; required to give the next tries a chance!
  	Next
  	
  	If bSuccess
  	  Debug "Success"
  	Else
  	  Debug "failed"
    	Debug res
    	Debug VetoNameW
  	EndIf
    
  EndIf
  
  exit:
  
  ProcedureReturn 
EndProcedure


; -----------------------
; test code
; -----------------------

Define drive$
#setupapi = 1

If OpenLibrary(#setupapi, "setupapi.dll")
  CM_Get_Parent = GetFunction(#setupapi, "CM_Get_Parent")
  CM_Get_Device_ID = GetFunction(#setupapi, "CM_Get_Device_IDW")
  CM_Request_Device_EjectW = GetFunction(#setupapi, "CM_Request_Device_EjectW")
  CM_Query_And_Remove_SubTreeW = GetFunction(#setupapi, "CM_Query_And_Remove_SubTreeW")
  SetupDiEnumDeviceInterfaces = GetFunction(#setupapi, "SetupDiEnumDeviceInterfaces")
  SetupDiGetDeviceInterfaceDetail = GetFunction(#setupapi, "SetupDiGetDeviceInterfaceDetailW")
  
  drive$ = "z"    ;any drive letter to want to remove.
  RemoveDrive(drive$)
  
  CloseLibrary(#setupapi)
EndIf

End

DataSection
  GUID_DEVINTERFACE_FLOPPY:
    Data.l $53f56311
    Data.w $b6bf, $11d0
    Data.b $94, $f2, $00, $a0, $c9, $1e, $fb, $8b
  GUID_DEVINTERFACE_DISK:
    Data.l $53f56307
    Data.w $b6bf, $11d0
    Data.b $94, $f2, $00, $a0, $c9, $1e, $fb, $8b
  GUID_DEVINTERFACE_CDROM:
    Data.l $53f56308
    Data.w $b6bf, $11d0
    Data.b $94, $f2, $00, $a0, $c9, $1e, $fb, $8b
EndDataSection