To Err Is VBScript – Part 2
Doctor Scripto's Script Shop welds simple scripting examples together into more complex scripts to solve practical system administration scripting problems, often from the experiences of our readers. His contraptions aren't comprehensive or bullet-proof. But they do show how to build effective scripts from reusable code modules, handle errors and return codes, get input and output from different sources, run against multiple machines, and do other things you might want to do in your production scripts.
We hope find these columns and scripts useful – please let us know what you think of them. We'd also like to hear about other solutions to these problems that you've come up with and topics you'd like to see covered here in the future.
For an archive of previous columns, see the Doctor Scripto's Script Shop archive.
Special thanks to Jeremy Pack, Microsoft Infrastructure Architect, HP UK, for his helpful comments on a draft of this column.
On This Page
To Err Is VBScript – Part 2
In Part 1 of "To Err Is VBScript", we doubled back through the basics of errors in scripts, tiptoed around the philosophy of error handling, and strode boldly through some simple scripts that used the VBScript Err object and the On Error Resume Next statement to trap and deal with unfortunate occurrences. We also took a few winding byways into testing connections to objects, method return codes, and ping status codes. The broad prairies of error handling unfolded before us with their herds of leaping hexadecimals.
In Part 2, we'll extend the code and ideas of Part 1 into new frontiers. This time, our path is going to take us deep into the swamps of our two big COM scripting libraries: Windows Management Instrumentation (WMI) and Active Directory Service Interfaces (ADSI). For WMI, we'll have a scripting API object to carry us (and we'll find out how well it floats), but for ADSI, we're going to have to wade in and get muddy.
Nevertheless, don't be daunted by the digital jungles through which we're about to bushwhack. If we learn where to look, they can reveal to us a whole ecosystem of scripting techniques for getting more specific information about run-time errors and using this information to make scripts more robust.
Before we plunge in, though, Doctor Scripto would like to propose a meditation for our journey: If "truthiness" (the Word of the Year for 2005) is a simulation of, but not the same as, truth, what is its opposite? "Falsiness"? "Erracity"? When we work with software, we like to think we're living in the domain of 1s and 0s, True and False. But when we handle errors, we stumble into a world where there are a lot of different ways of being wrong.
WMI Error Handling
The great river of the VBScript Err object is fed by many sources, including Windows Management Instrumentation (WMI). But WMI also has its own ways of interpreting errors that can complement Err.
WMI Put a Hex on You
The WMI Scripting API exposes error information in WbemErrorEnum, which is included in the Scripting API type library. "Enum" stands for enumeration, a kind of data structure containing a list of constants. In the WMI SDK, the reference for WMI script errors is the WbemErrorEnum topic.
The SDK documentation for WbemErrorEnum lists names (all beginning with "wbemErr"), decimal and hexadecimal values, and descriptions of different error constants that WMI can return to scripts. Quite a few errors are listed, about a third of which are new for Windows XP and Windows Server 2003. For a really good time, try browsing the errors for odd names. Dr. Scripto's personal favorite is wbemErrInvalidFlavor, or 0x80041046, in case you speak hex fluently. The description: "The specified flavor was invalid." Doh! We forgot WMI doesn't serve French Vanilla.
When a script throws a run-time error, the Scripting API returns the decimal number as the value for Err.Number. It's easy to translate that into hex if needed, as we'll show in the sample code. To get at the name, though, you'd need to access the type library contained in Wbemdisp.tlb. VBScript and other WSH scripting languages can't get at this directly. The only way they can use the library is by embedding the script in a Windows Script File (WSF), an XML-based file format which allows scripts to reference type libraries.
Getting the name from WbemErrorEnum may not be that important, however, because the name is usually just a shorter, more cryptic version of the description with the spaces removed. And we can get at the description through the Description property of the Err object. Err in its bounty also gives us the Source property, which contains the name of the call on whose watch the error occurred.
So far, we know that we can get at the error number in WbemErrorEnum with the Err object. But WMI offers us a different form in which we can get more information about the last error to occur. To retrieve this information, we need to create a special type of object, SWbemLastError, from the WMI scripting API. We do that with this line:
Set WMI_Error = CreateObject("WbemScripting.SwbemLastError")
WbemScripting is the name of the COM automation server for the scripting API. The SWbemLastError object created by this call provides methods and properties to retrieve information about the error beyond what the Err object provides.
You must create the SWbemLastError object after the point in the script where an error could occur, as we do in Listing 1. Here we add the object creation to the DisplayWMIError function after the Err code inherited from the scripts in Part 1.
Only certain properties of SWbemLastError return values that are of much use in troubleshooting: Operation, ParameterInfo, or ProviderName seem the most relevant.
Strangely, though, the WMI SDK doesn't document these properties. It turns out that they are part of the collection returned by the Properties_ property, but they can also be called directly on the object. Go figure.
Listing 1 - Display WMI error
On Error Resume Next strComputer = "." 'Change to non-existent host to create binding error. strService = "Alerte" strPrinter = "FakePrinter" strProcessHandle = "3280" 'Bind to WMI on specified computer. Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") If Err = 0 Then WScript.Echo vbCrLf & "Bind success" WScript.Echo vbCrLf & "Computer: " & strComputer Else WScript.Echo "ERROR: Unable to bind to WMI provider on " & strComputer & "." WScript.Quit End If 'Try to connect to specified printer. Set objPrinter = objWMIService.Get("Win32_Printer.Name='" & strPrinter & "'") If Err = 0 Then WScript.Echo vbCrLf & "Printer success" WScript.Echo "Printer Name: " & objPrinter.Name WScript.Echo "Printer State: " & objPrinter.PrinterStatus Else DisplayWMIError("ERROR: Unable to retrieve state of " & strPrinter & _ " printer.") End If 'Try to instantiate specified process. Set objProcess = objWMIService.Get("Win32_Process.Handle='" & _ strProcessHandle & "'") If Err = 0 Then WScript.Echo vbCrLf & "Process success" WScript.Echo "Process Name: " & objProcess.Name WScript.Echo "Process ID: " & objProcess.ProcessId WScript.Echo "Process Command Line: " & objProcess.CommandLine Else DisplayWMIError("ERROR: Unable to retrieve state of process " & _ strProcessHandle & ".") End If 'Try to instantiate specified service. Set objService = objWMIService.Get("Win32_Service.Name='" & strService & "'") If Err = 0 Then WScript.Echo vbCrLf & "Service success" WScript.Echo "Service Name" & objService.Name WScript.Echo "Service State" & objService.State Else DisplayWMIError("ERROR: Unable to retrieve state of " & strService & _ " service.") End If '****************************************************************************** Sub DisplayWMIError(strMessage) strError = VbCrLf & strMessage & _ VbCrLf & "Number (dec) : " & Err.Number & _ VbCrLf & "Number (hex) : &H" & Hex(Err.Number) & _ VbCrLf & "Description : " & Err.Description & _ VbCrLf & "Source : " & Err.Source 'Instantiate SWbemLastError object. Set WMI_Error = CreateObject("WbemScripting.SWbemLastError") strError = strError & VbCrLf & "Operation : " & WMI_Error.Operation & _ VbCrLf & "ParameterInfo: " & WMI_Error.ParameterInfo & _ VbCrLf & "ProviderName : " & WMI_Error.ProviderName WScript.Echo strError Err.Clear End Sub
Here's the output if a different computer is specified but can't be found:
C:\scripts>eh-sub-displaywmierror.vbs ERROR: Unable to bind to WMI provider on erelkj.
Because the script hasn't yet connected to WMI, there's no WMI error information to be had. Incidentally, if you're wondering who would name a computer "erelkj", it was typed by a cat.
Here's typical output on the local computer:
C:\scripts>eh-sub-displaywmierror.vbs Bind success Computer: . ERROR: Unable to retrieve state of FakePrinter printer. Number (dec) : -2147217350 Number (hex) : &H8004103A Description : Invalid object path Source : SWbemServicesEx Operation : GetObject ParameterInfo: Win32_Printer.Name='FakePrinter' ProviderName : WinMgmt ERROR: Unable to retrieve state of process 3280. Number (dec) : -2147217406 Number (hex) : &H80041002 Description : Not found Source : SWbemServicesEx Operation : GetObject ParameterInfo: Win32_Process.Handle="3280" ProviderName : CIMWin32 ERROR: Unable to retrieve state of Alerte service. Number (dec) : -2147217406 Number (hex) : &H80041002 Description : Not found Source : SWbemServicesEx Operation : GetObject ParameterInfo: Win32_TerminalService.Name="Alerte" ProviderName : Win32_WIN32_TERMINALSERVICE_Prov
The SWbemLastError object gives us some additional information on the error. How useful that information may be depends on the particular error and how much you need to troubleshoot it.
In most cases, the ParameterInfo property seems to retrieve useful information: the parameters passed to the method call. But wait a minute, the value returned for the third error (above) isn't right: we got a particular instance of the Win32_Service class, not Win32_TerminalService. What was SWbemLastError thinking?
Another problem with the third error: the provider name looks a little mixed up. Win32_WIN32_TERMINALSERVICE_Prov is not mentioned anywhere in the WMI SDK, and Wbemtest.exe can't find it on any of our machines. But if you're working with a custom or unusual provider that you want to verify and that exposes its name to SWbemLastError, this property could be useful.
The Operation property seems to refer to the big picture: it returns the method call that started the whole WMI operation by getting the SWbemServices object, rather than the most recent Get method call.
If you're confused you're not alone. Apparently, some WMI providers get flustered when queried about errors. Doctor Scripto will take this up with the appropriate authorities. If his column suddenly disappears, you'll know what happened.
We're venturing deep into the error-handling backwaters here and the scripting API has sprung a few leaks. You be the judge: does the SWbemLastError object return useful additional information? Try adding error-handling code for a variety of WMI classes and scripting API objects and see what you get back. It's called the experimental process.
Handling WMI Registry Errors
There's one particular WMI class that marches to the beat of a different drummer: StdRegProv. It does things differently from nearly all the rest because it has no properties, only methods. So, for example, to get back the value of an entry that contains a string (REG_SZ), you call the GetStringValue method.
Because methods return a value for success or failure, scripts can use this return value to determine whether the method call succeeded and the script can proceed. But StdRegProv is quirky in another way. (What did you expect? The Scripting Guys and quirkiness exert a strong gravitational pull on each other.) It doesn't return an error if you provide a non-existent registry path, but it does if you don't provide any path at all. The moral of StdRegProv's story is that checking for the return value is usually the best way to go.
First let's look at that “best way,” then we'll take a glance at the quirkiness.
All the StdRegProv methods return 0 for success, so we can trap for that. The WMI SDK does not document other error codes, but for most purposes, just checking whether the method call succeeded or not is sufficient.
Listing 2 - Display WMI StdRegProv return codes
On Error Resume Next strComputer = "." 'Change to non-existent host to create binding error. Const HKLM = &H80000002 strSubKeyName = "SOFTWARE\Microsoft\NetSh" strEntryName = "hnetmonh" 'Connect to WMI and StdRegProv class. Set objReg = GetObject("winmgmts:\\" & strComputer & _ "\root\default:StdRegProv") If Err = 0 Then WScript.Echo vbCrLf & "Bind success" WScript.Echo vbCrLf & "Computer: " & strComputer Else WScript.Echo "ERROR: Unable to bind to WMI provider on " & strComputer & "." WScript.Quit End If 'Get string value from entry and check return value. intRet = objReg.GetStringValue(HKLM, strSubKeyName, strEntryName, strValue) If intRet = 0 Then WScript.Echo vbCrLf & "Registry success" WScript.Echo "Registry Path: HKLM\" & strSubKeyName & "\" & strEntryName WScript.Echo "Entry Value: " & strValue Else WScript.Echo vbCrLf & "ERROR: Unable to retrieve value of registry " & _ "entry HKLM\" & strSubKeyName & "\" & strEntryName & vbCrLf & _ "Return value: " & intRet End If
C:\scripts>eh-sub-displaywmireturn-reg.vbs Bind success Computer: . ERROR: Unable to retrieve value of registry entry HKLM\SOFTWARE\Microsoft\NetSh\ hnetmonh Return value: 1
Now for the quirkiness. Listing 3 shows a script that checks for an error by calling the same DisplayWMIError sub we used in Listing 1:
Listing 3 - Display WMI StdRegProv error
On Error Resume Next strComputer = "." 'Change to non-existent host to create binding error. Const HKLM = &H80000002 strSubKeyName = "SOFTWARE\Microsoft\NetSh" strEntryName = "hnetmonh" 'Connect to WMI and StdRegProv class. Set objReg = GetObject("winmgmts:\\" & strComputer & _ "\root\default:StdRegProv") If Err = 0 Then WScript.Echo vbCrLf & "Bind success" WScript.Echo vbCrLf & "Computer: " & strComputer Else WScript.Echo "ERROR: Unable to bind to WMI provider on " & strComputer & "." WScript.Quit End If 'Get string value from entry and check for error. intRet = objReg.GetStringValue(HKLM, strSubKeyName, strEntryName, strValue) If Err = 0 Then WScript.Echo vbCrLf & "Registry success" WScript.Echo "Registry Path: HKLM\" & strSubKeyName & "\" & strEntryName WScript.Echo "Entry Value: " & strValue Else strMessage = "ERROR: Unable to retrieve value of registry entry " & _ "HKLM\" & strSubKeyName & "\" & strEntryName & vbCrLf & _ "Return value: " & intRet DisplayWMIError(strMessage) End If '****************************************************************************** Sub DisplayWMIError(strMessage) strError = VbCrLf & strMessage & _ VbCrLf & "Number (dec) : " & Err.Number & _ VbCrLf & "Number (hex) : &H" & Hex(Err.Number) & _ VbCrLf & "Description : " & Err.Description & _ VbCrLf & "Source : " & Err.Source 'Instantiate SWbemLastError object. Set WMI_Error = CreateObject("WbemScripting.SwbemLastError") strError = strError & VbCrLf & "Operation : " & WMI_Error.Operation & _ VbCrLf & "ParameterInfo: " & WMI_Error.ParameterInfo & _ VbCrLf & "ProviderName : " & WMI_Error.ProviderName WScript.Echo strError Err.Clear End Sub
Passing these particular parameters to the GetStringValue method, this script displays a false positive. The final entry on the registry path doesn't exist, but the StdRegProv class doesn't throw an error to the VBScript Err object. Here's the output:
C:\scripts>eh-sub-displaywmierror-reg.vbs Bind success Computer: . Registry success Registry Path: HKLM\SOFTWARE\Microsoft\NetSh\hnetmonh Entry Value:
But watch what happens if we comment out the subkey and entry variables:
'strSubKeyName = "SOFTWARE\Microsoft\NetSh" 'strEntryName = "hnetmonh"
Oddly enough, the script now throws an error that SWbemNextError can handle:
C:\scripts>eh-sub-displaywmierror-reg.vbs Bind success Computer: . ERROR: Unable to retrieve value of registry entry HKLM\\ Return value: Number (dec) : -2147217400 Number (hex) : &H80041008 Description : Invalid parameter Source : SWbemObjectEx Operation : ExecMethod ParameterInfo: StdRegProv ProviderName : WinMgmt
Little mysteries like this never cease to fascinate us (but then, we're easily fascinated). For now, we've spent enough time neck deep in the Big Muddy of WMI. Let's push on to ADSI.
ADSI Error Handling
Active Directory Service Interfaces (ADSI) also provides ways for scripts to get information on the errors it returns beyond what the VBScript Err object provides. But in ADSI scripts, the error-handling landscape is somewhat different from in WMI scripts.
While ADSI does have an error-retrieval function, ADsGetLastError, which performs a role similar to that of WMI's SWbemLastError object, it is not accessible to scripting languages. You have to use C++ or another system-level programming language to call ADsGetLastError, so we won't discuss it here.
So where WMI gave us an object that can return supplementary information on its errors, ADSI scripts are on their own. One way to handle this situation is to look for and interpret particular ADSI error codes that may be returned.
ADSI does provide other sources of error information, however, all documented in the ADSI SDK topic ADSI Error Codes. There are four kinds of ADSI errors that may be returned to a script:
A fifth kind, ADSI extended error messages, can be retrieved only with the ADsGetLastError function called by a system-level language.
For the first two, generic COM and generic ADSI error codes, the ADSI SDK lists hex values, what it calls "error codes" (the constant names in string form), descriptions, and for some codes in the latter topic, a corrective action.
Five of the six generic COM errors begin with &H8000400 plus a final digit from 1 to 5. The exception is a doomsday sort of error: &H8000FFFF, aka E_UNEXPECTED, whose description is "Catastrophic failure." The VBScript Err object should pick up the descriptions and other properties for these, so we don't have to worry about them in dealing with ADSI errors.
Three of the generic ADSI codes begin with &H000050 plus two final digits. The other 18 ADSI codes begin with &H800050 plus two final digits.
The "Win32 error codes for ADSI" topic offers two tables, Win32 error codes and Win32 error codes for ADSI 2.0, both of which show the following data:
Hex value (prefaced by "0x", the C way of indicating hex, and ending in "L"; for example: 0x80070008L)
LDAP message (such as LDAP_NO_MEMORY)
Win32 message (such as ERROR_NOT_ENOUGH_MEMORY)
Description (such as "System is out of memory.")
If you can't find the value in the two tables, the SDK refers you to a header file, winerror.h, and describes a procedure to convert the returned code to a code that you can look up in winerror.h. We found that this method did not always work. Some of the codes not listed in the two tables are listed in winerror.h as a hex code, not a decimal code converted from the last four digits of the hex code, as the SDK describes. So just to be sure, search with both the converted decimal code (described below) and the hex code.
If the code is not in the two SDK tables or winerror.h, the procedure refers you to another header file, Lmerr.h. We will leave digging into Lmerr.h to seriously obsessed error aficionados. We've got lots of grist for the error mill already.
For LDAP error codes, which are generated by an LDAP server, the SDK describes a similar conversion method to a code that you can then look up in Winldap.h, another header file. If Lmerr.h was just an hors d'oeuvre for you, Winldap.h will be … well, an even tastier hors d'oeuvre. The Platform SDK does not cover either of these files, so you're out on the frontiers of error handling without a map. But don't despair: there are a wealth of error codes in the SDK and winerror.h, so we'll confine ourselves to them for this column.
Converting Between Decimal and Hexadecimal Codes
Dealing with both ADSI and WMI errors can involve converting between decimal and hexadecimal numbers, so we're going to talk briefly about how to do this. It's not hard with VBScript, which provides functions that perform conversions in either direction.
Because VBScript returns a decimal value in Err.Number, we have to begin by converting the decimal to hex. Here's a tiny script that does that task: just substitute the decimal number to convert for the value assigned to intDec in the first line. The script uses the built-in VBScript function Hex to convert the value.
Listing 4 – Convert decimal number to hexidecimal
intDec = -2147016672 WScript.Echo intDec strHex = "&H" & Hex(intDec) WScript.Echo strHex
The hex equivalent of this particular integer is:
C:\scripts>dec-to-hex.vbs -2147016672 &H80072020
The WMI error-handling script above does this conversion of Err. Number with one line:
"Number (hex) : &H" & Hex(Err.Number)
If you need to convert back the other way, from hexadecimal to decimal, you can use a script like the following, which uses the built-in VBScript function CLng to convert a hex string to a decimal value. The hex value must begin with "&H", which VBScript interprets as the beginning of a hexadecimal number.
Listing 5 – Convert hexidecimal number to decimal
strHex = "&H80072020" WScript.Echo strHex intDec = CLng(strHex) WScript.Echo intDec
The decimal equivalent of this particular hex value is:
C:\scripts>eh-hex-to-dec.vbs &H80072020 -2147016672
We can use these scripts in the conversion method describe by the SDK for some error codes.
Finding Descriptions of ADSI Error Codes
As we mentioned above, the "Win32 error codes for ADSI" topic of the ADSI SDK describes a conversion method for generic COM and generic ADSI error codes that works often, but not always.
To test the conversion method described by the ADSI SDK, let's take a different error code and follow the steps in the SDK. You can return this code by changing the "LDAP://" in the container ADsPath to "DAP://", so the script is trying to use a non-existent ADSI provider. The script returns:
Here's the method, which we've adapted to scripting from the C++ oriented description in the SDK.
Convert the code returned to the script (which the SDK calls by its C++ name, HRESULT) from decimal to hex.
Drop the left four hex digits.
Convert the remaining (right) four digits back to decimal.
Look up the decimal remainder in winerror.h (a text file available in the Platform SDK containing the Win32 error codes). The SDK divides it into two parts, so you have to search both. Ignore the "0x" at the beginning and the "L" at the end of the code, which are both C coding.
When we search part 1 or Winerror.h for 484, we get one hit, but it's 8484 so it's not what we're looking for. Likewise in part 2, we get a hit for 484 that's part of a hex code, not a decimal one, and doesn't seem to apply here in any case.
However, if we search for the converted hex code (minus the "&H" because in the header it will be prefaced with "0x"), we find:
// MessageId: MK_E_SYNTAX // // MessageText: // // Invalid syntax // #define MK_E_SYNTAX _HRESULT_TYPEDEF_(0x800401E4L)
OK, "Invalid syntax" may not be the first thing that comes to mind for an error caused by trying to connect to GetObject with an ADsPath beginning with "DAP://", but it will do.
In fairness to the method detailed in the SDK, it did work for other codes, including -2147023541 (&H8007054B), which produced a very thoughtful message: "The specified domain either does not exist or could not be contacted."
Now that we've seen how to hunt down the meaning of ADSI errors, let's try to apply this to an error-handling script.
Interpreting ADSI Error Codes in a Script
With an ADSI script, we can't create a special error object as we can with WMI, and we can't call an ADSI function as we could if we were using C++. But we can still track down some likely codes with the help of the ADSI SDK and Winerror.h and then use these codes to identify likely errors after an operation.
For the following script, we've chosen just a few error codes that seemed as though they might occur in attempting to bind to an ADSI container. For other situations, such as getting a Computer object, we'd probably choose a different or expanded set. If you do a lot of ADSI scripting, you might want to put the error codes together into an omnibus ADSI error function that you can use in all your ADSI scripts, but we're not going to do that here. After all, we have to leave something to your imagination and creativity, right?
This script does a few things differently from the WMI script. First, the If … Then … Else clause handles the error contingency first:
If Err <> 0 Then
rather than success, as in the WMI script:
If Err = 0 Then
This order is mainly a matter of preference: do you want to hear the good news or the bad news first? Are you an optimist or a pessimist? In this script, where not much happens in either case, it doesn't make much difference. But if, for example, you wanted to make some changes on the computer in the container after you connect to it, you might want to put that first and leave the error-handling for last. It's more a question of human readability than coding efficiency.
Another difference is that the error-handling is contained here in a function, rather than the subroutine we used for WMI. The error message string is assembled in the function and then returned by the function to the calling statement. Then it's up to the main body of the script to display the message (and potentially take action based on it).
We call the function in-line as part of the string that WScript.Echo outputs:
WScript.Echo strMessage1 & vbCrLf & DisplayADSIError
After the DisplayADSIError function returns, the function call acts as a string variable containing the return value. So we can concatenate it with the first part of the error message.
Again, it's partly a matter of preference how you divide the labor of getting the error information, constructing the error message, and outputting that message. If the script does a lot of other work in subroutines, you may want to use the sub approach. If more happens in the main body of the script, say for example the script branches and does different things depending on the error involved, you might want to use a function that returns information to the calling statement. Your coding decisions here should depend on the particular needs of the script in question.
In the DisplayADSIError function, we've chosen a few ADSI error codes that seemed as though they might apply to the potential error being trapped, a failure to bind to the specified container. Feel free to expand them based on your happy hours spent with ADSI errors.
Listing 6 - Display ADSI error
On Error Resume Next strProvider = "LDAP://" strContainer = "cn=computers,cn=fabrikam,cn=com" 'Bind to container with LDAP and check for error. Set colContainer = GetObject(strProvider & strContainer) If Err <> 0 Then strMessage1 = "Container " & strProvider & strContainer & " not found." WScript.Echo strMessage1 & vbCrLf & DisplayADSIError Else colContainer.Filter = Array("Computer") For Each objComputer in colContainer WScript.Echo objComputer.CN Next End If '****************************************************************************** Function DisplayADSIError 'Display information from VBScript Err object and ADSI meaning. strADSIError = "" Select Case Err.Number Case "-2147221020" strADSIError = "Invalid syntax. ADSI provider not found." '&H800401E4 Case "-2147463168" strADSIError = "An invalid ADSI pathname was passed." '&H80005000 Case "-2147463167" strADSIError = "An unknown ADSI domain object " & _ "was requested." '&H80005001 Case "-2147023541" strADSIError = "The specified domain either " & _ "does not exist or could not be contacted." '&H8007054B Case "-2147016672" strADSIError = "An operations error occurred. " & _ "ADsPath not found" '&H80072020 Case "-2147024843" strADSIError = "The network path was not found." '&H80070035 Case "-2147023570" strADSIError = "Supplied credential is invalid." '&H8007052E Case "-2147024891" strADSIError = "User has insufficient access rights." '&H80070005 Case Else strADSIError = "Error description not found" End Select strError = " Number (dec): " & Err.Number & vbCrLf & _ " Number (hex): &H" & Hex(Err.Number) & vbCrLf & _ " Description: " & Err.Description & vbCrLf & _ " Source: " & Err.Source & vbCrLf & _ " ADSI Error Message: " & strADSIError DisplayADSIError = strError Err.Clear End Function
Note that the VBScript Err object in the error-handling function doesn't get a description or source back from ADSI. We depend on the interpretations of the ADSI error numbers that we've looked up in the ADSI SDK and Winerror.h and added to the script.
Handling File Access Problems
We've covered a lot of different kinds of error-handling so far in these two columns, but there's one important kind we've left out: potential problems when we open a file with the FileSystemObject, part of Script Runtime. Any time we get information from or write to a .txt or .csv file, we're probably using FileSystemObject, or FSO, as we affectionately call it. Hey, it's a scripting celebrity, so it deserves a TLA just like FDR or JFK.
Here, we don't need to make use of the services of the VBScript Err object because we can use some handy functionality that's built into FSO. Before trying to read the contents of a file, it's a good idea to check a couple things:
Does the file exist?
If so, is it empty?
In either case, we probably don't want to go ahead. If the file doesn't exist, but the script assumes it does, we're likely to get an error the next time we try to do something with it. If the file is empty rather than containing a list of computer names, the script is not going to be able to perform whatever tasks it was supposed to on those computers.
For code that handles these two potential file access errors, we'll return to a familiar script: the one we examined in the November 2005 Script Shop column on procedures, "Bring in da Subs, Bring in da Funcs." (We've also used a similar function that reads text files in previous columns.)
The text file can contain anything. For our purposes we’re going to use a simple one that contains the names of computers, one to a line. We'll call it List.txt.
client1 client2 client3 client4
The following script checks whether the file name in strInputFile exists by calling the FileExists method of FSO and passing it the filename as the only parameter. The script shows a simple filename, which would have to be in the same directory as the script, but you can also use a path, such as c:\scripts\list.txt. If the file does not exist, the script quits with an appropriate message, as you can't do anything to a non-existent list of computers.
If the file does exist, however, the script opens it for reading with the OpenTextFile method of FSO. OpenTextFile returns a stream of all the characters in the file, and the FSO TextStream object provides methods and properties to work with it. One of the properties is named AtEndOfStream. If the script is already at the end of the text stream when it's only just been opened, the file must be empty. In this case, the script also quits and informs the user.
Once the file has been opened and is found to contain at least some text, the function returns the lines of the file as elements in an array. This is handy if the script is going to iterate through a list of computer names contained in the file. For other purposes, a similar function could just return the whole text stream as a string by using the ReadAll method of the TextStream object without splitting it up on the line breaks.
The FileSystemObject in its wisdom provides functionality that lets us anticipate possible errors up front and handle them before they throw a VBScript run-time error.
Listing 7 – Handle file access errors
strInputFile = "list.txt" WScript.Echo "Text file contents:" For Each strItem In ReadTextFile(strInputFile) WScript.Echo strItem Next '****************************************************************************** Function ReadTextFile(strInputFile) 'Read contents of text file and return array with one element for each line. On Error Resume Next Const FOR_READING = 1 Set objFSO = CreateObject("Scripting.FileSystemObject") If Not objFSO.FileExists(strInputFile) Then WScript.Echo "Input text file " & strInputFile & " not found." WScript.Quit End If Set objTextStream = objFSO.OpenTextFile(strInputFile, FOR_READING) If objTextStream.AtEndOfStream Then WScript.Echo "Input text file " & strInputFile & " is empty." WScript.Quit End If arrLines = Split(objTextStream.ReadAll, vbCrLf) objTextStream.Close ReadTextFile = arrLines End Function
If the file is found and contains text, the output looks like this:
C:\scripts>eh-func-read-textfile.vbs Text file contents: client1 client2 client3 client4
If the file is not found, the output looks like this:
C:\scripts>eh-func-read-textfile.vbs Input text file list.txt not found.
If the file is empty, the output looks like this:
C:\scripts>eh-func-read-textfile.vbs Input text file list.txt is empty.
It's been a long, winding road in search of that elusive perfect error-handling script. We've made some wrong turns and ended up in some obscure bayous of WMI and ADSI. But as Doctor Scripto likes to remind us, whatever doesn't kill you builds character. So we hope you've come out of these error-prone columns a tougher, more mature scripter who knows that there are inevitably bumps in the road and has some good code examples at hand to serve as shock absorbers.