posh | Powershell‎ > ‎posh | notes‎ > ‎

posh.note | event

Chapter 31. Event Handling

Introduction

Much of system administration is reactionary: taking some action when a system service shuts down, when files are created or deleted, when changes are made to the Windows registry, or even on a timed interval.

The easiest way to respond to system changes is to simply poll for them. If you're waiting for a file to be created, just check for it every once in a while until it shows up. If you're waiting for a process to start, just keep calling the Get-Process cmdlet until it's there.

This approach is passable for some events (such as waiting for a process to come or go), but quickly falls apart when you need to monitor huge portions of the system—such as the entire Registry, or file system.

An an alternative to polling for system changes, many technologies support automatic notifications—known as events. When an application registers for these automatic notifications, it can respond to them as soon as they happen—rather than having to poll for them.

Unfortunately, each technology offers its own method of event notification. .NET defines one approach, while WMI defines another. When you have a script that wants to generate its own events, neither technology offers an option.

PowerShell addresses this complexity by introducing a single, consistent, set of event-related cmdlets. These cmdlets let you work with all of these different event sources. When an event occurs, you can let PowerShell store the notification for you in its event queue, or use an Action script block to process it automatically:

PS > "Hello" > file.txt
PS > Get-Item file.txt

    Directory: C:\temp


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         2/21/2010  12:57 PM         16 file.txt


PS > Get-Process notepad

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     64       3     1140       6196    63     0.06   3240 notepad


PS > Register-WmiEvent Win32_ProcessStopTrace `
>>     -SourceIdentifier ProcessStopWatcher `
>>     -Action {
>>         if($EventArgs.NewEvent.ProcessName -eq "notepad.exe")
>>         {
>>             Remove-Item c:\temp\file.txt
>>         }
>>     }

PS > Stop-Process -n notepad
PS > Get-Item c:\temp\file.txt
Get-Item : Cannot find path 'C:\temp\file.txt' because it does not exist.

By building on PowerShell eventing, you can write scripts to quickly react to an ever-changing system.

Respond to Automatically-Generated Events

Problem

You want to respond automatically to a .NET, WMI, or Engine event.

Solution

Use the -Action parameter of the Register-ObjectEventRegister-WmiEvent, and Register-EngineEvent cmdlets to be notified when an event arrives, and have PowerShell invoke the script block you supply:

PS > $timer = New-Object Timers.Timer
PS > $timer.Interval = 1000
PS > Register-ObjectEvent $timer Elapsed -SourceIdentifier Timer.Elapsed `
>>     -Action { $GLOBAL:lastRandom = Get-Random }
>>

Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
2               Timer.Elapsed   NotStarted False


PS > $timer.Enabled = $true
PS > $lastRandom
836077209
PS > $lastRandom
2030675971
PS > $lastRandom
1617766254
PS > Unregister-Event Timer.Elapsed

Discussion

PowerShell's event registration cmdlets give you a consistent way to interact with many different event technologies: .NET events, WMI events, and PowerShell engine events.

By default, when you register for an event, PowerShell adds a new entry to the session-wide event repository called the event queue. You can use the Get-Event cmdlet to see events added to this queue, and the Remove-Event cmdlet to remove events from this queue.

In addition to its support for manual processing of events, you can also supply a script block to the -Action parameter of the event registration cmdlets. When you provide a script block to the -Actionparameter, PowerShell automatically process events when they arrive.

However, doing two things at once means multithreading. And multithreading? Thar be dragons! To prevent you from having to deal with multithreading issues, PowerShell tightly controls the execution of these script blocks. When it's time to process an action, it suspends the current script or pipeline, executes the action, and then resumes where it left off. It only processes one action at a time.

PS > $timer = New-Object Timers.Timer
PS > $timer.Interval = 1000
PS > Register-ObjectEvent $timer Elapsed -SourceIdentifier Timer.Elapsed `
>>     -Action { Write-Host "Processing event" }
>> $timer.Enabled = $true

