Out of Sync: The Return of Asynchronous Event Monitoring

Published: September 20, 2005 | Updated : September 20, 2005

By The Microsoft Scripting Guys

Doctor Scripto at work

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.

On This Page

Out of Sync: The Return of Asynchronous Event Monitoring
The Anatomy of an Asynchronous Event-Handling Script
Filtering Processes in a WQL Query with a WHERE Clause
Using Win32_ProcessStartTrace to Handle Process Events
Displaying an Interpretation of the Return Value
Using objAsyncContext parameter for SINK_OnObjectReady
Taking a Snapshot of Running Processes and Alerting on New Ones
Other Kinds of Asynchronous Methods
Resources

Out of Sync: The Return of Asynchronous Event Monitoring

Just when you thought it was safe to open the server room door again…

It's back and it's gnarlier than ever: asynchronous event monitoring refuses to die. Doctor Scripto keeps going into his script crypt and tinkering around with it. That's because, as lengthy as they were, the last couple of columns left a few stones unturned. Besides, we've received some interesting feedback from readers that we wanted to share with you. And, dang it, we think asynchronous event monitoring can be pretty useful for monitoring multiple machines.

So this column is going to be interactive, a buzzword that used to make the stock price of anything appended to it rise at least 20 percent in the late 90s. OK, maybe it doesn't work quite as well for columns these days, but we thank you all the same for sending us your comments. As a result, this async-fest is also going to be a kind of "string of pearls" – journalistic parlance for a story that strings together interesting tidbits. The thread we're stringing our pearls onto will be following up on asynchronous event handling and clarifying some details in our asynchronous scripts. Whether they're pearls of wisdom or plastic we'll let you decide.

The Anatomy of an Asynchronous Event-Handling Script

We talked about asynchronous event-handling methods at some length in the previous column. But because they are a bit more complex than the synchronous/semisynchronous variety, let's go over a few points about how they work once more—with feeling—and see if we can confuse you more completely this time.

In an asynchronous method call, two routines are created that work in parallel: the asynchronous routine and the sink. The two routines are independent of each other in time, which is why they are called "asynchronous." (According to Webster’s, "synchronous" means "happening, existing or arising at precisely the same time," and the "a" negates the original meaning).

The asynchronous routine goes out to perform some task, in our example querying for process events, creating a separate thread that in effect behaves like an independent agent dispatched by the script. Once the asynchronous call has unleashed this thread, it returns and allows the script to go on to the next line. When the query finds what it is looking for, in this case when an event matching the query is triggered, the independent thread reports back to the waiting sink. This report is called a callback. The sink runs on the calling machine, even if the asynchronous routine is operating against a remote machine, so the asynchronous query thread is in effect calling into headquarters to give its report. As long as the script continues to run, the thread can continue to report new events that match its query back to the sink.

Easy, huh? Don't worry, you don't really need an advanced degree in particle physics to use asynchronous methods – an understanding of string theory may help, but just the VBScript kind of strings. As the Scripting Guys love to repeat until you roll your eyes, you can simply use templates for the basic patterns involved and adapt them to the purposes of your scripts. With that goal, we're going to look at some template code for monitoring an intrinsic event, one provided directly by WMI.

Listing 1: Asynchronously monitor specific processes

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
objWMIService.ExecNotificationQueryAsync _
 SINK, _
 "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Process'"
Do
   WScript.Sleep 10000
Loop
Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
  Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
End Sub

There are a few unfamiliar things going on here for those used to other kinds of WMI scripting, so let's drill down into the details of the code line by line.

Set SINK = _
 WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

This line creates the event sink that the asynchronous method will call back to. Set, as usual, assigns an object reference to the variable SINK. You can name the variable anything you want – the name need not match the second parameter of CreateObject at the end of this line (although this is a useful convention). But once you've named the variable, you've got to use this name as the first parameter passed to ExecNotificationQueryAsync a couple of lines down. This tells the asynchronous query the name of the event sink to which to make its callback. (You can have more than one sink, as we saw in the last column.)

WScript (the Windows Script Host parent object) has a CreateObject method that is slightly different from VBScript's CreateObject. To create an event sink, you use the WScript version.

