Thursday, April 17, 2008

System Disk And Drive Letter In Perl

There's no easy way to discover the system disk and drive letter on Windows, so I came up with this tedious work-around. Basically, you use Windows Scripting Host (WSH) to write a file that Perl can then read. Someone may wonder why I'm not using Win32:: packages - well, they don't implement the whole CIMv2 spec, so you'll begin running into errors such as Win32::OLE(0.1707) error 0x80020009: "Exception occurred", smartly described as a "Generic Error". It's a nightmare to dig through MSDN to figure out what the heck an ambiguous error like this means.

So the simple workaround: write a VBScript file that contains this code:

Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
Set colDiskDrives = objWMIService.ExecQuery("SELECT * FROM Win32_DiskDrive")
For Each objDrive In colDiskDrives
strDeviceID = Replace(objDrive.DeviceID, "\", "\\")
Set colPartitions = objWMIService.ExecQuery _
("ASSOCIATORS OF {Win32_DiskDrive.DeviceID=""" & _
strDeviceID & """} WHERE AssocClass = " & _
"Win32_DiskDriveToDiskPartition")
For Each objPartition In colPartitions
If objPartition.BootPartition = True Then
bootinfo = objPartition.DiskIndex
Set colLogicalDisks = objWMIService.ExecQuery _
("ASSOCIATORS OF {Win32_DiskPartition.DeviceID=""" & _
objPartition.DeviceID & """} WHERE AssocClass = " & _
"Win32_LogicalDiskToPartition")
For Each objLogicalDisk In colLogicalDisks
bootinfo = bootinfo & " " & objLogicalDisk.DeviceID
Next
strOutputFile = "./theboot.txt"
Set objFileSystem = CreateObject("Scripting.fileSystemObject")
Set objOutputFile = objFileSystem.CreateTextFile(strOutputFile, TRUE)
objOutputFile.WriteLine(bootinfo)
objOutputFile.Close
Set objFileSystem = Nothing
End If
Next
Next

There are zillion ways to write this code to the filesystem in Perl; I leave that up to you. Execute the file (with a .vbs extension) on the system shell (DOS), and have Perl read the file theboot.txt (or whatever you call your file) - parse it for the physical disk number (given by Win32_DiskDrive.DeviceID) and the drive letter (given by Win32_LogicalDisk.DeviceID). Associating the two WMI classes is weird, as the code above shows, but it works. In the code, the file is written in the format [disk_number] [drive_letter].

Note that I use for-loops in this workaround, so if you have multiple boot disks, only one will be discovered.
Also, don't rely on DISKPART for this job, especially if you are scripting things. It works well interactively, but I've found it unreliable on systems with large drive counts - in such cases, the disk number won't always be the same.

This workaround essentially saved my day: reliance on diskpart had cost me a couple of lost OSes as it sometimes reported that there was no system partition or could not find drives whatsoever, especially if you had a lot of drives. The WMI/CIM solution seems bullet-proof - for now.