PS > while($true) { Write-Host "Processing loop"; Sleep 1 }
Processing loop
Processing event
Processing loop
Processing event
Processing loop
Processing event
Processing loop
Processing event
Processing loop
(...)

Inside of the -Action scriptblock, PowerShell gives your script access to five automatic variables:

  • eventSubscriber: The subscriber (event registration) that generated this event.

  • event: The details of the event itself: MessageDataTimeGenerated, etc.

  • args: The arguments / parameters of the event handler. Most events place the event sender and customized event information as the first two arguments, but this depends on the event handler.

  • sender: The object that fired the event (if any)

  • eventArgs: The customized event information that the event defines, if any. For example, theTimers.Timer object provides a TimerElapsedEventArgs object for this parameter. This object includes a SignalTime parameter, which identifies exactly when the timer fired. Likewise, WMI events define an object that places most of the information in the $eventArgs.NewEventproperty.

In addition to the script block that you supply to the -Action parameter, you can also supply any objects you'd like to the -MessageData parameter during your event registration. PowerShell associates this data with any event notifications it generates for this event registration.

To prevent your script block from accidentally corrupting the state of scripts that it interrupts, PowerShell places it in a very isolated environment. Primarily, PowerShell gives you access to your event action through its job infrastructure. As with other PowerShell jobs, you can use the Receive-Job cmdlet to retrieve any output generated by your event action:

PS > $timer = New-Object Timers.Timer
PS > $timer.Interval = 1000
PS > Register-ObjectEvent $timer Elapsed -SourceIdentifier Timer.Elapsed `
>>     -Action {
>>         $SCRIPT:triggerCount = 1 + $SCRIPT:triggerCount
>>         "Processing Event $triggerCount"
>>     }
>> $timer.Enabled = $true

Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
1               Timer.Elapsed   NotStarted False

PS > Get-Job 1

Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
1               Timer.Elapsed   Running    True


PS > Receive-Job 1
Processing Event 1
Processing Event 2
Processing Event 3
(...)

For more information about working with PowerShell jobs, see the section called “Invoke a Long-Running or Background Command”.

In addition to exposing your event actions through a job interface, PowerShell also uses a Module to ensure that your -Action script block is not impacted by (and does not impact) other scripts running on the system. As with all modules, $GLOBAL variables are shared by the entire session. $SCRIPT variables are shared and persisted for all invocations of the script block. All other variables persist only for the current triggering of your event action. For more information about PowerShell Modules, see the section called “Write Commands that Maintain State”.

For more information about useful .NET and WMI events, see Appendix I, Selected Events and Their Uses.

Create and Respond to Custom Events

Problem

You want to create new events for other scripts to consume, or want to respond automatically when they occur.

Solution

Use the New-Event cmdlet to generate a custom event. Use the -Action parameter of the Register-EngineEvent cmdlet to respond to that event automatically.

PS > Register-EngineEvent -SourceIdentifier Custom.Event `
>>     -Action { Write-Host "Received Event" }
>>

PS > $null = New-Event Custom.Event
Received Event

Discussion

The New-Event cmdlet lets you create new custom events for other scripts or event registrations to consume. When you call the New-Event cmdlet, PowerShell adds a new entry to the session-wide event repository called the event queue. You can use the Get-Event cmdlet to see events added to this queue, or you can use the Register-EngineEvent cmdlet to have PowerShell respond automatically.

One prime use of the New-Event cmdlet is to adapt complex events surfaced through the generic WMI and .NET event cmdlets. By writing task-focused commands to surface this adapted data, you can offer and work with data that is simpler to consume.

To accomplish this goal, you use the Register-ObjectEvent or Register-WmiEvent cmdlets to register for one of their events. In the -Action script block, you use the New-Event cmdlet to generate a new, more specialized, event.

