Automating the world one-liner at a time…
The second CTP of PowerShell V2 (CTP2) introduces another new feature called PowerShell Eventing. PowerShell Eventing lets you respond to the asynchronous notifications that many objects support. We didn’t get a chance to fully document these cmdlets in the CTP2, though, so here’s a quick start and primer to help you explore the feature.
Before playing with PowerShell eventing, you’ll probably want to find an object that supports events. But how do you know what an object supports? You will most commonly discover this through API documentation ("To receive notifications, subscribe to the <Foo> event,") but Get-Member has also been enhanced to show the events supported by an object:
PS >$timer = New-Object Timers.Timer PS >$timer | Get-Member -Type Event TypeName: System.Timers.Timer Name MemberType Definition ---- ---------- ---------- Disposed Event System.EventHandler Disposed(System.Object, System.EventArgs) Elapsed Event System.Timers.ElapsedEventHandler Elapsed(System.Object, System.Timers.ElapsedEventArgs)
PS >$timer = New-Object Timers.Timer PS >$timer | Get-Member -Type Event
TypeName: System.Timers.Timer
Name MemberType Definition ---- ---------- ---------- Disposed Event System.EventHandler Disposed(System.Object, System.EventArgs) Elapsed Event System.Timers.ElapsedEventHandler Elapsed(System.Object, System.Timers.ElapsedEventArgs)
Once you've determined the event name you are interested in, the Register-ObjectEvent cmdlet registers your event subscription in the system. The SourceIdentifier parameter helps identify events generated by this object, and lets you manage its event subscription:
PS >Register-ObjectEvent $timer Elapsed -SourceIdentifier Timer.Elapsed
In addition to the Register-ObjectEvent cmdlet, additional cmdlets let you register for WMI events and PowerShell engine events.
The Get-PsEventSubscriber cmdlet shows us all event subscribers in the session:
PS >Get-PSEventSubscriber SubscriptionId : 4 SourceObject : System.Timers.Timer EventName : Elapsed SourceIdentifier : Timer.Elapsed Action : HandlerDelegate : SupportEvent : False ForwardEvent : False
PS >Get-PSEventSubscriber
SubscriptionId : 4 SourceObject : System.Timers.Timer EventName : Elapsed SourceIdentifier : Timer.Elapsed Action : HandlerDelegate : SupportEvent : False ForwardEvent : False
The simplest way to manage events is to simply subscribe to them, and occasionally check the event queue. When you are done with the event, remove it from the queue. The Get-PsEvent and Remove-PsEvent cmdlets let you do that:
PS >Get-PsEvent PS >$timer.Interval = 2000 PS >$timer.Autoreset = $false PS >$timer.Enabled = $true PS >Get-PsEvent PS >Get-PsEvent EventIdentifier : 11 Sender : System.Timers.Timer SourceEventArgs : System.Timers.ElapsedEventArgs SourceArgs : {System.Timers.Timer, System.Timers.ElapsedEventArgs} SourceIdentifier : Timer.Elapsed TimeGenerated : 6/10/2008 3:33:39 PM MessageData : ForwardEvent : False PS >Remove-PsEvent * PS >Get-PsEvent
PS >Get-PsEvent PS >$timer.Interval = 2000 PS >$timer.Autoreset = $false PS >$timer.Enabled = $true PS >Get-PsEvent PS >Get-PsEvent
EventIdentifier : 11 Sender : System.Timers.Timer SourceEventArgs : System.Timers.ElapsedEventArgs SourceArgs : {System.Timers.Timer, System.Timers.ElapsedEventArgs} SourceIdentifier : Timer.Elapsed TimeGenerated : 6/10/2008 3:33:39 PM MessageData : ForwardEvent : False
PS >Remove-PsEvent * PS >Get-PsEvent
Most objects pack their interesting data in a class derived from "EventArgs." To make working with these as easy as possible, the SourceEventArgs property is a shortcut to the first parameter in the event handler that derives from EventArgs. The SourceArgs parameter gives you full access to the event handler parameters, letting you deal with events that don't follow the basic "Object sender, EventArgs e" pattern.
Polling for an event is tedious, since you usually want to wait until the event is raised before you do something. The Wait-PsEvent cmdlet lets you do that. Unlike synchronous objects on methods (i.e.: Process.WaitForExit(),) this cmdlet lets you press Control-C to halt the wait:
PS >$timer.Interval = 2000 PS >$timer.Autoreset = $false PS >$timer.Enabled = $true; Wait-PsEvent Timer.Elapsed <2 seconds pass> EventIdentifier : 12 Sender : System.Timers.Timer SourceEventArgs : System.Timers.ElapsedEventArgs SourceArgs : {System.Timers.Timer, System.Timers.ElapsedEventArgs} SourceIdentifier : Timer.Elapsed TimeGenerated : 6/10/2008 3:24:18 PM MessageData : ForwardEvent : False
PS >$timer.Interval = 2000 PS >$timer.Autoreset = $false PS >$timer.Enabled = $true; Wait-PsEvent Timer.Elapsed
<2 seconds pass>
EventIdentifier : 12 Sender : System.Timers.Timer SourceEventArgs : System.Timers.ElapsedEventArgs SourceArgs : {System.Timers.Timer, System.Timers.ElapsedEventArgs} SourceIdentifier : Timer.Elapsed TimeGenerated : 6/10/2008 3:24:18 PM MessageData : ForwardEvent : False
Once you've finished working with the event, remove it from the queue.
While waiting for an event is helpful, you usually don't want to block your script or shell session just waiting for the event to fire. To support this, the Register-*Event cmdlets support an -Action scriptblock. PowerShell will invoke that scriptblock in the background when your event arrives. These actions invoke in their own module — they can get and set $GLOBAL variables, but regular variable modifications happen in their own isolated environment.
function DoEvery { param([int] $seconds,[ScriptBlock] $action ) $timer = New-Object System.Timers.Timer $timer.Interval = $seconds * 1000 $timer.Enabled = $true Register-ObjectEvent $timer "Elapsed" -SourceIdentifier "Timer.Elapsed" -Action $action } DoEvery 2 { [Console]::Beep(300, 100) } ## Eventually Unregister-PsEvent "Timer.Elapsed"
function DoEvery { param([int] $seconds,[ScriptBlock] $action )
$timer = New-Object System.Timers.Timer $timer.Interval = $seconds * 1000 $timer.Enabled = $true Register-ObjectEvent $timer "Elapsed" -SourceIdentifier "Timer.Elapsed" -Action $action }
DoEvery 2 { [Console]::Beep(300, 100) }
## Eventually Unregister-PsEvent "Timer.Elapsed"
(Warning: The $args parameter in event actions has changed since CTP2 based on usability feedback. Such is the risk and reward of pre-release software!)
However, doing two things at once means multithreading. And multithreading? Thar be dragons! To prevent you from having to deal with multi-threading 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.
One great use of asynchronous actions is engine events, and the Register-PsEvent cmdlet:
## Now in your profile $maximumHistoryCount = 1kb ## Register for the engine shutdown event Register-PsEvent ([System.Management.Automation.PsEngineEvent]::Exiting) -Action { Get-History -Count $maximumHistoryCount | ? { $_.CommandLine -ne "exit" } | Export-CliXml (Join-Path (Split-Path $profile) "commandHistory.clixml") } ## Load our previous history $historyFile = (Join-Path (Split-Path $profile) "commandHistory.clixml") if(Test-Path $historyFile) { Import-CliXml $historyFile | Add-History }
## Now in your profile $maximumHistoryCount = 1kb
## Register for the engine shutdown event Register-PsEvent ([System.Management.Automation.PsEngineEvent]::Exiting) -Action { Get-History -Count $maximumHistoryCount | ? { $_.CommandLine -ne "exit" } | Export-CliXml (Join-Path (Split-Path $profile) "commandHistory.clixml") }
## Load our previous history $historyFile = (Join-Path (Split-Path $profile) "commandHistory.clixml") if(Test-Path $historyFile) { Import-CliXml $historyFile | Add-History }
Ultimately, almost any event can be written in terms of Register-ObjectEvent. PowerShell is all about task-based abstractions, though, so event forwarding lets you (and ISVs) map complex event domains (such as WMI queries) to much simpler ones. The -SupportEvent parameter to the event registration cmdlets help there, as its event registrations don't show in the default views:
## Enable process creation events function Enable-ProcessCreationEvent { $query = New-Object System.Management.WqlEventQuery "__InstanceCreationEvent", ` (New-Object TimeSpan 0,0,1), ` "TargetInstance isa 'Win32_Process'" $processWatcher = New-Object System.Management.ManagementEventWatcher $query $identifier = "WMI.ProcessCreated" Register-ObjectEvent $processWatcher "EventArrived" -SupportEvent $identifier -Action { [void] (New-PsEvent "PowerShell.ProcessCreated" -Sender $args[0] -EventArguments $args[1].SourceEventArgs.NewEvent.TargetInstance) } } ## Disable process creation events function Disable-ProcessCreationEvent { Unregister-PsEvent -Force -SourceIdentifier "WMI.ProcessCreated" } ## Register for the custom "PowerShell.ProcessCreated" engine event Register-PsEvent "PowerShell.ProcessCreated" -Action { $processName = $args[1].SourceArgs[0].Name.Split(".")[0] (New-Object -COM Sapi.SPVoice).Speak("Welcome to $processName") } ## Eventually Unregister-PsEvent PowerShell.ProcessCreated
## Enable process creation events function Enable-ProcessCreationEvent { $query = New-Object System.Management.WqlEventQuery "__InstanceCreationEvent", ` (New-Object TimeSpan 0,0,1), ` "TargetInstance isa 'Win32_Process'" $processWatcher = New-Object System.Management.ManagementEventWatcher $query
$identifier = "WMI.ProcessCreated" Register-ObjectEvent $processWatcher "EventArrived" -SupportEvent $identifier -Action { [void] (New-PsEvent "PowerShell.ProcessCreated" -Sender $args[0] -EventArguments $args[1].SourceEventArgs.NewEvent.TargetInstance) } }
## Disable process creation events function Disable-ProcessCreationEvent { Unregister-PsEvent -Force -SourceIdentifier "WMI.ProcessCreated" }
## Register for the custom "PowerShell.ProcessCreated" engine event Register-PsEvent "PowerShell.ProcessCreated" -Action { $processName = $args[1].SourceArgs[0].Name.Split(".")[0] (New-Object -COM Sapi.SPVoice).Speak("Welcome to $processName") }
## Eventually Unregister-PsEvent PowerShell.ProcessCreated
When registering for an event on a remote machine, you can specify the "-Forward" parameter if you want to deliver the event to the client session connected to the remote machine. Here's an example:
Hope this helps getting started.
-- Lee Holmes [MSFT] Windows PowerShell Development Microsoft Corporation
PingBack from http://www.alvinashcraft.com/2008/06/11/dew-drop-june-11-2008/
Some very powerful techniques in there. Thanks.
I noticed that the history preservation works if you type 'exit', but not if you click the top-right X to close Powershell. Is it possible to do something similar to intercept that event too, or would that be a different kettle of fish?
Jon
i tried following your post. For some reason, Register-ObjectEvent throws an error saying that it not recognized as a cmdlet, function, etc. etc.
is there any extra library/snap-in that i need before i could use this cmdlet?
Please help me.
Hi Shaurav;
This requires CTP2 of PowerShell.
Thank you. I was not aware of the new CTP. I hvae another question, is it possible to register TFS events using what you wrote above? i want to create a script which kicks off a build as soon as a developer checks-in some code.
from James Manning on the TFS team:
Short answer:
what you're asking for is "continuous integration" and it's built-in to TFS 2008 and there are multiple ways of doing it with TFS 2005. I would imagine you'll be able to leverage existing work for this.
http://blogs.msdn.com/buckh/archive/2006/08/09/more-continuous-integration.aspx
Long answer:
TFS "events" (a bad choice on our part to overload that term) aren't .NET events, they're just notification mechanisms initiated by the TFS server (send email or call web service).
For your case, the simplest thing would probably be a powershell script that hosted a web service (using WCF or whatever), register it on the TFS server (BisSubscribe.exe or some other tool for managing the subscriptions) then when the service gets called for a checkin, kick off the build.
However, as mentioned earlier, TFS 2008 already has this built-in, so I'd recommend upgrading to it (if you're still on 2005) and using the CI we already put in the box :)
For those running CTP3 I thought I'd let you know how to update Lees's script "Event Forwarding/Supports Events". Paul expained to me how to do this in the public discussion group.
-> Enable-ProcessCreationEvent
change
$args[1].SourceEventArgs.NewEvent.TargetInstance
to
$args[1].NewEvent.TargetInstance
-> Register-PsEvent
Register-EngineEvent
in addition to this change the parameter
$args[1].SourceArgs[0].Name.Split
$args[0].Name.Split
one other note $args are scoped to the action block.
In the case of RegisterObjectEvent these are ManagementEventWatcher and EventArrivedEventArgs respectively.
In the example above case New-Event is returning a WMI query to Register-EngineEvent so this will be a ManagementBaseObject that encapsulates a WMI class. In this case the properties of Win32_Process.
Hopefully this will save some of you time and frustration
Can we do multithreading using powershell?
Like wise shooting some query strings to a set of database servers simultaneousl;y.
Another update--in the final version of Powershell v2, "PSEvent*" cmdlets have been renamed "Event*"
Is it possible to handle
events from COM objects?
e.g.
$comobj = New-Object -ComObject ...
Following command doesn't show anything. Why?
$comobj | Get-Member -Type Event
The COM object has events. In VBScript I can receive events.
@Jo: I'm looking for exactly the same thing, myself. I can do it from, say, C#, so it's possible, but I can't yet work out the Powershell voodoo necessary to do it.
I have some troubles with event forwarding.
Im using code like this:
$remoteComputer = "some_comp"
$session = New-PsSession $remoteComputer
Unregister-Event CatchEvent -ErrorAction SilentlyContinue
Invoke-Command $session {
service
$query = @"
SELECT *
FROM __instancecreationevent
WHERE TargetInstance ISA 'Win32_NtLogEvent'
and targetinstance.eventcode = '7036'
"@
Register-WmiEvent -Query $query "CatchEvent" -Forward
}
$null = Register-EngineEvent CatchEvent -Action { $GLOBAL:MyEvent = $event}
But $MyEvent.SourceEventArgs.SerializedRemoteEventA
rgs.NewEvent.TargetInstance is string, not event.
Tell me please, whats wrong with my script?
Consider the case where you have set the timer to 10 sec. Which means you have schedule to execute some action script block after every 10 seconds.
But it might happen that the action script block may take itselft more than 10 sec for execution. In that case it will create problem!
Is there any way to solve this ?