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 it quickly falls apart when you need to monitor huge portions of the system—such as the entire Registry or filesystem.

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 and 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:	emp


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:	empfile.txt
        }
    }

PS > Stop-Process -n notepad
PS > Get-Item c:	empfile.txt
Get-Item : Cannot find path 'C:	empfile.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-ObjectEvent, Register-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 -Action parameter, PowerShell automatically processes 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 processes only 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: MessageData, TimeGenerated, etc.

args

The arguments and 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, the Timers.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.NewEvent property.

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 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 Write Commands That Maintain State.

For more information about useful .NET and WMI events, see Appendix I.

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, use the Register-ObjectEvent or Register-WmiEvent cmdlets to register for one of their events. In the -Action script block, use the New-Event cmdlet to generate a new, more specialized event.

In this scenario, the event 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-WmiEvent cmdlets offer a -SupportEvent parameter.

Here is an example of two functions that 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)
        }
}

## Disable process creation events
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:Windowssystem32calc.exe"
CreationClassName          : Win32_Process
CreationDate               : 20100221151553.574124-480
CSCreationClassName        : Win32_ComputerSystem
CSName                     : LEEHOLMES1C23
Description                : calc.exe
ExecutablePath             : C:Windowssystem32calc.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 exit keyword 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, see Save State Between Sessions.

PowerShell treats engine events like any other type of event. You can use the Register-EngineEvent cmdlet to automatically react to these events, just as you can use the Register-ObjectEvent and Register-WmiEvent cmdlets to react to .NET and WMI events, respectively. For information about how to respond to events automatically, see 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-TemporaryEvent command shown in Example 31-1 automates this for you.

Example 31-1. Register-TemporaryEvent.ps1

##############################################################################
##
## Register-TemporaryEvent
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Registers an event action for an object and automatically unregisters
itself afterward.

.EXAMPLE

PS >$timer = New-Object Timers.Timer
PS >Register-TemporaryEvent $timer Disposed { [Console]::Beep(100,100) }
PS >$timer.Dispose()
PS >Get-EventSubscriber
PS >Get-Job

#>

param(
    ## The object that generates the event
    $Object,

    ## The event to subscribe to
    $Event,

    ## The action to invoke when the event arrives
    [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 it 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 you 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 Respond to Automatically Generated Events. For more information about PowerShell Remoting, see Chapter 29.

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.WriteError
   Exception,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 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 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 delegate 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 the ServerCertificateValidationCallback property in the System.Net.ServicePointManager class. Your delegate should return $true if the certificate should be accepted and $false otherwise. To accept all certificates during a development session, simply run the following 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 keeps them from 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 more lightweight development experience. To see an example of this approach, see Program: Add a Graphical User Interface to Your Script.

For more information about PowerShell’s event handling, see Respond to Automatically Generated Events.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.133.54