Have your scripts complete in a fraction of the time that they would typically take
|On the Internet, you can likely find VBScript code that suits your needs. The real trick is figuring out how to put all the disparate parts together into a cohesive whole while circumventing some of VBScript's limitations. One of those limitations is that VBScript can do only one thing at a time. MultiThreader.vbs works around that limitation by simulating multi-threaded behavior|
Once you learn the basics of VBScript, you can accomplish almost anything you want with ease. Armed with a browser and an Internet connection, you can find someone who has written code that collects the information you need or that sets the setting that you need to configure. The real trick is figuring out how to put all the disparate parts together into a cohesive whole while circumventing some of VBScript's limitations.
For example, you can find scores of Web sites that provide code that determines how much free space is on a computer. DiskSpace.vbs in Listing 1 shows a script that does just that. Or you can find code that lists all of the computers in a domain running a Windows server OS, like FindServers.vbs in Listing 2 does.
It doesn’t even take much effort to combine these code snippets and create a script that determines the amount of free disk space on all servers in a domain, as DiskSpaceAllServers.vbs in Listing 3 shows. But what do you do when you want to determine the amount of free space every 60 minutes, but the script takes 90 minutes to run against all the computers in your domain? That can take a little bit of creativity because VBScript can only do one thing at a time.
One solution is to break your list of servers into smaller lists and execute the script against each smaller list. However, this solution requires you to manually maintain lists that you'll need to modify every time you add or remove computers from your environment. What if a script could break down your large list of servers into manageable pieces for you? Or better yet, what if you had a separate script that simulates multi-threading for any script you happen to write?
MultiThreader.vbs, which Listing 4 shows, is such a script. It accomplishes three tasks:
- Queries an external source for a list of computers.
- Breaks that list of computers into smaller lists.
- Executes a second "worker" script (i.e., a script that performs a specific job), using the computers in one of the smaller lists as arguments.
How MultiThreader.vbs Works
As always, we start our script with constants and variable declarations, as callout A in Listing 4 shows. We'll want to use this multi-threading script without having to modify it for every worker script we write, so we need to be able to pass in the worker script as an argument. The code at callout B accomplishes this.
Next, we need to obtain the list of servers that we want to execute the worker script against (callout C). In this case, we're using the same method as that used in FindServers.vbs in Listing 2: Query Active Directory (AD) for all computers in the domain that are running a Windows server OS and put those names into a recordset. Alternatively, we could get the computer names from other sources, such as a database or text file. The implementation might be a little different, but the general idea is the same.
Finally, we need to figure out a way to break that computer list into smaller pieces and execute the worker script for each smaller list. As callout D shows, we use the intCount variable to specify how many servers to group in each execution of the worker script. The value of intCount should be tested for your environment. I have successfully used groups with as many as 100 computers, but your results may vary based on the path of the scripts and the names of your computers. Keep in mind that the length of the command we're about to build is finite.
In Windows Script Host (WSH), the Run method of the WshShell object (objShell) lets us execute commands in an external command shell window (i.e., cmd.exe), so that's what we'll use to execute the command that launches the worker script. But first we need to build that command.
The command (strCommand) starts with the worker script's name. We then use VBScript's Do…Loop statement to iterate through the server names returned from the AD query, appending each server name as an argument to that command. The y variable keeps track of how many servers have been added. Once the count in the y variable equals the number of servers specified in the intCount variable, we execute the worker script using the WshShell object's Run method. There are two parameters in use with objShell that need some explanation: bWaitForCompletion and Hide_Window.
The bWaitForCompletion parameter defines whether or not the shell should wait for the external command to complete before continuing MultiThreader.vbs. Since the purpose of MultiThreader.vbs is to kick off multiple instances of a worker script, it’s clear that we don’t want to wait. So, the correct value for this functionality is False, which we defined earlier in the script. If you use the WshShell object in other scripts, you might want the script to wait for the command to finish before moving on. In those scenarios, set the bWaitForCompletion constant to True.
The Hide_Window parameter tells WSH whether or not to show the command shell window during execution. We have chosen to hide it here by setting the constant to the value of 0, but you can choose to show the window by changing this value to 1. There are also other options available. They're described in MSDN's Run Method (Windows Script Host) Web page at http://msdn2.microsoft.com/en-us/library/d5fk67ky.aspx.
After the worker script is launched, we set our count (y) back to 1 and eliminate the arguments that we have appended to the script by setting the command equal to just the name of the worker script. We then start the loop again, building a new command. When the Run method executes this newest command, it opens a new command shell window. This loop continues until it reaches the end of the recordset. After we exit the loop, we execute the worker script one final time if there are any arguments appended to the command.
How to Modify the Worker Script
We now have a script that can execute another script in parallel processes, simulating multi-threaded behavior. The only step left is to modify our worker script to handle a list of servers as an argument. We also need to write the output to a file so that the information is usable. After all, having the information reside in a dozen command shell windows, especially when those windows are hidden, isn’t very useful.
Listing 5 shows the modifications made to DiskSpace.vbs. Callout A in Listing 5 highlights one of the most important changes. This code retrieves the computer names from the command line. Without this code, the worker script won’t know which computers to process.
In callout B in Listing 5, notice that we're collecting all the data in an array before writing it to the output file. One shortcoming of launching a bunch of scripts that perform the same job is that they're all going to want to write to that file. The first script that does so will lock the text file until it’s done writing its data, and you end up losing all the other data. To avoid this problem, we save all the data in memory, then write it to the file all at once.
It's possible that multiple scripts will still try to write at the same time, but the chances are drastically reduced. I'm not going to provide specific examples on how to avoid this condition, but I'll offer two potential workarounds. One approach is to have each worker script write to a separate output file, then manually aggregate the data after all the scripts have finished. A second approach is to relaunch MultiThreader.vbs when a writing error occurs, making sure there is a small delay between launches. If you take the second approach, make sure that you limit the amount of retries. Otherwise, you might find yourself in an infinite loop.
How to Run MultiThreader.vbs
To run MultiThreader.vbs with DiskSpace_Threaded.vbs as the worker script, open a command shell window and execute the command
cscript.exe MultiThreader.vbs "c:\scripts\DiskSpace_Threaded.vbs"
(Although this command wraps here, it should all be on one line in the command shell window.) DiskSpace_Threaded.vbs will now complete in a fraction of the time it would have taken one script to perform that job on all the servers.
One drawback to using multi-threaded scripts is that there is no clear way to tell when the worker scripts have all finished running. MultiThreader.vbs exits as soon as all the worker scripts have been launched. However, you can use Task Manager to see whether cscript.exe is still running. This drawback is a small price to pay when you can have your scripts run in a fraction of the time that they would typically take.