Search code examples
vb.netioerror

IO Exception on some XP systems getting WMI Disk information


The Problem

Two machines display the runtime error: IO Exception The device is not ready. This only happens for 2 out of 8 machines I've tested it on.

These are server versions from SBS 2003 to Windows XP, Vista, 7,8,8.1, Server 2012. It's a broad range.

The two machines in question are:

  • Windows Server 2003 SP2 (.NET framework V4/V4.0 both installed)
  • Windows XP SP3 (.NET framework V4/V4.0 both installed)

Please note: I installed Windows XP fresh on a VM, installed .NET framework 4.0 and the program ran without error.

My Investigation and Testing

To start things of my application is targeted to .NET framework 4.0 and all referenced external .DLLS are included in the application start folder.

From research I determined that the error was related to drive access. In my application there is two instances where I specifically query the system drive of the device. Once to grab disk space and the other to grab the serial.

So I created two programs, one has the function I use to grab diskspace and the other has the function I use to grab the HDD Serial.

I ran both programs on the work machines and low and behold a message box was displayed with free disk space and the HDD serial (no surprise there.)

I tried it on the above machines that display IO errors and I receive (for both applications) programname.exe is not a valid Win32 application.

^ That's weird right?

Here are the two functions in question.

Public Shared Function getHardwareID() As String

    Dim drive As String = "C"
    Dim disk As ManagementObject = _
        New ManagementObject _
        ("win32_logicaldisk.deviceid=""" + drive + ":""")
    disk.Get()
    Return disk("VolumeSerialNumber").ToString()

End Function


Public Shared Function getFreeDiskSpace() As String

    Dim freespacekb = My.Computer.FileSystem.Drives.Item(0).AvailableFreeSpace.ToString
    freespacekb = Format(freespacekb / 1024 / 1024 / 1024, "#0.00") _
        & " GB Free"
    Return freespacekb.ToString
End Function

Yes "C" is the drive letter for both machines.

EDIT:

I targeted one of the IO Tests to .NET Framework 4.0 Client Profile and it ran! Although with an exception, see paste bin below.

http://pastebin.com/FRngUeBN