In this scenario, the events registrations that interact with .NET or WMI directly are merely "support" events, and users would not expect to see them when they use the Get-EventSubscriber cmdlet. To hide these event registrations by default, both the Register-ObjectEvent and Register-WmiEventcmdlets offer a -SupportEvent parameter.

Here is an example of two functions to easily notify you when a new process starts:

## Enable process creation events
function Enable-ProcessCreationEvent
{
    $identifier = "WMI.ProcessCreated"
    $query = "SELECT * FROM __instancecreationevent " +
                 "WITHIN 5 " +
                 "WHERE targetinstance isa 'win32_process'"
    Register-WmiEvent -Query $query -SourceIdentifier $identifier `
        -SupportEvent -Action {
            [void] (New-Event "PowerShell.ProcessCreated" `
                -Sender $sender -EventArguments $EventArgs.NewEvent.TargetInstance)
        }
}

function Disable-ProcessCreationEvent
{
   Unregister-Event -Force -SourceIdentifier "WMI.ProcessCreated"
}

When used in the shell, the experience is much simpler than working with the WMI events directly:

PS > Enable-ProcessCreationEvent
PS > calc
PS > Get-Event

ComputerName     :
RunspaceId       : feeda302-4386-4360-81d9-f5455d74950f
EventIdentifier  : 2
Sender           : System.Management.ManagementEventWatcher
SourceEventArgs  :
SourceArgs       : {calc.exe}
SourceIdentifier : PowerShell.ProcessCreated
TimeGenerated    : 2/21/2010 3:15:57 PM
MessageData      :

PS > (Get-Event).SourceArgs

(...)
Caption                    : calc.exe
CommandLine                : "C:\Windows\system32\calc.exe"
CreationClassName          : Win32_Process
CreationDate               : 20100221151553.574124-480
CSCreationClassName        : Win32_ComputerSystem
CSName                     : LEEHOLMES1C23
Description                : calc.exe
ExecutablePath             : C:\Windows\system32\calc.exe
(...)

PS > Disable-ProcessCreationEvent
PS > notepad
PS > Get-Event

ComputerName     :
RunspaceId       : feeda302-4386-4360-81d9-f5455d74950f
EventIdentifier  : 2
Sender           : System.Management.ManagementEventWatcher
SourceEventArgs  :
SourceArgs       : {calc.exe}
SourceIdentifier : PowerShell.ProcessCreated
TimeGenerated    : 2/21/2010 3:15:57 PM
MessageData      :

In addition to events that you create, engine events also represent events generated by the engine itself. In PowerShell version two, the only defined engine event is PowerShell.Exiting, which lets you do some work when the PowerShell session exits. For PowerShell to handle this event, you must use the exitkeyword to close your session, rather than the X button at the top right of the console window. In the Integrated Scripting Environment, the close button generates this event as well. For an example of this, seethe section called “Save State Between Sessions”.

PowerShell treats engine events like any other type of event. You can use the Register-EngineEventcmdlet to automatically react to these events, just as you can use the Register-ObjectEvent andRegister-WmiEvent cmdlets to react to .NET and WMI events, respectively. For information about how to respond to events automatically, see the section called “Respond to Automatically-Generated Events”.

Create a Temporary Event Subscription

Problem

You want to automatically perform an action when an event arrives, but automatically remove the event subscription once that event fires.

Solution

To create an event subscription that automatically removes itself once processed, remove the event subscriber and related job as the final step of the event action. The Register-TemporaryEventcommand automates this for you.

Example 31.1. Register-TemporaryEvent.ps1

##############################################################################

param($object, $event, [ScriptBlock] $action)

Set-StrictMode -Version Latest

$actionText = $action.ToString()
$actionText += @'

$eventSubscriber | Unregister-Event
$eventSubscriber.Action | Remove-Job
'@

$eventAction = [ScriptBlock]::Create($actionText)
$null = Register-ObjectEvent $object $event -Action $eventAction

Discussion

