Hey, Scripting Guy! Question

Hey, Scripting Guy! How can I write a script that starts a process, waits until that process ends, and then logs the user off the computer?

-- AG

SpacerHey, Scripting Guy! AnswerScript Center

Hey, AG. You know, the Scripting Guy who writes this column is never one to brag, mainly because he never has much of anything to brag about. In addition tot that, however, he is a humble and modest sort of person; that’s why he hasn’t said a word about something he did a couple weeks ago. But seeing as how he doesn’t have anything else to talk about today (You say he could talk scripting? What, in a daily scripting column?) maybe it’s time to mention what was undoubtedly the greatest athletic feat in recorded history.

What’s that? Lance Armstrong? Well, sure, Lance Armstrong has had a few successes. But, let’s face it, all Lance Armstrong did was ride a bike. Did Lance Armstrong ever throw a basketball the length of the court and make a basket?

Oh, he did? Well, that figures. But too bad. If Lance Armstrong wants to brag about his athletic feats, well, let him get his own daily scripting column!

The occasion was yet another one-on-one basketball game between the Scripting Dad and the Scripting Son. This time out the Scripting Dad managed to win, sinking a last-second shot from behind the three-point line. The Scripting Son – always a good sport when he plays against his father – picked up the ball and heaved it towards the basket at the end of the gym. As is so often the case, that act of frustration suddenly turned into a competition, with the two vying to see who could be the first to make a full-court basket.

To be honest, the Scripting Dad’s first attempt was a bit pathetic; it bounced right around the foul line (to the vast amusement of the Scripting Son, we might add). But that embarrassment only spurred the Scripting Dad on, and around his 15th try or so: swish! Game over, Scripting Son!

Not that the Scripting Dad takes any pleasure in defeating his own son, mind you.

At any rate, we’re sure everyone will agree that making a full-court basket is the greatest athletic feat since the beginning of time; heck, it might very well be the great feat of any kind, athletic or not. As for the second-greatest feat of all time, that’s hard to say. However, and with all due modesty, we’d like to nominate a script that starts a process, waits until that process ends, and then logs the user off the computer:

strComputer = "."

Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2:Win32_Process")
 
objWMIService.Create "Notepad.exe", null, null, intProcessID

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

Set colItems = objWMIService. _
    ExecNotificationQuery("Select * From __InstanceDeletionEvent " _ 
            & "Within 1 Where TargetInstance ISA 'Win32_Process'")
Do 
    Set objProcess = colItems.NextEvent
    If objProcess.TargetInstance.ProcessID = intProcessID Then
        Exit Do
    End If
Loop

Set objWMIService = GetObject("winmgmts:{(Shutdown)}\\" & _
        strComputer & "\root\cimv2")

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")
 
For Each objItem in colItems
    objItem.Win32Shutdown(0)
Next

You know, you’re right: this is kind of a long script, isn’t it? But what the hey; let’s walk through it anyway and see if we can figure out how it all works.

As you can see, we start out by binding to the WMI service on the local computer. There are two important things to note here. First, we bind directly to the Win32_Process class; that’s what the \root\cimv2\:Win32_Process portion of this command does:

Set objWMIService = GetObject _
    ("winmgmts:\\" & strComputer & "\root\cimv2:Win32_Process")

Second, any time we show you a WMI script we usually say, “Blah, blah, blah. But, of course, you can also run this script against a remote computer.” Is that the case here, can you run this script against a remote computer? Well, maybe. The script will actually run just fine against a remote machine; however, the process you create will run in a hidden window. In other words, that process will not be visible on the remote machine (or anywhere else, for that matter). If your application requires no user intervention to run (and then to terminate) then this script will work fine against a remote machine. However, if your application requires some sort of user intervention, well, in that case this script won’t work against a remote machine. That’s because no one will be able to intervene with the application.

Incidentally, that’s all done for security reasons, and there’s no way to change it.

After we connect to the WMI service (and the Win32_Process class) we then use this line of code to start the application (in this case, Notepad.exe):

objWMIService.Create "Notepad.exe", null, null, intProcessID

As you can see, this isn’t too terribly complicated: we simply call the Create method followed by the name of the process we want to create. That process name is, in turn, followed by a pair of Null parameters (these are optional parameters that we don’t care about for this script), which are followed by an “out parameter” we named intProcessID.

