Executing programs on a remote machine is a common administrative task, and various tools are available to perform the job. Sysinternals' PsExec tool is a powerful example. (For more information, see "PsExec," Windows IT Pro, July 2004, InstantDoc ID 42919.) I also created a script, JTRun.vbs, that uses Microsoft's jt.exe and the Task Scheduler service. (For more information, see "Command-Line Task Scheduler," March 2005, InstantDoc ID 45148.)
With some restrictions, Windows Management Instrumentation (WMI) can also run programs locally or on a remote computer. Table 1 shows some of the advantages and disadvantages of each approach. In this article, I'll present a script that uses WMI to run programs either locally or remotely. It can also optionally wait for the program to end and display its run time.
The RunProgram.vbs Command
RunProgram.vbs uses WMI's Win32_ProcessStartup and Win32_Process objects to run a program either locally or remotely. You can download the script by going to http://www.windowsitpro.com/windowsscripting, entering 48218 in the InstantDoc ID text box, and clicking the 48218.zip hotlink. RunProgram.vbs requires Windows 2000 with Windows Script Host (WSH) 5.6 or Windows XP or later. (WSH 5.6 is preinstalled on XP and later.) The syntax for the launch command is
RunProgram.vbs "command" \[/startin:path\] \[/window:n\] \[/computer:computer \[/username:username\] \[/password:password\]\] \[/wait \[/elapsed\]\]
The command argument specifies the command line to execute. Make sure to enclose it in double quotes (""). The command name at the beginning of the command line must be an executable, so if you need to run a cmd.exe command (e.g., Dir, Copy) or a cmd.exe shell script, start the command with cmd /c. If you need to run a WSH script, start the command with cscript or wscript.
The /startin:path option lets you specify the directory in which the program will start. If you don't specify this option, the program starts in the same directory as the calling process (typically the %SystemRoot%\system32 directory). Make sure to enclose the path in double quotes if it contains spaces.
The /window:n option configures the initial window state for the program. This number is the same as the intWindowStyle parameter of the WshShell object's Run method. For example, /window:7 starts the program in a minimized window. The default value is /window:1 (i.e., display normally). See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/script56/html/wsmthrun.asp for a list of possible values. Note: For security reasons, programs executed on a remote system always run in a hidden window on Windows 2000 Service Pack 3 (SP3) and later.
The /computer:computer option specifies a remote computer on which the command should be executed. If you omit the /computer option, the command will execute on the local computer. The /user:username and /password:password options specify alternative credentials for connecting to a remote computer. If you omit these options, RunProgram.vbs uses your current logon credentials. Both of these options require the /computer option because WMI won't allow you to specify credentials for a local connection. Exercise caution when using these options because you must type the username and password in clear text at the command line.
The /wait option causes RunProgram.vbs to wait for the program to end, and the /elapsed option displays the program's running time. Note that the /elapsed option requires the /wait option.
If you're running a program on a remote computer (or on the local computer in a hidden window), it's important that the program be able to proceed without user intervention because the program won't be visible. Also, if you run a program on a remote computer, the program won't have network access (for security reasons).
When you run a program with RunProgram.vbs, it displays some initial output: the computer on which the command was executed, the command line, the command's starting directory (if any), and the program's process ID (a unique number that represents the process). Figure 1 shows the general format of the script's output. If you use both /wait and /elapsed, the script will display the start, end, and elapsed times after the program ends.
The Main Subroutine
RunProgram.vbs defines two constants at the top of the script: SCRIPT_NAME, which is used by the ShowUsage subroutine, and PROCESS_CHECK_INTERVAL, which is used when the script is waiting for the program to end. The script then executes the Main subroutine, which Listing 1 shows.
First, the Main subroutine declares the variables it will use, then it sets the Args variable to contain a reference to the WScript.Arguments object. If the Unnamed collection contains no members (i.e., its Count property is 0) or if the /? option is present in the command line, the subroutine calls the ShowUsage subroutine, which ends the script after displaying a short usage message.
Next, the Main subroutine reads the first unnamed command-line argument—that is, the first argument that doesn't start with the slash (/) character—and uses the VBScript Unescape function to replace %xx character sequences with their ASCII equivalents, as the code at callout A in Listing 1 shows. The Unescape function provides a mechanism for embedding double quotes inside a quoted string. See the sidebar "Embedding Double Quotes in the Command" for more detail.
The Main subroutine then reads the starting directory (/startin), window state (/window), computer name (/computer), username (/username), and password (/password) into the respective variables. To do this, the subroutine uses the WScript.Arguments.Named collection, which contains the collection of command-line arguments that start with the / character. If a named argument doesn't exist in the command line, referencing its contents returns the special value Empty. In VBScript, comparing Empty with a blank string ("") returns True, so comparing the command-line option's argument with an empty string is an easy way to tell whether the option has an argument (e.g., /startin:C:\ has an argument of C:\; /startin has no argument).
Next, the Main subroutine determines whether the /wait and /elapsed named options are present in the command line. If they are, the corresponding variables (Wait and Elapsed) are set to True.
After parsing the command line, the script is ready to connect to the computer through WMI and start the program. The code at callout B shows how the Main subroutine creates a new instance of the WMIExec class to make the connection and execute the program. The subroutine uses VBScript's New keyword rather than the CreateObject function because the WMIExec class definition exists in the same script file.
The Main subroutine uses the WMIExec object's ConnectServer method (named after the WMI method it uses) to attempt to connect to the computer named in the command line (or the local computer if the /computer option wasn't specified). If this method returns a nonzero exit code, the Main subroutine displays an error message and ends the script with the method's exit code. Typically, these error codes begin with 0x8007 (signaling an automation error) and end with four hexadecimal digits that refer to the Win32 error that occurred. To interpret the error, convert the last four digits to decimal and use the decimal number with the Net Helpmsg command at the command prompt. For example, the error 0x800706BA translates to 1722, and the command
Net Helpmsg 1722
displays the message The RPC server is unavailable (i.e., the computer isn't reachable over the network).
If the ConnectServer method succeeds, the Main subroutine continues. If the /elapsed option is present in the command line (i.e., the Elapsed variable contains True), the subroutine uses VBScript's Now function to copy the current date and time into the StartTime variable.
Next, the Main subroutine calls the WMIExec object's RunProgram method to run the command line. The method will return a zero value if it succeeds. If it fails, it will return one of the values listed in the "Create Method of the Win32_Process Class" documentation at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/create_method_in_class_win32_process.asp, and the script will end. If the RunProgram method succeeds, the script outputs the string returned from the WMIExec object's Status method.
If the /wait option is present in the command line, the Main subroutine's last task is to pause the script while the WMIExec object's ProcessExists function returns True. The Do...Loop statement suspends the script by using the WScript object's Sleep method in each iteration of the loop for the number of milliseconds specified in the PROCESS_CHECK_INTERVAL constant, which by default is 500 (half a second). You can increase this interval if it's too short and causes the script to consume too many CPU cycles.
If the /elapsed option exists in the command line, the Main subroutine uses VBScript's Now function a second time to store the current date and time into the EndTime variable. It then creates an output string called TimeInfo that contains the start time, end time, and elapsed time. To create the string containing the elapsed time, the subroutine uses VBScript's DateDiff function to determine the difference in seconds between the start and end times and passes this as a parameter to the GetElapsed function, which I'll describe in a moment.
If the script is being executed by WScript, the final script output will contain both the process information retrieved from the WMIExec's Status method and the time information. If the script is being executed by CScript, the final output will contain just the time information. There's no need for the script to output the process information because it's already there in the command window.
The WMIExec Class
The script's WMI functionality is implemented in a separate class because the class uses a set of five variables that need to be shared between its methods. Writing the script without using a separate class would mean that the script would need to either use global variables or pass a long list of parameters to supporting functions, which would make the script more cumbersome to read and maintain. I used a c_ prefix for the five variables to avoid confusion and naming conflicts with the other variables used inside the class's methods. I previously described how the Main subroutine calls the WMIExec class's four methods, but I want to provide a little more information about how the methods work:
The ConnectServer method. This method creates a WMI SWbemLocator object by using VBScript's CreateObject function. It configures the object's Security_ property (which happens to be a SWbemSecurity object) by setting its ImpersonationLevel property to 3 (i.e., wbemImpersonationLevelImpersonate). Next, the method uses On Error Resume Next to disable VBScript's default error handler. Then it uses the SWbemLocator object's ConnectServer method. The WMIExec ConnectServer method's return value is equal to the Err object's Number property, which will be zero if the connection succeeded. If the connection succeeded, the c_SWbemServices variable will contain a SWbemServices object. The WMIExec ConnectServer method uses a SWbemLocator object rather than a WMI moniker string (e.g., winmgmts: ...) because there's no way to set connection credentials inside a moniker string.
The RunProgram method. The RunProgram method uses the SWbemServices object created by the WMIExec ConnectServer method to create a Win32_ProcessStartup object and configures the Win32_ProcessStartup object's ShowWindow property. Then, it uses the SWbemServices object a second time to create a Win32_Process object, uses its Create method to start the program, and exits with the Create method's return value.
The ProcessExists method. This method uses the SWbemServices object created by the WMIExec ConnectServer method as well as the process ID created by the RunProgram method to determine whether the process exists. It uses a WMI query to check for the process ID, reads the collection's Count property, and returns True if the number of returned instances in the collection is greater than zero.
The Status method. The Status method uses the computer name, command line, and starting directory class variables to return a string in the format that Figure 1 shows.
The GetElapsed Function
RunProgram.vbs's GetElapsed function uses VBScript's integer division operator (\) to divide the number of seconds by 3600 (the number of seconds in an hour) to determine the number of hours. If the result is greater than zero, the function subtracts out the hours. The function then divides the remaining number of seconds by 60 to find the number of minutes and subtracts out the minutes. The result is a string in the format n hour(s), n minute(s), n second(s).
WMI is a powerful tool in the system administrator's scripting arsenal, and RunProgram.vbs demonstrates yet another of its useful capabilities. Add this tool to your toolkit and you'll have another way of starting programs on remote computers.