When you provide a script block for the -Action parameter of Register-ObjectEvent, PowerShell creates an event subscriber to represent that subscription, and also creates a job that lets you interact with the environment and results of that action. If the event registration is really a "throwaway" registration that you no longer want after the event gets generated, cleaning up afterward is a little complex.

Fortunately, PowerShell automatically populates several variables for event actions, one of the most important being $eventSubscriber. This variable represents, perhaps not surprisingly, the event subscriber related to this action. To automatically clean up after the event is generated, pass the event subscriber to the Unregister-Event cmdlet, and then pass the action's job ($eventSubscriber.Action) to the Remove-Job cmdlet.

Forward Events from a Remote Computer

Problem

You have a client connected to a remote machine through PowerShell Remoting, and want to be notified when an event occurs on that machine.

Solution

Use any of PowerShell's event registration cmdlets to subscribe to the event on the remote machine. Then, use the -Forward parameter to tell PowerShell to forward these events when they arrive:

PS > Get-Event
PS > $session = New-PsSession leeholmes1c23
PS > Enter-PsSession $session

[leeholmes1c23]: PS C:\> $timer = New-Object Timers.Timer
[leeholmes1c23]: PS C:\> $timer.Interval = 1000
[leeholmes1c23]: PS C:\> $timer.AutoReset = $false
[leeholmes1c23]: PS C:\> Register-ObjectEvent $timer Elapsed `
>>     -SourceIdentifier Timer.Elapsed -Forward
[leeholmes1c23]: PS C:\> $timer.Enabled = $true
[leeholmes1c23]: PS C:\> Exit-PsSession

PS >
PS > Get-Event

ComputerName     : leeholmes1c23
RunspaceId       : 053e6232-528a-4626-9b86-c50b8b762440
EventIdentifier  : 1
Sender           : System.Timers.Timer
SourceEventArgs  : System.Management.Automation.ForwardedEventArgs
SourceArgs       : {System.Timers.Timer, System.Timers.ElapsedEventArgs}
SourceIdentifier : Timer.Elapsed
TimeGenerated    : 2/21/2010 11:01:54 PM
MessageData      :

Discussion

PowerShell's eventing infrastructure lets you define one of three possible actions when you register for an event:

  • Add the event notifications to the event queue

  • Automatically process the event notifications with an -Action script block

  • Forward the event notifications to a client computer

The -Forward parameter on all of the event registration cmdlets enables this third option. When you are connected to a remote machine that has this type of behavior enabled on an event registration, PowerShell will automatically forward those event notifications to your client machine. Using this technique, you can easily monitor many remote computers for system changes that interest you.

For more information about registering for events, see the section called “Respond to Automatically-Generated Events”. For more information about PowerShell Remoting, see Chapter 29, Remoting.

Investigate Internal Event Action State

Problem

You want to investigate the internal environment or state of an event subscriber's action.

Solution

Retrieve the event subscriber, and then interact with the Subscriber.Action property:

PS > $null = Register-EngineEvent -SourceIdentifier Custom.Event `
>>     -Action {
>>         "Hello World"
>>
>>         Write-Error "Got an Error"
>>
>>         $SCRIPT:privateVariable = 10
>>     }
>>

PS > $null = New-Event Custom.Event
PS > $subscriber = Get-EventSubscriber Custom.Event
PS > $subscriber.Action | Format-List


Module        : __DynamicModule_f2b39042-e89a-49b1-b460-6211b9895acc
StatusMessage :
HasMoreData   : True
Location      :
Command       :
                        "Hello World"
                        Write-Error "Got an Error"
                        $SCRIPT:privateVariable = 10

JobStateInfo  : Running
Finished      : System.Threading.ManualResetEvent
InstanceId    : b3fcceae-d878-4c8b-a53e-01873f2cfbea
Id            : 1
Name          : Custom.Event
ChildJobs     : {}
Output        : {Hello World}
Error         : {Got an Error}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}
State         : Running