As parameters to the WScript.CreateObject method, you specify first the programmatic ID (progID) of the COM object (in this case the SWbemSink object of the WMI Scripting API), and second a prefix representing the asynchronous sink to use with it for event methods. This second parameter is just a string that you can name as you please, but the name of the Sub at the end of this script that handles the OnObjectReady method of the sink must begin with the same name you use here. Again, "SINK_" is the default we use.

Set objWMIService = _
 GetObject("winmgmts:\\.\root\cimv2")

This line, which connects to the WMI service, is the same as you would use in any WMI script. We're keeping it simple here and just connecting to the \root\cimv2 namespace, normally the default, on the local box (represented by ".").

objWMIService.ExecNotificationQueryAsync _
 SINK, _
 "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Process'"

This line contains the asynchronous event call and two parameters.

We just got a reference to the WMI service, which is represented by an SWbemServices object, in the last line. Now we call its ExecNotificationQueryAsync method and pass it the two parameters:

  • the name of the sink object we created in the first line, which the query will call back with events

  • the WQL query for the method to execute

The query tells the method to get all the properties ( * ) of the __InstanceCreationEvent class (that's two underscores), one of WMI's intrinsic (built-in) event classes.

The WITHIN clause specifies one second as the polling interval – the time the method waits between checks for events. As we mentioned in the last column, using a short polling interval such as one second could affect performance if you are querying for a large amount of data from many machines. In such cases, it may be worth experimenting with increasing the polling interval. WITHIN 10 will use fewer processor cycles and less network bandwidth, but it could miss processes that start and end within the 10 seconds of inactivity, or allow an unwanted process to misbehave for up to 10 seconds.

If you use special extrinsic event classes such as Win32_ProcessStartEvent, you don't have to use WITHIN to specify a polling interval. The section "Using Win32_ProcessStartTrace to catch processes," below, illustrates how to use this class.

"WHERE TargetInstance ISA 'Win32_Process'" filters the events that the query is hunting for. If there were no filter, __InstanceCreationEvent theoretically would return an event for every instance of a WMI class that is created on the computer, which would be a bit overwhelming. The filtering clause tells WMI to look for only those events representing instances of Win32_Process. TargetInstance is a property of __InstanceCreationEvent that retrieves the instance of the class that has been created.

Do
   WScript.Sleep 10000
Loop

This endless loop makes the script wait indefinitely for events. If this code were not in the script, the script would terminate after the event query and the sink would not be able to wait around to receive events. The Sleep method of WScript takes an integer representing milliseconds. This interval, though, does not affect the polling interval of the event query. As always in a command window, you can end the script by pressing Ctrl+C.

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
  Wscript.Echo objLatestEvent.TargetInstance.Name
End Sub

This subroutine runs when the asynchronous query finds a match. There are a few other events available for the SWbemSink object, but OnObjectReady is the one we'll usually use with asynchronous event queries.

This is where we have to use "SINK_" as the beginning of the sub name because we specified this string when we created the sink. The OnObjectReady event of the SWbemSink object fires when the monitoring thread left by the event query calls back with an object representing the event, and passes in two parameters.

  • objLatestEvent represents an object (of type SWbemObject) that the script can use to find out about the event. Because this query returns an instance of the __InstanceCreationEvent class, the object has the properties of this class, including TargetInstance. Through TargetInstance, the subroutine can access the properties of the instance that was created, in this case an instance of Win32_Process, which has a Name property.

  • objAsyncContext is a named value set that can optionally be passed to the async query, when multiple asynchronous calls use the same sink, to specify which call triggered this event. The call in this script does not specify this parameter, but we show an example of using this later in this column.

The one line within this routine echoes the name of the process that triggered the instance creation event, which we get from TargetInstance.

Note that Windows Server 2003 provides an authorization mechanism to ensure that the callback is actually coming from the asynchronous routine that the script sent out. On previous operating system versions, though, impersonation of the callback could be a concern in some environments. For more details, see the WMI SDK topics Calling a Method and Querying WMI.

There, was that really so painful? Um … sorry about that. What's that? No, we haven't heard of any cases of post-traumatic stress disorder as a result of using asynchronous event queries. We do hope the emotional scars heal quickly. Doctor Scripto suggests you take two aspirin and visit the Script Center in the morning.

Filtering Processes in a WQL Query with a WHERE Clause

One of the comments that came back to us suggested that the first script in the last column, "Find processes," should use a WHERE clause in the WQL query to filter rather than performing its own filtering by checking each running process against the list of target processes to filter out the target processes.

As a general rule, it is more efficient to filter with the WQL query than with custom code. It often makes sense to take advantage of this, particularly when the dataset the query will return from WMI is large or network bandwidth is an issue.

In this case, neither the list of running processes nor that of target processes is long, so on a moderate number of machines there is probably not much difference in performance. Generally, we try to write our example scripts to reveal clearly what they are doing rather than trying to optimize for performance. But the observation seemed worth passing along and keeping in mind, particularly when large numbers of machines and large amounts of data are involved.

Here's the first script from the last column. It checks the running processes against the array of target processes by using nested For Next loops. The outside loop iterates through the running processes returned by the query, while the inner one iterates through the array of target processes. The script compares each running process against each target process, lower-casing both to avoid false negatives based on case differences.

Listing A (Listing 1 in last column): Find processes.

strComputer = "."
arrTargetProcs = Array("calc.exe","notepad.exe","freecell.exe")
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process")
For Each objProcess in colProcesses
  For Each strTargetProc In arrTargetProcs
    If LCase(objProcess.Name) = LCase(strTargetProc) Then
      WScript.Echo objProcess.Name
    End If
  Next
Next

The following script shows an alternative way to accomplish the same task. It uses just one For Each loop to loop through the array of target processes. For each target process, it calls ExecQuery with a query that uses a WHERE clause to filter for that process name, returning only matches. The WHERE filter in the WQL query here does the work of the outer For Each loop in the previous script.

Listing 2: Alternative way to find specific processes

strComputer = "."
arrTargetProcs = Array("calc.exe","notepad.exe","freecell.exe")
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
For Each strTargetProc In arrTargetProcs
  Set colProcesses = objWMIService.ExecQuery _
  ("SELECT * FROM Win32_Process WHERE Name='" & strTargetProc & "'")
  For Each objProcess in colProcesses
    WScript.Echo objProcess.Name
  Next
Next

Using Win32_ProcessStartTrace to Handle Process Events

A couple of columns back, in "It's 2 a.m. Do you know where your processes are?", we mentioned three special event classes that are designed to monitor processes: Win32_ProcessTrace and its derived classes Win32_ProcessStartTrace and Win32_ProcessStopTrace. In that column we showed how to use Win32_ProcessStopTrace to monitor process deletion events.

As one reader suggested, we could also use Win32_ProcessStartTrace to monitor process creation events, instead of using __InstanceCreationEvent and Win32_Process as we did in the last column.

Using an extrinsic WMI event class such as the members of the Win32_ProcessTrace family eliminates the need to set a polling interval with the WITHIN clause in the WQL event query. This may reduce the load on those machines on which it is running. The down side, however, is that once we have trapped an event, we cannot immediately call Win32_Process properties or methods on it, because we don't have access to the TargetInstance property of the __InstanceCreationEvent class. But as the second script in this section shows, there are workarounds.

The first example script in this section illustrates how to asynchronously monitor the creation of specific processes with Win32_ProcessStartTrace events. In addition, if we want to terminate unwanted processes we can combine Win32_ProcessStartTrace with Win32_Process, as the second example in this section demonstrates.

The first script merely takes the first asynchronous example in this column and adds code to check for specific processes. The asynchronous query then simply uses Win32_ProcessStartTrace without a filter.

When a process starts up and the event triggers, the event sink code loops through the list of target processes and checks the ProcessName property of Win32_ProcessStartTrace against each target process name, lower-casing both. If a match is found the script displays the name, process ID and time.

Listing 3: Asynchronously monitor specific processes with Win32_ProcessStartTrace

On Error Resume Next

strComputer = "."
arrTargetProcs = Array("calc.exe", "notepad.exe", "freecell.exe")

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM Win32_ProcessStartTrace"

Wscript.Echo "Waiting for target processes ..."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)

