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:
Very good stuff! I seldom see this type of professional work on the net. It helps a lot for even experenced VB.NET programmers.
ReplyDeleteOne 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!
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.
ReplyDelete@rliunet: once you have the PhysicalDrive information, you can use QueryDosDevice to get the volume information. There's a working example on pinvoke.net.
Thanks for the info.
ReplyDeleteI didn't really use your source, but you did provide enough info otherwise.