Freigeben über


It's 2 a.m. Do you know where your processes are?

By The Microsoft Scripting Guys

Doctor Scripto at work

Doctor Scripto's Script Shop talks about practical system administration scripting problems, often from the experiences of our readers, and develops scripts to solve them. Most of our examples in the Script Repository perform one simple task; in this column, by contrast, we'll put those pieces together into more complex scripts. They won't be comprehensive or bullet-proof, but they will show how to build 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 you will find these columns and scripts useful – please let us know what you think of them, what solutions you've come up with for these problems, and what you'd like to see covered in the future.

For an archive of previous columns, see the Doctor Scripto's Script Shop archive.

(Note: Many thanks to Patrick Lanfear and Tom Yaguchi for Dr. Scripto's new look.)

On This Page

It's 2 a.m. Do you know where your processes are?
Scheduling a job versus creating a process
Test executables
When you WSH upon a process
WMI to the rescue
Configuring the process with Win32_ProcessStartup
Monitoring Process Events
Coding WMI event handling in VBScript
Other Ways to Monitor Process Events
Finding the Duration of a Process

It's 2 a.m. Do you know where your processes are?

We send a lot of processes out into the world, but do we really know what they do once they get out there?

Doctor Scripto cares a lot about processes. He often hangs out with them on the heap and sometimes even runs with them. He knows that processes, left to their own devices, can sometimes get into trouble—not because they're bad processes, but just because processes will be processes.

With the supervision of a caring monitor, processes can have happy and productive lives, doing the important work that keeps our networks running. But to ensure this, we sometimes have to keep track of what they're doing after we start them.

Dr. Scripto got interested in the arcane art of process monitoring when he worked in a halfway house for recovering viruses many years ago. He soon became obsessed with finding ways to automate tracking what every process there was doing.

Scheduling a job versus creating a process

More recently, he crossed paths with the issue in our last column, "Inventorying Windows XP Service Packs – Part 3 – Scripting the Rollout." In that screed, you may recall, we decided to run the Windows XP Service Pack 2 installation executable (Xpsp2.exe) by creating a scheduled task on each remote machine with the WMI class Win32_ScheduledJob.

Originally, Dr. Scripto had planned to run the setup with the Create method of Win32_Process. However, he wanted to run Xpsp2.exe from a file share on a central server, and in that scenario, running a process did not fill the bill. The problem was that WMI bridled at the idea of creating a process on a remote machine that then had to call a remote executable on yet another machine using a UNC path to the share. Can you blame it?

With Service Pack 2, Dr. Scripto could have taken the approach of copying the executable out to each machine and using Win32_Process to run it there. He chose not to because of the hefty footprint of Xpsp2.exe.

In many cases, though, Win32_Process is the simpler way to go to run small executables easily copied to remote hosts, or to run applications already known to exist on the remote hosts. If it runs in a process, you can start it with the Create method – with the caveat that on remote machines it will always run in a hidden window, even if you specify a visible window. So you can't create a process this way on remote machines and then expect users to interact with it there. On the bright side, users will not be surprised by strange windows popping up uninvited on their desktops.

Why would you want to create remote processes? Well, you might want to write a script that installs patches for the operating system or applications (that is, if for some reason you prefer not to use Windows Update or Microsoft Systems Management Server). A script that creates a remote process might also be a good way to run a backup program on desktops or a disk cleanup or diagnostic program on the hard drives of your file servers.

Note: Incidentally, if you want to run defrag on Windows Server 2003, an easier way is to do it with a script that uses the Defrag method of the WMI class Win32_Volume. For more information on how to do this, see the recent Scripting Guys column, "Defragmenting Disks the Windows 2003 Way."

Test executables

For some of the examples in this column, we'll use our old friend Notepad to simulate an application that runs until terminated by the user. Remember that if you're running one of these examples against a remote machine, you'll have to open Task Manager to see the notepad.exe process because the window will be hidden.

For those scripts where, on the other hand, we want the process to run for a certain length of time (as a setup program would, for example), we'll use a simple stand-in script, Test.vbs. This script contains just one line of code:

WScript.Sleep 30000

The Windows Script Host method "Sleep" simply runs for the specified number of milliseconds without doing anything, then exits. This example runs for 30000 milliseconds, or 30 seconds. In the scripts, we execute this script by running:

cscript test.vbs

If we don't specify a script host (cscript or wscript, the executables that actually run scripts), WMI gets confused because the script is not an executable in itself and the Create process returns a code of 8, an "unknown failure."

Watch the Processes tab of Task Manager on the target machine as you run Test.vbs: a cscript.exe process appears for 30 seconds, then disappears. You can also see this process in the command-line output of Tasklist.exe or Tlist.exe.

This test script is a simple way to simulate any application that runs for a period of time, then exits without user intervention. Even though executables such as patches or backup programs run for indeterminate amounts of time rather than a set number of seconds, Test.vbs is still a reasonable proxy for such applications when you test a script. If you wanted the test script to run for variable durations, you could add code to it that would randomize a variable containing the number of seconds (with the VBScript functions Randomize and Rnd), and pass that variable as the parameter to Wscript.Sleep. For details on how to do this, see "Hey, Scripting Guy! How Can I Generate Random Numbers Using a Script?" https://www.microsoft.com/technet/scriptcenter/resources/qanda/may05/hey0518.mspx

When you WSH upon a process

If you've been scripting for a while, the basics of running an executable from within a script may be old hat. But in case you haven't had occasion to do this, let's review the possibilities.

The simplest way to run an executable is to use the Run method of the WshShell object, part of the object model of Windows Script Host. Run lets you control the window in which the application runs, and so can be useful for GUI applications.

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "notepad.exe"

When you run this script, a Notepad window should open on your desktop.

Keep in mind that all the scripts in this column are designed to run under Cscript.exe. So if Cscript is not the default script host on your machine, you must preface the name of the script on the command line with "cscript", for example, cscript script1.vbs.

To run command-line executables, you can also use the WshShell Exec method. Exec returns a WshScriptExec object, which contains status and error information. This method also provides access to the standard streams of the executable, such as STDOUT, which contains the command-line output of the executable.

Set objShell = CreateObject("Wscript.Shell")
Set objWshScriptExec = objShell.Exec("ipconfig.exe")
WScript.Echo objWshScriptExec.StdOut.ReadAll

This script captures the command-line output of Ipconfig and displays it, also at the command line. It will look something like this:

C:\scripts>wshexec.vbs

Windows IP Configuration

Ethernet adapter Local Area Connection:

        Connection-specific DNS Suffix  . : fabrikam.com
        IP Address. . . . . . . . . . . . : 192.168.0.192
        Subnet Mask . . . . . . . . . . . : 255.255.255.0
        Default Gateway . . . . . . . . . : 192.168.0.1

Quite a feat of scripting wizardry, eh? You're doing in three lines the same thing you could accomplish by simply running Ipconfig. Sarcasm aside, though, this capability can be very useful when you want to do something with the command-line output of a tool in a script – such as parse it for a particular string.

With both the Run and Exec methods, however, you're limited to running executables on the local machine. WSH does also include a WshController object that can run scripts remotely, but it involves some setup on local and remote machines.

Unfortunately, then, WSH functionality is awkward to work with if our goal is to run executables on remote machines and track the results.

WMI to the rescue

As it so often happens in system administration scripting, all roads lead to Windows Management Instrumentation (WMI). The WMI class Win32_Process has a Create method that lets you run a GUI or command-line executable locally or remotely and get back the process ID for the process in which it's running.

strCommand = "notepad.exe" 
Set objProcess = GetObject("winmgmts:root\cimv2:Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, Null, intProcessID)

Notice that in line 2, the GetObject call retrieves the Win32_Process class definition rather than instances of it (as many scripts do with ExecQuery). This is necessary because the Create method is called on the Win32_Process class, not on an instance of it. When Create is called, it then creates an instance of Win32_Process.

When calling the Create method, you pass four parameters to it (in order):

  • CommandLine (strCommand), a string containing the name of the executable and any command-line arguments to be passed to it.

  • CurrentDirectory, the current path for the process to be spawned. We’ve specified Null here to use the current directory.

  • ProcessStartupInformation, an object of type Win32_ProcessStartup that contains startup configuration for the process. We've specified Null here, but we’ll use this parameter in later scripts.

  • ProcessID (intProcessID), the ID number of the spawned process. While the first three parameters are in parameters, ProcessID is an out parameter, that is, a parameter that the method returns to the caller. You specify a variable name for the fourth parameter, and Create fills it with an integer representing the PID of the created process that you can then use in the script.

To run a process on a remote machine, you assign the name of the machine to the strComputer variable (as in the next script), which substitutes the machine name in the moniker string passed to GetObject (line 3):

strComputer = "server1"
strCommand = "notepad.exe"
Set objProcess = GetObject("winmgmts:\\" & strComputer & _
 "\root\cimv2:Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, Null, intProcessID)

Pretty simple, huh? WMI gives us remoting nearly for free. And despite the pleasures and health benefits of walking around to all of our machines, the ability to sit at an administrative workstation and do work on remote machines anywhere is one of the big benefits that scripting has to offer. But you knew that. Dr. Scripto just enjoys preaching to the choir and hearing all those amens.

Configuring the process with Win32_ProcessStartup

Although we didn't use it in the last two scripts, we did mention that the third parameter of Create, ProcessStartupInformation, can contain an object that tells the method some details about how the process should run.

To create that object representing the startup configuration of the process, you can use Win32_ProcessStartup. This class has 14 properties dealing with the window in which the process runs, environmental variables, and priority. We'll simply pass a constant to one of these properties, ShowWindow, specifying that the window should be displayed as a normal window for that application.

Win32_ProcessStartup is unusual for a WMI class in that it is a method type definition, used only for passing information to a method. After retrieving the class definition of Win32_ProcessStartup with the Get method, you have to call the SpawnInstance_ method to create an instance of the class. Then you can set the read/write properties of the instance to the desired values. After that, the object is ready to pass to Create as the third parameter.

This is a fair amount of work just to say you want a normal window. But once you have the boilerplate code, if you want to change the window configuration it's easy to do that by modifying the constant. And setting other properties requires no more than adding a line of code for each to the template.

In the previous two scripts, we called the Create method and assigned its return value to the variable intReturn. We didn't do anything with it then, but in this script, we use the return value to determine what information the script outputs to the command line. If the return value is 0, that means the process was successfully created so we display the process ID. If the return value is any other value, it indicates that the method failed, so we display the return value (which you could look up in the WMI SDK) as an indicator.

Const NORMAL_WINDOW = 1
strComputer = "."
strCommand = "notepad.exe"
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set objStartup = objWMIService.Get("Win32_ProcessStartup")
Set objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = NORMAL_WINDOW
Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, objConfig, intProcessID)
If intReturn = 0 Then
  Wscript.Echo "Process Created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Process ID: " & intProcessID
Else
  Wscript.Echo "Process could not be created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Return value: " & intReturn
End If

Typical script output:

C:\scripts>process-create.vbs
Process Created.
Command line: notepad.exe
Process ID: 3464

Monitoring Process Events

OK, now we've created a process, which is pretty exciting in itself (at least for Dr. Scripto). But having sent that process out into the world, how are we going to track what happens to it?

We could just check every 10 seconds for an instance of Win32_Process with that process ID and see if anything changed. At some point, we wouldn't find that process ID anymore and we'd know the process had terminated. But that would be a bit labor-intensive, and still might not tell us what we want to know.

Fortunately, WMI provides us with a better way. When things happen to WMI classes and their instances, WMI generates events that describe what's going on in a form that applications and scripts can consume and use.

Events are happening thick and fast all the time on any computer. Every change in a hard drive or directory or service or process generates an event. WMI alone unleashes what mathematicians would call gazillions of events per second. But just as when a tree falls in the forest, you have to be there listening to hear it − yet not so close that it falls on you − so it is with events. Something, some pseudo-sentient entity made of code, has to be listening for events or they fly off ineffectually into the void. Fortunately, events don't weigh very much so you don't have to worry about being hit by a falling one.

This discussion is veering dangerously close to the philosophical, and we want at all costs to avoid setting Dr. Scripto off on his rant about epistemology. For a much more digestible explanation of WMI events, tune into our recent TechNet on-demand webcast "An Ounce of Prevention: An Introduction to WMI Events."

Doctor Scripto as existentialist

For now, let's lose the philosophy and get down to brass tacks. What scripting techniques can we use to dip into this Amazonian stream of events?

Coding WMI event handling in VBScript

When you connect to WMI with the "winmgmts:" moniker, as we have in the previous scripts, you get back an SWbemServices object, which is defined by the WMI scripting API. In most WMI scripts on the Script Center, we use the ExecQuery method of SWbemServices when we need to get a collection of instances of a class.

To receive WMI events, however, we have to use a different method: ExecNotificationQuery. The pattern for querying for and trapping events is different from our standard scripts, so let's take a look at how to do it.

Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " & _ 
 "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'")

The ExecNotificationQuery method takes as a parameter a Windows Query Language (WQL) query string much like the one we're used to passing to ExecQuery. In this case, though, there are a couple of new twists. The class we're selecting instances from is the __InstanceOperationEvent class, an intrinsic (built-in) WMI event class that captures any creation, modification, or deletion of an instance of a WMI class.

To specify the polling interval (how often the script will check for events), we use the WITHIN statement and an integer representing the number of seconds. Then, we filter the query with a WHERE clause that uses an ISA statement, which tells ExecNotificationQuery what kind of events we're looking for. Here, we're filtering for events where the TargetInstance property of __InstanceOperationEvent is an instance of Win32_Process.

The class we select from in this query, __InstanceOperationEvent, is a WMI system class that serves as the base class for all intrinsic event classes that receive events from instances of other classes. The event classes derived from it are:

  • __InstanceCreationEvent
    Notifies whenever a new instance of a WMI class is created.

  • __InstanceModificationEvent
    Notifies whenever an existing instance of a WMI class is modified.

  • __InstanceDeletionEvent
    Notifies whenever an existing instance of a WMI class is deleted.

When you query for __InstanceOperationEvent, the collection returned includes instances of all three derived classes.

Note: The WMI SDK explains more about these classes in "Determining the Type of Event to Receive".

In other words, this query is asking WMI to keep us posted on the comings, goings and doings of every process on this machine. However, you can use WMI event monitoring to trap events triggered by any WMI class, not just Win32_Process. We're focusing on processes here because they're familiar and we're going to use them further in this column. But you could just as easily monitor events generated by, say, Win32_LogicalDisk or Win32_Service.

When you execute an ExecNotificationQuery, you get back an SWbemEventSource object, which is also part of the WMI scripting API object model. In the next script, we name the returned object reference "colMonitorProcess" because it's a sequential collection of the processes we've selected to monitor with the query.

The SWbemEventSource object has a NextEvent method that retrieves information on the next event to occur that matches the query defined in ExecNotificationQuery. We assign the result of the call to NextEvent to an object reference, objLatestEvent, with this line:

Set objLatestEvent = colMonitorProcess.NextEvent

NextEvent just sits there until an event comes along that fits the query. Then it springs into action.

When the event fires and is frozen in the headlights of NextEvent, objLatestEvent contains an instance of the event class that fired. We retrieve the name of that event class with objLatestEvent.Path_.Class. Through objLatestEvent, the script can access the TargetInstance property of __InstanceOperationEvent that we referred to in the query. Now TargetInstance represents the first instance of Win32_Process that has been created, modified or deleted. We use TargetInstance to get the properties of the Win32_Process instance, in this case Name and ProcessID. The last thing we do is call the built-in VBScript function Now to tell us the date and time that the event was fired.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " _ 
 & " WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process change event ..."
Set objLatestEvent = colMonitorProcess.NextEvent
WScript.Echo VbCrLf & objLatestEvent.Path_.Class
Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
WScript.Echo "Time: " & Now

Typical script output:

C:\scripts>event-single.vbs
Waiting for process change event ...

__InstanceModificationEvent
Process Name: System Idle Process
Process ID: 0
Time: 5/17/2005 4:21:05 PM

If a script simply calls NextEvent, it will catch the next event that occurs and then terminate. In most cases, though, we're trying to monitor certain kinds of events and we probably don't want to see just the first event that comes along. To continually monitor events, all we have to do is put the code that calls NextEvent into an endless Do loop. The following script illustrates how to do this to continually trap all process creation, modification and deletion events.

Incidentally, to end any script such as this one that runs continuously, use Ctrl+C.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process to start or stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  WScript.Echo VbCrLf & objLatestEvent.Path_.Class
  Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  WScript.Echo "Time: " & Now
Loop

Typical script output:

C:\scripts>event-loop.vbs
Waiting for process change event ...

__InstanceModificationEvent
Process Name: System Idle Process
Process ID: 0
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: scardsvr.exe
Process ID: 1100
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: wmiprvse.exe
Process ID: 1416
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: wmiprvse.exe
Process ID: 1788
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: wdfmgr.exe
Process ID: 1868
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: WINWORD.EXE
Process ID: 1944
Time: 5/17/2005 5:13:48 PM
^C

Whoa, Nellie! As you can see when you run the previous script, __InstanceModificationEvent fires so frequently that the noise-to-signal ratio of the output goes off the scale. Some processes, it would appear, keep changing relentlessly but are still never satisfied.

To produce more usable output, the following script filters for process creation and deletion events only, ignoring modification events.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process to start or stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  If objLatestEvent.Path_.Class = "__InstanceCreationEvent" _
   Or objLatestEvent.Path_.Class = "__InstanceDeletionEvent" Then
    WScript.Echo VbCrLf & objLatestEvent.Path_.Class
    Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
    Wscript.Echo "Process ID: " & _
     objLatestEvent.TargetInstance.ProcessId
    WScript.Echo "Time: " & Now
  End If
Loop

Typical script output:

C:\scripts\processes>col4-event2.vbs
Waiting for process to start or stop ...

__InstanceCreationEvent
Process Name: notepad.exe
Process ID: 3656
Time: 5/17/2005 5:19:26 PM

__InstanceCreationEvent
Process Name: calc.exe
Process ID: 2644
Time: 5/17/2005 5:19:33 PM

__InstanceDeletionEvent
Process Name: calc.exe
Process ID: 2644
Time: 5/17/2005 5:19:36 PM

__InstanceDeletionEvent
Process Name: notepad.exe
Process ID: 3656
Time: 5/17/2005 5:19:40 PM

__InstanceCreationEvent
Process Name: SMSCliUI.exe
Process ID: 2316
Time: 5/17/2005 5:19:43 PM
^C

Try starting and stopping Notepad and other simple applications. Any time a process starts or stops, we find out about it. Now we're getting a continuous stream of usable information on events in our command-prompt window.

Because TargetInstance contains an instance of Win32_Process, we can use it to find out any property of that class. In this case, we got only the name and process ID from that class. But we might want to find out other things that Win32_Process can tell us − and even things beyond what Win32_Process can tell us.

Other Ways to Monitor Process Events

When we monitor Win32_Process events in particular, there's an even easier way to go than the __InstanceOperationEvent classes. WMI in its wisdom has provided three special event classes that simplify monitoring process events: Win32_ProcessTrace and its derived classes Win32_ProcessStartTrace and Win32_ProcessStopTrace.

The following script shows a slightly simpler way to get process deletion events:

On Error Resume Next
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcessStopTrace = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM Win32_ProcessStopTrace")
WScript.Echo "Waiting for process to stop ..."
Do
  Set objLatestEvent = colProcessStopTrace.NextEvent
  WScript.Echo
  Wscript.Echo "Process Name: " & objLatestEvent.ProcessName
  Wscript.Echo "Process ID: " & objLatestEvent.ProcessId
  Wscript.Echo "Time: " & objLatestEvent.TIME_CREATED
'Property exists only on Windows Server 2003.
  Wscript.Echo "Exit Code: " & objLatestEvent.ExitStatus
Loop

Typical script output:

C:\scripts>event-trace.vbs
Waiting for process to stop ...

Process Name: notepad.exe
Process ID: 3904
Time: 127608497739906527
Exit Code: 0

Process Name: calc.exe
Process ID: 308
Time: 127608497793389831
Exit Code: 0
^C

The Win32_ProcessStopTrace class contains its own properties, which unfortunately don't include all the properties of Win32_Process. It does, however, contain one useful property not found in Win32_Process: ExitStatus. From this property, you can get the exit code, which indicates the success or failure of the executable running in the process. Because this property is available only in Windows Server 2003, you might want to add code to this script to check the OS version before trying to retrieve it. Here, we've simply used On Error Resume Next.

For many purposes the properties of Win32_ProcessStopTrace may be adequate, but what if we wanted to find out the duration that the process ran? Among the properties of Win32_ProcessStopTrace is TIME_CREATED, which you might, if you were literal-minded, expect to be the time the process was created. We're monitoring the event fired when a process stops, though, so TIME_CREATED in this context is the time that the stop event occurred − that is, the time the process stopped. There may be a certain twisted logic to this, but a name like TIME_OCCURRED might have been more intuitive.

Wait, there's more to whine about. What's worse is the format in which TIME_CREATED returns the time of the event. According to the WMI SDK, TIME_CREATED contains a uint64 (64-bit) data type that "represents the number of 100-nanosecond intervals after January 1, 1601."

We're sorry, but Doctor Scripto refuses on principle to have anything to do with data types that originate in the 17th Century. (The rest of the Scripting Guys think it might fetch a pretty good price on Antiques Roadshow.) We could probably write a function that turns this into a friendlier date, but there's actually a better way to go if we want to find out more about the life and times of a process.

Finding the Duration of a Process

An obvious thing we might like to find out is how long the process ran. We already have the date and time it stopped when we use Now. But what about the time it started?

In earlier examples we trapped __InstanceCreationEvent and __InstanceDeletionEvent for a process; we could record Now for each of these. But then matching creation and deletion times for many processes from the two separate events becomes a daunting job.

For an easier way to get the process start time, we'll have to go back to the intrinsic event classes and Win32_Process, rather than use Win32_ProcessStopTrace.

big code

When we query for __InstanceDeletionEvent and instances of Win32_Process, we can find out when the process started from TargetInstance.CreationDate. CreationDate, of course, is a property of Win32_Process. The class also has a TerminationDate property, but to get access to it you have to hold open a handle to the process after it terminates, which you can't do with VBScript. For our purposes, which do not require nanosecond precision, Now will do pretty well as the termination date.

Note: If you run this script against a remote machine by changing strComputer, keep in mind that you're getting the process creation time from WMI on the remote machine, but the deletion time from Now on the local machine where the script is running. This means that the clocks on the two machines must be synchronized exactly to get a time accurate down to the second. Fortunately, on many networks, clocks are synchronized automatically. Then, too, in many cases the application will be running for minutes or hours and to-the-second accuracy is not critical. In any case, if machine clocks were not synchronized and you wanted to get the time on the remote machine, you could add code that uses a WMI class such as Win32_LocalTime .

The following script traps the process deletion event and gets start and stop times for the process:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for a process to stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  WScript.Echo VbCrLf & objLatestEvent.Path_.Class
  Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  Wscript.Echo "Time Created: " & _
   objLatestEvent.TargetInstance.CreationDate
  WScript.Echo "Time Deleted: " & Now
Loop

Typical output:

C:\scripts>event-created.vbs
Waiting for a process to stop ...

__InstanceDeletionEvent
Process Name: calc.exe
Process ID: 3652
Time Created: 20050517173536.064608-420
Time Deleted: 5/17/2005 5:35:39 PM

__InstanceDeletionEvent
Process Name: notepad.exe
Process ID: 1736
Time Created: 20050517173530.135373-420
Time Deleted: 5/17/2005 5:35:42 PM
^C

Wait, what's that ugly blob after "Time Created:"? If you stare at it for a long time, it may start to make some sense, especially after you notice that it's essentially backwards (or you may just get dizzy). At least it has nothing to do with the 17th Century.

The long string of mostly numbers returned by the CreationDate property is in a format called DATETIME by WMI. Starting with Windows Server 2003, the WMI scripting API provides a new object called SWbemDateTime that includes functionality to manipulate and convert three different date/time formats: DATETIME, FILETIME (the antique nanoseconds format), and the human-friendlier format, known to WMI as VT_DATE.

date conversion

Because most of us probably don't have networks consisting entirely of machines running Windows Server 2003, however, we'll write our own function to convert DATETIME to a U.S.-style date string (apologies to those used to other formats − it shouldn't be too hard to rewrite this function to make it return your favored format).

The WMIDateToString function takes as a parameter a WMI DATETIME string, which begins with the year on the left and ends with the + or - offset from Greenwich Mean Time. It applies VBScript string manipulation functions Mid and Left to dismember and reassemble the string of numbers into the same readable date format seen after "Time Deleted:".

Function WMIDateToString(dtmDate)

WMIDateToString = CDate(Mid(dtmDate, 5, 2) & "/" & _
                  Mid(dtmDate, 7, 2) & "/" & _
                  Left(dtmDate, 4) & " " & _
                  Mid(dtmDate, 9, 2) & ":" & _
                  Mid(dtmDate, 11, 2) & ":" & _
                  Mid(dtmDate, 13, 2))

End Function

Now let's trap that process deletion event again. This time, though, we'll also use a VBScript function called DateDiff to calculate the process duration, the difference in seconds between the time the process was created and the time it was deleted. DateDiff requires that both dates be in VT_DATE format, so we'll use our WMIDateToString function to convert the process CreationDate value.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process to stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  strProcDeleted = Now
  strProcCreated = _
   WMIDateToString(objLatestEvent.TargetInstance.CreationDate)
  Wscript.Echo VbCrLf & "Process Name: " & _
   objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  Wscript.Echo "Time Created: " & strProcCreated
  WScript.Echo "Time Deleted: " & strProcDeleted
  intSecs = DateDiff("s", strProcCreated, strProcDeleted)
  WScript.Echo "Duration: " & intSecs & " seconds"
Loop

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

'Convert WMI DATETIME format to US-style date string.
Function WMIDateToString(dtmDate)

WMIDateToString = CDate(Mid(dtmDate, 5, 2) & "/" & _
                  Mid(dtmDate, 7, 2) & "/" & _
                  Left(dtmDate, 4) & " " & _
                  Mid(dtmDate, 9, 2) & ":" & _
                  Mid(dtmDate, 11, 2) & ":" & _
                  Mid(dtmDate, 13, 2))

End Function

Typical output:

C:\scripts>event-duration.vbs
Waiting for process to stop ...

Process Name: calc.exe
Process ID: 2068
Time Created: 5/17/2005 5:39:26 PM
Time Deleted: 5/17/2005 5:39:33 PM
Duration: 7 seconds

Process Name: notepad.exe
Process ID: 1800
Time Created: 5/17/2005 5:39:21 PM
Time Deleted: 5/17/2005 5:39:39 PM
Duration: 18 seconds
^C

Now that we know how to monitor process events, let’s try creating an event that runs for a given time and monitoring its lifespan. We'll start with the simplest case so we can see the logic clearly, creating a process on a local or remote machine, then monitoring the process event and outputting only raw start and stop times. We’ll be running the Test.vbs script we created earlier so we know the process will run for only a specified period of time.

We first attempt to create the process. If the return value of Create is not 0, that means the process was not created, so we display the return code and exit.

If we successfully create the process, we get back the process ID in the out parameter (intProcessID), so we now know the unique identifier of the process we want to monitor. Then in the event handling part of the code, we query for the __InstanceDeletionEvent event for Win32_Process that has the process ID of the process we created. Once we find it, we output information about the process.

Const NORMAL_WINDOW = 1
strComputer = "."
strCommand = "cscript c:\scripts\test.vbs"

'Connect to WMI.
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

'Create process startup parameters.
Set objStartup = objWMIService.Get("Win32_ProcessStartup")
Set objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = NORMAL_WINDOW

'Create a new process.
Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, objConfig, intProcessID)

If intReturn = 0 Then
  Wscript.Echo "Process Created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Process ID: " & intProcessID

'Monitor process deletion events
  Set colMonitorProcess = objWMIService.ExecNotificationQuery _
   ("SELECT * FROM __InstanceDeletionEvent " & _ 
   "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
   "AND TargetInstance.ProcessId = '" & intProcessID & "'")
  WScript.Echo "Waiting for process to stop ..."
  Set objLatestEvent = colMonitorProcess.NextEvent
  strTimeDeleted = Now
  Wscript.Echo VbCrLf & "Process Name: " & _
   objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  Wscript.Echo "Time Created: " & _
   objLatestEvent.TargetInstance.CreationDate
  WScript.Echo "Time Deleted: " & strTimeDeleted

Else
  Wscript.Echo "Process could not be created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Return value: " & intReturn
End If

Typical output:

C:\scripts>monitor-event-simple.vbs
Process Created.
Command line: cscript c:\scripts\test.vbs
Process ID: 2052
Waiting for process to stop ...

Process Name: cscript.exe
Process ID: 2052
Time Created: 20050517164539.367041-420
Time Deleted: 5/17/2005 4:46:10 PM

For this column's final, more complex script, we've broken this process creation and event monitoring machinery down into separate procedures and added a little error-handling. This time we'll calculate process duration in seconds and then use a separate function, SecsToHours, to divide the total seconds into hours, minutes and seconds. For long processes, such as a backup program, this produces more understandable output than a large number of seconds.

For the conversion of the WMI DATETIME value of CreationDate, we're again using the WMIDateToString function used in the previous script.

Besides SecsToHours and WMIDateToString, we've broken the code from the last script into two procedures:

  • The CreateProcess function, which takes a command string as a parameter and returns the process ID if successful or -1 if not.

  • The MonitorEvent subroutine, which takes the process ID as a parameter.

The logic in the body of the script checks the return value of CreateProcess and calls MonitorEvent only if the value is not zero, which insures that the value passed to MonitorEvent represents the process ID of the created process.

In MonitorEvent, besides the duration of the process we could have used TargetInstance to find out anything else about the process for which Win32_Process has a property. And the class has 45 properties.

On Error Resume Next

strComputer = "." 'Can change to name of remote machine.
strCommand = "cscript c:\scripts\test.vbs"

'Connect to WMI.
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

If Err.Number <> 0 Then
  HandleError
Else
  intPID = CreateProcess(strCommand)
  If intPID <> -1 Then
    MonitorEvent(intPID)
  End If
End If

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

Function CreateProcess(strCL)

Const NORMAL_WINDOW = 1

'Create process startup parameters.
Set objStartup = objWMIService.Get("Win32_ProcessStartup")
Set objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = NORMAL_WINDOW

'Create a new process.
Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCL, Null, objConfig, intProcessID)

