27 November 2008

Howto: Get the physical drive string //./PhysicalDriveX from a path

There are lots of strings that you can feed CreateFile, if we are looking at volumes and drives then they include:

The Unique Volume Name:

\\?\Volume{013eeefb-9b12-11dd-bee5-806e6f6e6963}\

You can list those at the command prompt with: "mountvol".

The mount point:

\\.\C:

If you really want to list them at the command line, then "fsutil fsinfo drives" will do the trick.

The physical drive string (or whatever it is called)

\\.\PhysicalDrive0

command: "wmic diskdrive get name,size,model"

But, how can we get all of the physical drive strings available? Well, the hint is in the CreateFile documentation.

To obtain the physical drive for a volume, open a handle to the volume and call the DeviceIoControl function with IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS. This control code returns the disk number of offset for each of the volume's extents; a volume can span disks.

In .Net then, start by enumerating all the drives, and create strings like the mount points above "\\.\X:". Use this with CreateFile to get a handle to the volume. Then call DeviceIOControl with IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS.

The Class that does the hard work:

Project type - vb.net class library (2005 or 2008) - name JdMcF.VolumeInfo:

Option Strict On
Option Explicit On

Imports Microsoft.Win32.SafeHandles
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.ComponentModel

Public Class VolumeInfo

   Private Const GenericRead As Integer = &H80000000
   Private Const FileShareRead As Integer = 1
   Private Const Filesharewrite As Integer = 2
   Private Const OpenExisting As Integer = 3
   Private Const IoctlVolumeGetVolumeDiskExtents As Integer = &H560000
   Private Const IncorrectFunction As Integer = 1
   Private Const ErrorInsufficientBuffer As Integer = 122

   Private Class NativeMethods
       <DllImport("kernel32", CharSet:=CharSet.Unicode, SetLastError:=True)> _
       Public Shared Function CreateFile( _
           ByVal fileName As String, _
           ByVal desiredAccess As Integer, _
           ByVal shareMode As Integer, _
           ByVal securityAttributes As IntPtr, _
           ByVal creationDisposition As Integer, _
           ByVal flagsAndAttributes As Integer, _
           ByVal hTemplateFile As IntPtr) As SafeFileHandle
       End Function

       <DllImport("kernel32", SetLastError:=True)> _
       Public Shared Function DeviceIoControl( _
           ByVal hVol As SafeFileHandle, _
           ByVal controlCode As Integer, _
           ByVal inBuffer As IntPtr, _
           ByVal inBufferSize As Integer, _
           ByRef outBuffer As DiskExtents, _
           ByVal outBufferSize As Integer, _
           ByRef bytesReturned As Integer, _
           ByVal overlapped As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
       End Function

       <DllImport("kernel32", SetLastError:=True)> _
       Public Shared Function DeviceIoControl( _
           ByVal hVol As SafeFileHandle, _
           ByVal controlCode As Integer, _
           ByVal inBuffer As IntPtr, _
           ByVal inBufferSize As Integer, _
           ByVal outBuffer As IntPtr, _
           ByVal outBufferSize As Integer, _
           ByRef bytesReturned As Integer, _
           ByVal overlapped As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
       End Function
   End Class

   ' DISK_EXTENT in the msdn.
   <StructLayout(LayoutKind.Sequential)> _
   Private Structure DiskExtent
       Public DiskNumber As Integer
       Public StartingOffset As Long
       Public ExtentLength As Long
   End Structure

   ' DISK_EXTENTS
   <StructLayout(LayoutKind.Sequential)> _
   Private Structure DiskExtents
       Public numberOfExtents As Integer
       Public first As DiskExtent ' We can't marhsal an array if we don't know its size.
   End Structure

   ' A Volume could be on many physical drives.
   ' Returns a list of string containing each physical drive the volume uses.
   ' For CD Drives with no disc in it will return an empty list.
   Public Shared Function GetPhysicalDriveStrings(ByVal driveInfo As DriveInfo) As List(Of String)
       Dim sfh As SafeFileHandle = Nothing
       Dim physicalDrives As New List(Of String)(1)
       Dim path As String = "\\.\" & driveInfo.RootDirectory.ToString.TrimEnd("\"c)
       Try
           sfh = NativeMethods.CreateFile(path, GenericRead, FileShareRead Or Filesharewrite, IntPtr.Zero, _
                                                          OpenExisting, 0, IntPtr.Zero)
           Dim bytesReturned As Integer
           Dim de1 As DiskExtents = Nothing
           Dim numDiskExtents As Integer = 0
           Dim result As Boolean = NativeMethods.DeviceIoControl(sfh, IoctlVolumeGetVolumeDiskExtents, IntPtr.Zero, _
                                                                 0, de1, Marshal.SizeOf(de1), bytesReturned, IntPtr.Zero)
           If result = True Then
               ' there was only one disk extent. So the volume lies on 1 physical drive.
               physicalDrives.Add("\\.\PhysicalDrive" & de1.first.DiskNumber.ToString)
               Return physicalDrives
           End If
           If Marshal.GetLastWin32Error = IncorrectFunction Then
               ' The drive is removable and removed, like a CDRom with nothing in it.
               Return physicalDrives
           End If
           If Marshal.GetLastWin32Error <> ErrorInsufficientBuffer Then
               Throw New Win32Exception
           End If           
           ' Houston, we have a spanner. The volume is on multiple disks.
           ' Untested...
           ' We need a blob of memory for the DISK_EXTENTS structure, and all the DISK_EXTENTS
           Dim blobSize As Integer = Marshal.SizeOf(GetType(DiskExtents)) + _
                                     (de1.numberOfExtents - 1) * Marshal.SizeOf(GetType(DiskExtent))
           Dim pBlob As IntPtr = Marshal.AllocHGlobal(blobSize)
           result = NativeMethods.DeviceIoControl(sfh, IoctlVolumeGetVolumeDiskExtents, IntPtr.Zero, 0, pBlob, _
                                                  blobSize, bytesReturned, IntPtr.Zero)
           If result = False Then Throw New Win32Exception
           ' Read them out one at a time.
           Dim pNext As New IntPtr(pBlob.ToInt32 + 4) ' is this always ok on 64 bit OSes? ToInt64?
           For i As Integer = 0 To de1.numberOfExtents - 1
               Dim diskExtentN As DiskExtent = DirectCast(Marshal.PtrToStructure(pNext, GetType(DiskExtent)), DiskExtent)
               physicalDrives.Add("\\.\PhysicalDrive" & diskExtentN.DiskNumber.ToString)
               pNext = New IntPtr(pNext.ToInt32 + Marshal.SizeOf(GetType(DiskExtent)))
           Next
           Return physicalDrives
       Finally
           If sfh IsNot Nothing Then
               If sfh.IsInvalid = False Then
                   sfh.Close()
               End If
               sfh.Dispose()
           End If
       End Try
   End Function

End Class

The Test project:

Project type: vb.net windows forms app (2005 or 2008) name: whatever.

Option Strict On
Option Explicit On
Option Infer Off

Imports JDMcF.VolumeInfo
Imports System.IO
Imports System.Text

Public Class Form1

   Private lv1 As New ListView

   Sub New()

       ' This call is required by the Windows Form Designer.
       InitializeComponent()

       ' Add any initialization after the InitializeComponent() call.
       With lv1
           .View = View.Details
           .Columns.Add("Root", 100, HorizontalAlignment.Left)
           .Columns.Add("Physical Drive String", 200, HorizontalAlignment.Left)
           .Dock = DockStyle.Fill
       End With
       Me.Controls.Add(lv1)       
   End Sub

   Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       For Each di As DriveInfo In DriveInfo.GetDrives
           Dim drivesList As List(Of String) = VolumeInfo.GetPhysicalDriveStrings(di)

           Dim drives As New StringBuilder
           If drivesList.Count > 0 Then
               For Each s As String In drivesList
                   drives.Append(s)
                   drives.Append(", ")
               Next
               drives.Remove(drives.Length - 2, 2)
           Else
               drives.Append("n/a")
           End If
           Dim lvi As New ListViewItem(di.RootDirectory.ToString)
           lvi.SubItems.Add(drives.ToString)
           lv1.Items.Add(lvi)
       Next
   End Sub

End Class

Linky to code:

3 comments:

  1. Very good stuff! I seldom see this type of professional work on the net. It helps a lot for even experenced VB.NET programmers.

    One question though, you mentioned the volume name at the very beginning but the code does not seem giving a clue how to get the vlome name (this one does: http://technet.microsoft.com/en-us/sysinternals/bb896648). I am looking for a way to find the first volume name on a disk given a disk numner or a DOS device name like \\.\PhysicalDrive1. Is that possible at all?

    Thanks a lot! Again, your .NET stuff is great!

    ReplyDelete
  2. Thanks for sharing this! I was searching for this for a while... There so much info about this on the net for c / c++, and so little for .net that works. Even the pinvoke.net example doesn't work... lol. Great code, man. Thanks again.

    @rliunet: once you have the PhysicalDrive information, you can use QueryDosDevice to get the volume information. There's a working example on pinvoke.net.

    ReplyDelete
  3. Thanks for the info.

    I didn't really use your source, but you did provide enough info otherwise.

    ReplyDelete