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.
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
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.
You want to create new events for other scripts to consume or want to respond automatically when they occur.
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
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-
Engine
Event
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.
You want to automatically perform an action when an event arrives but automatically remove the event subscription once that event fires.
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
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.
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.
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 :
PowerShell’s eventing infrastructure lets you define one of three possible actions when you register for an event:
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.
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
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.
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_
method:Event
()
$form.Add_Shown( { $form.Activate(); $textbox.Focus() } )
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.
3.142.133.54