If intReturn = 0 Then
  Wscript.Echo "Process Created." & _
   vbCrLf & "Command line: " & strCL & _
   vbCrLf & "Process ID: " & intProcessID
  CreateProcess = intProcessID
Else
  Wscript.Echo "Process could not be created." & _
   vbCrLf & "Command line: " & strCL & _
   vbCrLf & "Return value: " & intReturn
  CreateProcess = -1
End If

End Function

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

Sub MonitorEvent(intProcessID)

'Monitor process deletion events
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " & _ 
 "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
 "AND TargetInstance.ProcessId = '" & intProcessID & "'")
WScript.Echo "Waiting for process to stop ..."
Set objLatestEvent = colMonitorProcess.NextEvent
strProcDeleted = Now
strProcCreated = _
 WMIDateToString(objLatestEvent.TargetInstance.CreationDate)
Wscript.Echo VbCrLf & "Process Name: " & _
 objLatestEvent.TargetInstance.Name
Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
Wscript.Echo "Time Created: " & strProcCreated
WScript.Echo "Time Deleted: " & strProcDeleted
intSecs = DateDiff("s", strProcCreated, strProcDeleted)
arrHMS = SecsToHours(intSecs)
WScript.Echo "Duration: " & arrHMS(2) & " hours, " & _
 arrHMS(1) & " minutes, " & arrHMS(0) & " seconds"