For Each strTargetProc In arrTargetProcs
  If LCase(objLatestEvent.ProcessName) = LCase(strTargetProc) Then
    Wscript.Echo VbCrLf & "Process Name: " & objLatestEvent.ProcessName
    Wscript.Echo "  Process ID: " & objLatestEvent.ProcessID
    Wscript.Echo "  Time: " & Now
  End If
Next

End Sub

The next listing takes this same script and adds code to terminate target processes. When it finds a match, it assigns the process ID to the variable strHandle. The Handle property of Win32_Process contains a number that is the same as the process ID, but as a string data type. Handle, however, has the Key qualifier, which means it can be used to uniquely identify instances of Win32_Process, while ProcessId is not a Key property.

We now use the strHandle variable in a call to the Get method of the WMI service. This call retrieves the single instance of Win32_Process whose Handle property matches the value of the process ID contained in strHandle. With the object returned by the Get method, we can call the Terminate method of Win32_Process to kill the process. We have to take these extra steps because the Win32_ProcessStartTrace class does not have any methods, so we cannot use it directly to terminate the process.

Listing 4: Terminate specific processes with Win32_ProcessStartTrace and Win32_Process.

On Error Resume Next

strComputer = "."
arrTargetProcs = Array("calc.exe", "notepad.exe", "freecell.exe")

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM Win32_ProcessStartTrace"

