Summary: Bruce Payette shows how to manage event subscriptions with Windows PowerShell.
Microsoft Scripting Guy, Ed Wilson, here. I am really excited about the idea I had for this week, and I hope you will be too. I asked Candace Gillhoolley at Manning Press about posting some sample works from some of the Manning Press library of books. She responded enthusiastically and shared five samples that we will post this week. Today is part two of two parts from Bruce Payette and Windows PowerShell in Action. See yesterday’s blog post for part 1.
Windows PowerShell in Action, Second Edition
By Bruce Payette
The key difference between event-based scripting and traditional procedural scripting is that, instead of an activity being executed as a result of an action in the script, a script (or at least a portion of it) is executed as a result of an action by the system.. In this article based on chapter 20 of Windows PowerShell in Action, Second Edition, author Bruce Payette discusses asynchronous event-handling models in PowerShell. To save 35% on your next purchase use Promotional Code payette22035 when you check out at www.manning.com.
Managing event subscriptions
In this section, you’ll see how to find your event subscriptions and how to remove them when you’re done with them. Being able to remove them is important because event subscriptions persist in the session until explicitly removed.
Listing event subscriptions
Of course, before you can remove a subscription, you have to find it. Windows PowerShell provides the Get-EventSubscriber to do this. Let’s use it to look at the subscription you registered in the previous section (see yesterday’s Hey! Scripting Guy blog):
PS (1) > Get-EventSubscriber > SubscriptionId : 1 SourceObject : System.Timers.Timer EventName : Elapsed SourceIdentifier : fca4b869-8d5a-4f11-8d45-e84af30845f1 Action : System.Management.Automation.PSEventJob HandlerDelegate : SupportEvent: False ForwardEvent: False
The Get-EventSubscriber cmdlet returns PSEventSubscriber objects, which have complete information about the registration: the object generating the event, the action to execute, and so on. There are a couple of interesting properties to note in this output. Because you didn’t give the subscription a friendly name using -Source-Identifier when you created it, the Register-ObjectEvent generated one for you. This autogenerated name is the string representation of a GUID, so you know it’s unique (but not very friendly). The other thing to notice is that the action shows up as a PowerShell Job object.
Removing event subscriptions
Now that you can list the event subscriptions, you can set about removing them. The cmdlet to do this is not Unsubscribe-Event because unsubscribe isn’t on the approved verbs list and it’s not what you want to do anyway. You registered event subscriptions with Register-ObjectEvent, so what you need to do is unregister the subscription, which you’ll do with Unregister-Event. The cmdlet noun in this case is Event, not ObjectEvent, because you can use a common mechanism to unregister any kind of event. It’s only the registration part that varies. The rest of the eventing cmdlets remain the same.
When you’re unregistering an event subscription, there are two ways of identifying the event to unregister: by the SubscriptionId property or by the Source-Identifier. The subscription ID is simply an integer that’s incremented each time an event subscription is created. Because you didn’t give your event registration a friendly name, you’ll use the SubscriptionId to unregister it:
> PS (4) > Unregister-Event -SubscriptionId 1 -Verbose > VERBOSE: Performing operation "Unsubscribe" on Target "Event subscription 'timertest2'
Note that you included the -Verbose flag in this command so that you could see something happening. Let’s try running the command again
> PS (5) > Unregister-Event -SubscriptionId 1 > Unregister-Event : Event subscription with identifier '1' does not > exist. > At line:1 char:17 > + Unregister-Event <<<< -SubscriptionId 1 > + CategoryInfo : InvalidArgument: (:) [Unregister-Event > ], ArgumentException > + FullyQualifiedErrorId : INVALID_SUBSCRIPTION_IDENTIFIER, > Microsoft.PowerShell.Commands.UnregisterEventCommand >
…and it results in an error. The Unregister-Event cmdlet is silent as long as nothing goes wrong. If something does go wrong, you get an error.
We’ve covered the basics of creating and managing event subscriptions. But before the handlers for these events can do much useful work, they’ll need access to additional information. In the next section, you’ll write more sophisticated handlers and see how they can use the automatic variables provided by the eventing subsystem.
Asynchronous event handling with scriptblocks
In this section, we’ll look at the automatic variables and other features that PowerShell provides to allow scriptblocks to be used as effective event handlers.
Automatic variables in the event handler
In PowerShell eventing, the scriptblock that handles the event action has access to a number of variables that provide information about the event being handled:
> $event, $eventSubscriber, $sender, $sourceEventArgs, and $sourceArgs
These variables are described in table 2.
Table 2 The automatic variables available in the event handler scriptblock
Variable |
Description |
$event |
This variable contains an object of type System.Management.Automation.PSEventArgs that represents the event that’s being handled. It allows you to access a wide variety of information about the event, as you’ll see in an example. The value of this variable is the same object that the Get-Event cmdlet returns. |
$eventSubscriber |
This variable contains the PSEventSubscriber object that represents the event subscriber of the event that’s being handled. The value of this variable is the same object that the Get-EventSubscriber cmdlet returns. |
$sender |
The value in this variable is the object that generated the event. This variable is a shortcut for $EventArgs.Sender. |
$sourceEventArgs |
Contains objects that represent the arguments of the event that’s being processed. This variable is a shortcut for $Event.SourceArgs. |
$sourceArgs |
Contains the values from $Event.SourceArgs. Like any other scriptblock, if there is a param statement, the parameters defined by that statement will be populated and $args will only contain leftover values for which there were no parameters. |
Let’s write a quick test event handler to see what’s in the object in $Event. You’ll use the timer event again:
> PS (1) > $timer = New-Object System.Timers.Timer -Property @{ > >> Interval = 1000; Enabled = $true; AutoReset = $false } > >>
In the event subscription action, you’ll display the contents of the event object:
PS (2) > Register-ObjectEvent $timer Elapsed -Action { > >> $Event | Out-Host > >> } > >> > > Id Name State HasMoreData Location > -- ---- ----- ----------- -------- > 4 9e3586c3-534... NotStarted False >
You’ll start the timer to generate the event:
PS (3) > $timer.Start() > PS (4) > > > ComputerName : > RunspaceId : 373d0ee9-47a5-4ceb-89e5-61e6389d6838 > EventIdentifier : 7 > Sender : System.Timers.Timer > SourceEventArgs : System.Timers.ElapsedEventArgs > SourceArgs : {System.Timers.Timer, System.Timers.ElapsedEv > entArgs} > SourceIdentifier : 9e3586c3-534b-465a-84b3-7404110a0f12 > TimeGenerated : 8/10/2010 12:17:40 PM > MessageData : >
In this output, you see the properties on the PSEvent**** **object that correspond to the variables listed in table 2. The **Timer object that generated the event is available through the Sender property on the object and the $sender variable in the scriptblock.
The PSEvent object also includes context data about the event, including the time the event occurred, the event identifier, and the RunspaceId this event is associated with. The ComputerName property is blank because this is a local event, but, in the case of a remote event, it would contain the name of the computer where the event occurred.
Dynamic modules and event handler state
Because an event can fire at any time, you could never know what variables were in scope and this, in turn, could make it hard to know what state will exist when the action is executed. Instead, you want to be able to run the event handlers in a well-defined, isolated environment. This objective aligns with the design goals for PowerShell modules, so you can leverage this feature by creating a dynamic module for the action scriptblock. The eventing subsystem does this by calling the New-BoundScriptBlockScriptblock() method to attach a dynamic module to the handler scriptblock.
Beyond ensuring a coherent runtime environment for your event handler scriptblock, the module also allows it to have private state. This ability can be quite useful when you’re monitoring a system’s behavior over a period of time. The information can be accumulated privately and then processed once enough samples have been gathered. Let’s look at an example that illustrates how this state isolation works. The following is a trivial example where you maintain a count of the number of timer events fired. Once you reach a predetermined limit, the timer will be stopped. Let’s walk through the example. First, you create the Timer object:
PS (1) > $timer = New-Object System.Timers.Timer -Property @{ > >> Interval = 500; AutoReset = $true} > >> >
As usual, subscribe to the Elapsed event on the timer:
PS (2) > Register-ObjectEvent -InputObject $timer ` > >> -MessageData 5 ` > >> -SourceIdentifier Stateful -EventName Elapsed -Action { > >> $script:counter += 1 > >> Write-Host "Event counter is $counter" > >> if ($counter -ge $Event.MessageData) > >> { > >> Write-Host "Stopping timer" > >> $timer.Stop() > >> } > >> } > $null > >> >
In the handler scriptblock for this event, you’re updating a script-scoped variable $script:counter, which holds the number of times the event has fired. This variable will only be visible within the dynamic module associated with the event, thus preventing your $counter from colliding with any other users of a variable called $counter.
After the variable is incremented, you print the event count and then check to see if the limit has been reached. Notice that you’re making use of the -MessageData parameter to pass the limit to the event handler, which it retrieves from the MessageData property on the **Event **object. Now start the timer running to see it in action:
PS (3) > $timer.Start() > PS (4) > > PS (5) > Event counter is 1 > Event counter is 2 > Event counter is 3 > Event counter is 4 > Event counter is 5 > Stopping timer > > PS (6) > >
As intended, the timer message is displayed five times and then the timer is stopped. This example can easily be modified to, for example, monitor CPU usage or process working sets over a period of time.
Summary
There are two fundamental event types: synchronous and asynchronous. In synchronous events, all activities are synchronized so that no activity is ever interrupted. Asynchronous events execute in a nondeterministic order. To deal with these asynchronous events, PowerShell includes an eventing subsystem that takes care of synchronizing all operations. The core model for eventing in PowerShell is built around the idea of event subscriptions. There are three cmdlets for creating these subscriptions: Get-ObjectEvent, Get-WmiEvent, and Get-EngineEvent for .NET, WMI, and PowerShell engine events respectively.
As part of the event subscription, an action scriptblock may be specified that will be executed when the event is triggered. Context information for the event is made available to the scriptblock through the $Event automatic variable. Some of the properties on the object in $Event are also directly available through additional automatic variables.
Thank you, Bruce.
Well, this concludes an awesome week of guest writers from Manning Press. Join me tomorrow for the Weekend Scripter as I delve into my top ten favorite Windows PowerShell tricks.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy
0 comments