End Sub

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

Function WMIDateToString(dtmDate)
'Convert WMI DATETIME format to US-style date string.

WMIDateToString = CDate(Mid(dtmDate, 5, 2) & "/" & _
                  Mid(dtmDate, 7, 2) & "/" & _
                  Left(dtmDate, 4) & " " & _
                  Mid(dtmDate, 9, 2) & ":" & _
                  Mid(dtmDate, 11, 2) & ":" & _
                  Mid(dtmDate, 13, 2))

End Function

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

Function SecsToHours(intTotalSecs)
'Convert time in seconds to hours, minutes, seconds and return in array.

intHours = intTotalSecs \ 3600
intMinutes = (intTotalSecs Mod 3600) \ 60
intSeconds = intTotalSecs Mod 60
SecsToHours = Array(intSeconds, intMinutes, intHours)

End Function

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

'Handle errors.
Sub HandleError

WScript.Echo "ERROR " & Err.Number & VbCrLf & _
 "Description: " & Err.Description & VbCrLf & _
 "Source: " & Err.Source
Err.Clear

End Sub

Typical output:

C:\scripts>monitor-event.vbs
Process Created.
Command line: cscript c:\scripts\test.vbs
Process ID: 2168
Waiting for process to stop ...

Process Name: cscript.exe
Process ID: 2168
Time Created: 5/17/2005 4:25:38 PM
Time Deleted: 5/17/2005 4:26:10 PM
Duration: 0 hours, 0 minutes, 32 seconds

Thirty-two seconds? Hey, Test.vbs runs for 30 seconds! Hmm, that could have something to do with the one-second polling interval specified by "WITHIN 1" in the query and with a possible rounding error because we're measuring only to the second. Or it could be a little long because Dr. Scripto was chatting with the process at the water cooler before they went back to work. If we needed a very precise time, we'd want to use a different approach, but for the purposes of this script our algorithm works fine.

Well, here we are at a logical break point: we've run a process on one machine and found out how long it took to run. Big deal, you may be saying to yourself. I don't administer one machine: I have a whole network out there to patch and maintain. Don't touch that dial, stay tuned to this frequency. Dr. Scripto will not rest until he learns the life story of every process he's created on every machine on his network.