This out parameter turns out to be the key to getting the script to work. An out parameter is nothing more than a variable that you supply to a method; in turn, the method assigns a value to that variable. In the case of the Create method, that value is the process ID for our new instance of Notepad. Needless to say, that’s how we’ll be able to determine when our application is terminated. If that process ID disappears, that can only mean that our application is no longer running.

After we’ve started the application our next step is to sit around and wait for the application to end. To do that we re-bind to the WMI service (this time binding just to the root\cimv2 namesapce) and then create a new WMI query, one that looks like this:

Set colItems = objWMIService. _
    ExecNotificationQuery("Select * From __InstanceDeletionEvent " _ 
            & "Within 1 Where TargetInstance ISA 'Win32_Process'")

What we’re doing here is creating an event subscription: we’re asking WMI to notify us each time there are new instances of the __InstanceDeletionEvent class; as the name implies, a new instance of this class is created any time an object is deleted.

You know, you’re right: we did kind of mislead you, didn’t we? After all, we aren’t asking to be notified when any object is deleted; instead, and as our Where clause states, we only want to be notified if the object in question (the TargetInstance) happens to be a member of the Win32_Process class. In other words, we only want to be notified when processes are deleted (that is, each time a process is terminated).

Note. __InstanceDeletionEvent class? TargetInstance? You say you have no idea what we’re talking about here? In that case, you should take a peek at the Scripting Week 2 webcast on WMI events. Granted, you probably still won’t have any idea what we’re talking about; this column has that effect on people. But at least you’ll have a better understanding of things like __InstanceDeletionEvent and TargetInstance.

After setting up our event subscription we then create a Do loop designed to run forever (notice that there’s no exit criteria anywhere):

Do 
    Set objProcess = colItems.NextEvent
    If objProcess.TargetInstance.ProcessID = intProcessID Then
        Exit Do
    End If
Loop

Of course you’re right, as usual: nothing lasts forever, does it? And, as it turns out, this loop won’t last forever, either. Instead, what it’s going to do is wait until a process gets deleted; that will trigger both our event subscription and, in turn, move the script off this line of code (a line of code designed to “block” the script until an event of interest has occurred):

Set objProcess = colItems.NextEvent

Once the event has fired we go ahead and check the ProcessID property of the TargetInstance in order to determine whether that value is equal to intProcessID, the ProcessID of the application we started:

If objProcess.TargetInstance.ProcessID = intProcessID Then

Suppose the two process IDs are different. No problem: that just means that the process that was terminated wasn’t the process that we started. Therefore we loop back around and wait for the next process to be deleted.

On the other hand, suppose the two process IDs are identical. Because process IDs must be unique at any given time, that can only mean one thing: our process has come to an end. And that means that it’s time to log the user off the computer.

To do that we first call the Exit Do statement to exit our heretofore un-exitable loop. As soon as we’re safely out of the loop we then run this block of code:

Set objWMIService = GetObject("winmgmts:{(Shutdown)}\\" & _
        strComputer & "\root\cimv2")

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")
 
For Each objItem in colItems
    objItem.Win32Shutdown(0)
Next

Here we start out by binding to the WMI service one more time, this time including the Shutdown privilege in our binding string. (And, yes, we could have included the Shutdown privilege earlier and then simply reused that object reference. But we thought this approach makes it a little easier for everyone to follow what we were doing.) After making the connection we then use this query to retrieve all instances of the Win32_OperatingSystem class. (Because this class can only retrieve information about the current operating system, there will always be one – and only one – item in the resulting collection.)

Set colItems = objWMIService.ExecQuery _
    ("Select * from Win32_OperatingSystem")

We then set up a For Each loop to loop through each item in the collection; for each of those items we call the Win32Shutdown method, passing the value 0 as the sole method parameter:

objItem.Win32Shutdown(0)

In case you’re wondering, the 0 tells Win32Shutdown to log the user off the computer. Are there other values we could supply to Win32Shutdown, values that would enable you to, say, restart or completely shut down a machine? You bet there are; take a look at the Microsoft Windows 2000 Scripting Guide for more information.

And that should do it, AG.

For those of you who keep track of this sort of thing, the Scripting Son later got his revenge: the next time he and the Scripting Dad played one-on-one he absolutely annihilated his father. But, for the moment at least, the Scripting Dad retains the last laugh. After that particular game they had a three-point shooting contest. The Scripting Son went first, and made 7 out of 15 three-pointers. How many shots did the Scripting Dad need to win the contest? You got it: 8.

Actually, we agree with you: the Scripting Dad should quit his job and become a full-time basketball player. Interestingly enough, that’s something his manager has been suggesting he do for years now ….