PS > $subscriber.Action.Error
Write-Error : Got an Error
At line:4 char:20
+         Write-Error <<<<  "Got an Error"
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteError
   Exception
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExc
   eption,Microsoft.PowerShell.Commands.WriteErrorCommand

Discussion

When you supply an -Action script block to any of the event registration cmdlets, PowerShell creates a PowerShell job to let you interact with that action. When interacting with this job, you have access to the job's output, errors, progress, verbose output, debug output, and warnings.

For more information about working with PowerShell jobs, see the section called “Invoke a Long-Running or Background Command”.

In addition to the job interface, PowerShell's event system generates a module to isolate your script block from the rest of the system—for the benefit of both you and the system.

When you want to investigate the internal state of your action, PowerShell surfaces this state through the action's Module property. By passing the module to the invoke operator, you can invoke commands from within that module:

PS > $module = $subscriber.Action.Module
PS > & $module { dir variable:\privateVariable }

Name                           Value
----                           -----
privateVariable                10

To make this even easier, you can use the Enter-Module script given by the section called “Diagnose and Interact with Internal Module State”.

Use a Script Block as a .NET Delegate or Event Handler

Problem

You want to use a PowerShell script block to directly handle a .NET event or delegate.

Solution

For objects that support a .NET delegate, simply assign the script block to that delegate:

$replacer = {
    param($match)

    $chars = $match.Groups[0].Value.ToCharArray()
    [Array]::Reverse($chars)
    $chars -join ''
}

PS > $regex = [Regex] "\w+"
PS > $regex.Replace("Hello World", $replacer)
olleH dlroW

To have a script block directly handle a .NET event, call that object's Add_Event() method:

$form.Add_Shown( { $form.Activate(); $textbox.Focus() } )

Discussion

When working with some .NET developer APIs, you might run into a method that takes a delegate as one of its arguments. Delegates in .NET act as a way to provide custom logic to a .NET method that accepts them. For example, the solution supplies a custom delegate to the regular expression Replace() method to reverse the characters in the match—something not supported by regular expressions at all.

As another example, many array classes support custom delegates for searching, sorting, filtering, and more. In this example, we create a custom sorter to sort an array by the length of its elements:

PS > $list = New-Object System.Collections.Generic.List[String]
PS > $list.Add("1")
PS > $list.Add("22")
PS > $list.Add("3333")
PS > $list.Add("444")
PS > $list.Add("5")
PS > $list.Sort( { $args[0].Length - $args[1].Length } )
PS > $list
5
1
22
444
3333

Perhaps the most useful delegete per character is the ability to customize the behavior of the .NET Framework when it encounters an invalid certificate in a web network connection. This happens, for example, when you try to connect to a website that has an expired SSL certificate. The .NET Framework lets you override this behavior through a delegate that you supply to theServerCertificateValidationCallback property in the System.Net.ServicePointManagerclass. Your delegate should return $true if the certificate should be accepted, $false otherwise. To simply accept all certificates during a development session, simply run the follwing statement:

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } 

In addition to delegates, you can also assign PowerShell script blocks directly to events on .NET objects.

Normally, you'll want to use PowerShell eventing to support this scenario. PowerShell eventing provides a very rich set of cmdlets that let you interact with events from many technologies: .NET, WMI, and the PowerShell engine itself. When you use PowerShell eventing to handle .NET events, PowerShell protects you from the dangers of having multiple script blocks running at once, and from them interfering with the rest of your PowerShell session.

However, when you write a self-contained script that uses events to handle events in a WinForms application, directly assigning script blocks to those events can be a much lighter-weight development experience. To see an example of this approach, see the section called “Program: Add a Graphical User Interface to Your Script”.

For more information about PowerShell's event handling, see the section called “Respond to Automatically-Generated Events”.


Prev   Next
Chapter 30. Transactions Home Appendix A. PowerShell Language and Environment

Copyright © 2010

All trademarks and registered trademarks appearing on labs.oreilly.com are the property of their respective owners.

Privacy Policy & Terms of Service

Comments