Wscript.Echo "Waiting for target processes ..."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

For Each strTargetProc In arrTargetProcs
  If LCase(objLatestEvent.ProcessName) = LCase(strTargetProc) Then
    Wscript.Echo VbCrLf & "Process Name: " & objLatestEvent.ProcessName
    strHandle = CStr(objLatestEvent.ProcessID)
    Wscript.Echo "  Process ID: " & strHandle
    Wscript.Echo "  Time: " & Now
    Set objProcess = objWMIService.Get _
     ("Win32_Process.Handle='" & strHandle & "'")
    intReturn = objProcess.Terminate
    If intReturn = 0 Then
      Wscript.Echo "  Terminated"
    Else
      Wscript.Echo "  Unable to terminate"
    End If
  End If
Next

End Sub

Displaying an Interpretation of the Return Value

Another enhancement suggested by one reader was to interpret the return codes from methods in order to display more detailed messages. In the previous script, we simply check whether the returned value was 0, which indicates success. If not, we display a generic failure message.

But many methods of WMI classes offer more specific return values, which may help in troubleshooting problems. For example, Win32_Process.Terminate has 5 other return codes besides zero with specific meanings. So in place of the If intReturn = 0 Then … Else … End If statement in the previous script, you could substitute a Select Case structure that interprets each valid return code for the method.

Listing 5: Interpret return codes for Win32_Process.Terminate.

Select Case intReturn
  Case 0 Wscript.Echo "  Terminated"
  Case 2 Wscript.Echo "  Access denied"
  Case 3 Wscript.Echo "  Insufficient privilege"
  Case 8 Wscript.Echo "  Unknown failure"
  Case 9 Wscript.Echo "  Path not found"
  Case 21 Wscript.Echo "  Invalid parameter"
  Case Else Wscript.Echo "  Unable to terminate for undetermined reason"
End Select

Using objAsyncContext parameter for SINK_OnObjectReady

We mentioned above, when we did the dissection of a simple asynchronous event monitoring script, that we'd show an example how to use the objAsyncContext parameter that is passed into the SINK_OnObjectReady sub. objAsyncContext references an object of type SWbemNamedValueSet, which is part of the WMI Scripting API.

The SWbemNamedValueSet object is a collection of SWbemNamedValue objects, each of which is just a name-value pair. If this sounds familiar, you may have used the Dictionary object of Script Runtime, which is very similar. Here we have to use SWbemNamedValueSet, because that's what the OnObjectReady event is expecting.

We create the set in these lines:

Set objContext = CreateObject("WbemScripting.SWbemNamedValueSet")
objContext.Add "hostname", strComputer

Now we have an SWbemNamedValueSet object with just one member, whose name is "hostname" and whose value is the contents of strComputer. Each iteration of the For Each loop will create a new context object with the name of that computer.

Asynchronous callbacks to the sink pass this object in as the second parameter in the line:

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)

And then in this line we use the object to display the computer name from which the async callback is coming:

WScript.Echo VbCrLf & "Computer Name: " & objAsyncContextItem.Value

This is a convenient way to get the name of the computer that is the source of the event when using Win32_ProcessStartTrace, since this class does not have a property that lets us get the source computer directly. This approach might also come in handy with other classes that, unlike Win32_Process, that don't have a property like CSName that directly gets the name of the computer against which the script is running.

You could also use the objAsyncContext parameter in situations where different asynchronous method calls on the same machine are calling back to the same sink, as a way to distinguish between the sources.

Listing 6: Asynchronously monitor process start events on multiple machines using the same sink.

On Error Resume Next