Solution

  • Since these are old machines, there are several things which may be at play. I have an XP machine (for Civ3) and it too failed with an IO Exception on one of the calls . I extrapolated for the other.

    For the VolumeSerial, it tries WMI first, then polls the API if that fails. For Freespace, I got rid of all the VisualBasic calls and replaced it with NET calls to DriveInfo and then a call to GetDiskFreeSpaceEx if NET cant deliver.

    The Code (I suggest a class for all this):

    Imports

    Imports System.IO
    Imports System.Runtime.InteropServices
    Imports System.Text
    

    NativeMethods:

    <DllImport("Kernel32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function GetVolumeInformation(ByVal RootPathName As String,
            ByVal VolumeNameBuffer As System.Text.StringBuilder,
            ByVal VolumeNameSize As UInt32,
            ByRef VolumeSerialNumber As UInt32,
            ByRef MaximumComponentLength As UInt32,
            ByRef FileSystemFlags As UInt32, _
            ByVal FileSystemNameBuffer As System.Text.StringBuilder,
            ByVal FileSystemNameSize As UInt32
            ) As <MarshalAs(UnmanagedType.Bool)> Boolean
    
    End Function
    
    <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
    Private Shared Function GetDiskFreeSpaceEx(lpDirectoryName As String,
                                     ByRef lpFreeBytesAvailable As ULong,
                                     ByRef lpTotalNumberOfBytes As ULong,
                                     ByRef lpTotalNumberOfFreeBytes As ULong
                                     ) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function
    
    <Flags>
    Private Enum FileSystemFeature As UInteger
        CaseSensitiveSearch = 1
        CasePreservedNames = 2
        UnicodeOnDisk = 4
        PersistentACLS = 8
        FileCompression = &H10
        VolumeQuotas = &H20
        SupportsSparseFiles = &H40
        SupportsReparsePoints = &H80
        VolumeIsCompressed = &H8000
        SupportsObjectIDs = &H10000
        SupportsEncryption = &H20000
        NamedStreams = &H40000
        ReadOnlyVolume = &H80000
        SequentialWriteOnce = &H100000
        SupportsTransactions = &H200000
    End Enum
    

    If both alternatives fail for DriveSpace, it returns -1, an obviously illegal value. I did this instead of throwing an exception. Formatting the return should really be separate from getting the return which would make evaluating the return easier, but I left it the way you had it.

    I changed the calls to require a drive letter to poll to help eliminate some errors like trying to poll a nonexistant drive. The WMI versions could easily be modified to work on first fixed disk or even the 'Windows' drive, depending on what this is for.

    Public Shared Function getFreeDiskSpace(drvName As String) As String
    
        ' since this fails on just some machines
        ' AND the machines mentioned are old, they may well have floppy drives
    
        ' failure signal
        Dim freespace As Double = -1
    
        Dim myFreeBytesAvailable As ULong
        Dim myTotalNumberOfBytes As ULong
        Dim myTotalNumberOfFreeBytes As ULong
    
        If String.IsNullOrEmpty(drvName) Then
            Return freespace.ToString
        End If
    
        ' try the NET method
        Dim di As DriveInfo() = DriveInfo.GetDrives
    
        Try
            For Each drv In di
                ' look for desired drive in list
                If drv.Name.ToLowerInvariant.StartsWith(drvName.ToLowerInvariant(0)) Then
    
                    ' apply your desired logic... 
                    ' check only fixed drives for this use?
                    ' at least check if they are ready
                    'If drv.DriveType = DriveType.Fixed
                    ' If drv.IsReady
    
                    ' min test
                    If drv.DriveType = drv.IsReady Then
                        freespace = drv.TotalFreeSpace
                    End If
    
                    Exit For
                End If
    
            Next
        Catch ex As Exception
    
        End Try
    
        ' no answer yet, ask the API; should always work on valid drives
        If freespace = -1 Then
            ' format drv letter to name
            Dim drvpath As String = String.Format("{0}:\", drvName.ToLowerInvariant(0))
            ' call API
            If GetDiskFreeSpaceEx(drvpath, myFreeBytesAvailable, 
                        myTotalNumberOfBytes, myTotalNumberOfFreeBytes) Then
                freespace = myFreeBytesAvailable
            End If
    
        End If
    
        ' format bytes
        If freespace > -1 Then
            freespace = freespace / 1024 / 1024 / 1024
        End If
    
        ' ret string
        Return freespace.ToString("#00.00 Gb Free")
    
    End Function
    

    The Volume serial is broken into 2 procs. The main public one:

    ' again, call with drive letter "C"
    Public Shared Function getHardwareID(drvLtr As String) As String
        Dim volSerial As String = ""
        Dim drive As String = (drvLtr.ToLower)(0)
    
        Try
            Dim disk As ManagementObject = _
                New ManagementObject("win32_logicaldisk.deviceid=""" + drive + ":""")
    
            disk.Get()
            volSerial = disk("VolumeSerialNumber").ToString()
        Catch ex As Exception
            ' call private API version for help
            volSerial = GetVolumeSerialFromAPI(drive)
        Finally
    
        End Try
    
        Return volSerial
    
    End Function
    

    Private helper when WMI cant do the job:

    Private Shared Function GetVolumeSerialFromAPI(driveLetter As String) As String
        ' format drive letter
        Dim myDrvLtr As String = (driveLetter(0) & ":\").ToLower
    
        ' allocate space
        Dim volname As New StringBuilder(261)
        Dim fsName As New StringBuilder(261)
        Dim sernum, maxlen As UInt32
    
        ' out_Flags
        Dim flags As FileSystemFeature
    
        If GetVolumeInformation(myDrvLtr, volname, volname.Capacity,
                                sernum, maxlen, flags, 
                                fsName, fsName.Capacity) Then
            ' format to Hex like WMI
            Return sernum.ToString("X")
        Else
            Return ""
        End If
    
    End Function
    

    I suspect the underlying issue is old floppy drives at Drives.Item(0). The code did not check if they were FixedDrives nor if that drive was ready. This is part of the reason this one requires a DriveLetter. You could easily change it to get the VolumeSerial for the First Fixed disk or Windows Drive depending on what this is for.

    Four procedures is a little bit overkill, the 2 API version should always work, but since we are talking about pretty old systems, a failsafe doesn't seem like a bad idea. Plus, as I said I could only reproduce one of the errors, so a multi-pronged solution seems prudent.