arrComputers = Array("sea-wks-1","sea-wks-2","sea-srv-1","sea-srv-2")
strQuery = "SELECT * FROM Win32_ProcessStartTrace"

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

For Each strComputer In arrComputers
  Set objContext = CreateObject("WbemScripting.SWbemNamedValueSet")
  objContext.Add "hostname", strComputer
  Set objWMIService = GetObject("winmgmts:" _
   & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
  If Err = 0 Then
    objWMIService.ExecNotificationQueryAsync SINK, strQuery, , , , objContext
    Wscript.Echo "Waiting for processes to start on " & strComputer & " ..."
  Else
    Wscript.Echo "Computer Name: " & strComputer & vbCrLf & _
     "ERROR: " & Err.Number & vbTab & Err.Description
    Err.Clear
  End If
Next

Wscript.Echo "In monitoring mode. Press Ctrl+C to exit."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************
        
Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

Set objAsyncContextItem = objAsyncContext.Item("hostname")
WScript.Echo VbCrLf & "Computer Name: " & objAsyncContextItem.Value
Wscript.Echo "  Process Name: " & objLatestEvent.ProcessName
Wscript.Echo "  Process ID: " & objLatestEvent.ProcessID
Wscript.Echo "  Time: " & Now

End Sub

Taking a Snapshot of Running Processes and Alerting on New Ones

A couple of readers pointed out the difficulty of maintaining a list of undesirable executables, some of which may use multiple and changeable filenames. One reader suggested creating a list of services and processes which should be running, which would have to be revised only when software was patched or upgraded, and locking out any services and processes not on the list. Another reader similarly suggested taking a snapshot of the current running processes as a benchmark, and then checking each new process – if it's not on the list, the script should prompt to either terminate it or let it run, and in the latter case add it to the list.

It might be hard to put a benchmark machine in a state where it was running all the services and processes that would typically be running on it so you could get a complete list. Perhaps this would be easier for specialized servers or skinny clients that run only a couple of specific applications.

In any case, once you had a benchmark machine in the desired state, it would be easy to dump the processes running on it into a text file, one process per line. We've already used similar FileSystemObject code in several previous columns.

Listing 7: Write list of running processes to a text file.

Const FOR_WRITING = 2
strComputer = "."
strOutputFile = "processes.txt"
Set objFSO = Createobject("Scripting.FilesystemObject")
Set objTextStream = objFSO.CreateTextFile(strOutputFile)
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process")
For Each objProcess In colProcesses
  objTextStream.WriteLine objProcess.Name
Next
objTextStream.Close

This script dumps all running processing, but it's bare-bones: it doesn’t sort the processes alphabetically or weed out duplicate names. That could be done manually, or functions to perform those tasks could be easily added to the script.

Now that we have a list of acceptable processes, here's a script that compares running processes to the benchmark list and displays those not on the list.

The logic for checking processes against the benchmark list again uses a pair of nested For Each loops. We iterate through each running process, starting out by setting a counter to 0. We then compare the running process to each of the benchmark processes, and if there is a match we increment the counter. If the counter is still 0 after checking against the entire list, the current process is not on the list, so we display its name and process ID. By adding another line that called the Terminate method of objProcess we could also kill the unlisted process.

Listing 8: Compare processes to a list in a text file

Const FOR_READING = 1
strInputFile = "processes.txt"
Set objFSO = Createobject("Scripting.FilesystemObject")
Set objTextStream = objFSO.OpenTextFile(strInputFile, FOR_READING)
arrProcesses = Split(objTextStream.ReadAll, vbCrLf)
objTextStream.Close
Set objWMIService = GetObject("winmgmts:")
Set colProcesses = objWMIService.ExecQuery _
 ("SELECT * FROM Win32_Process")
WScript.Echo "Processes not on list"
For Each objProcess In colProcesses
  i = 0
  For Each strProcess In arrProcesses
    If LCase(objProcess.Name) = LCase(strProcess) Then
      i = 1
    End If
  Next
  If i = 0 Then
    WScript.Echo VbCrLf & "Process Name: " & objProcess.Name
    WScript.Echo "  Process ID: " & objProcess.ProcessId
  End If
Next

For a script that runs without user intervention, of course, you wouldn't want to have a prompt pop up asking whether or not to terminate the process – in this scenario, we'd want to build that intelligence into the script. But if the script was intended to be run as a scanner with a user present, it could be easily adapted to prompt the user.

In the previous script, you could interpolate, after

WScript.Echo "  Process ID: " & objProcess.ProcessId

and before

End If

the following code:

WScript.Echo "Terminate process? Press y or n."
    strYesOrNo = WScript.StdIn.ReadLine
    If "y" = LCase(Left(strYesOrNo, 1)) Then
      objProcess.Terminate
      WScript.Echo "  Process terminated."
    Else
      WScript.Echo "  Process not terminated."
    End If

This would display a prompt in the command shell window where the script is running.

To open a popup message box for the same purpose, insert the following code:

intReturn = MsgBox("Terminate process?", 4, "Process Decision")
    If intReturn = 6 Then
      objProcess.Terminate
      WScript.Echo "  Process terminated."
    Else
      WScript.Echo "  Process not terminated."
    End If

The first parameter of the VBScript MsgBox function is the prompt displayed in the box. The second parameter, 4, causes the message box to display Yes and No buttons. The third parameter is the title displayed in the title bar of the box.

We might also want to add processes not terminated to the benchmark list, as the reader suggested. This could be a separate message box or prompt, so that not all processes allowed to run are added to the list.

Without too much more ado, you could rig up a function that monitors for events generated by processes not on a list, rather than processes on a list, and that could be plugged into the final script of the previous column.

The next script shows the basic code that function would use, although it is not broken out into functions and displays its output on the command line. It calls ExecNotificationQueryAsync to query the Win32_ProcessStartTrace class. Then in the sub that handles the OnObjectReady event, it uses code including a counter, much like the previous script, to determine whether the current process is not on the list. If it is not, the script uses the same call to get the process object and call Terminate on it that we've seen above.

Be sure to run this script only on a test machine, as it will try to terminate any processes that start up that are not on the list.

Listing 9: Asynchronously monitor processes and terminate those not on list.

On Error Resume Next

strComputer = "."

Const FOR_READING = 1
strinputfile = "proclist.txt"
Set objFSO = Createobject("Scripting.FilesystemObject")
Set objTextStream = objFSO.OpenTextFile(strInputFile, FOR_READING)
arrTargetProcs = Split(objTextStream.ReadAll, vbCrLf)
objTextStream.Close

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM Win32_ProcessStartTrace"

Wscript.Echo "Waiting for processes not on list ..."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

i = 0
For Each strTargetProc In arrTargetProcs
  If LCase(objLatestEvent.ProcessName) = LCase(strTargetProc) Then
    i = 1 
  End If
Next
If i = 0 Then
  Wscript.Echo VbCrLf & "Process Name: " & objLatestEvent.ProcessName
  strHandle = CStr(objLatestEvent.ProcessID)
  Wscript.Echo "  Process ID: " & strHandle
  Wscript.Echo "  Time: " & Now
  Set objProcess = objWMIService.Get _
   ("Win32_Process.Handle='" & strHandle & "'")
  intReturn = objProcess.Terminate
  Select Case intReturn
    Case 0 Wscript.Echo "  Terminated"
    Case 2 Wscript.Echo "  Access denied"
    Case 3 Wscript.Echo "  Insufficient privilege"
    Case 8 Wscript.Echo "  Unknown failure"
    Case 9 Wscript.Echo "  Path not found"
    Case 21 Wscript.Echo "  Invalid parameter"
    Case Else Wscript.Echo "  Unable to terminate for undetermined reason"
  End Select
End If

End Sub

Other Kinds of Asynchronous Methods

Although we've been talking only about asynchronous event handling here, keep in mind that there are other kinds of asynchronous methods beside ExecNotificationQueryAsync. As we pointed out in the last Script Shop, a few other WMI synchronous methods have asynchronous counterparts. Notably, ExecQuery, the stalwart method of the majority of the Scripting Guys’ example scripts, has an asynchronous fraternal twin, ExecQueryAsync.

Why would you want to perform a non-event WMI query asynchronously? Well, if you expected a whole lot of data to come pouring back in, especially from different machines, an asynchronous query might be more efficient. You might want to query asynchronously when querying event logs, for example, or getting a large list of files.

But that's grist for another mill. This is probably not the last you'll hear of asynchronicity at this URL, if Dr. Scripto has anything to say about it. Tune in next month to find out. Thanks again for your responses and please send more.

Resources