PowerShell and Events: Object Events

Continuing in my little series on PowerShell and different types of events, I will go into working with Object events and how you can use those with the Register-ObjectEvent cmdlet.

Because the parameters are the same as Register-EngineEvent, I won’t re-hash what each parameter does, instead check out this article to learn about some of the parameters that you can use for Support events and Forwarding events.

An object event is a .Net object that not only has the usual Properties and Methods in the object, but also has another member called Event,which you can register a subscription on using Register-EngineEvent that will fire every single time that the event happens. With a wide range of .Net objects out there, there will certainly be something that has an event that you will find useful!

Finding Events

So how do I know if a .Net object has an event or events associated with it? It can be done a number of ways.

Save the object as a variable and use Get-Member

$web = New-Object System.Net.WebClient
$web | gm -type Event | Select Name

image

Or use the .GetEvents() method from the object type itself.

([Microsoft.Win32.SystemEvents]).GetEvents() | Select Name

image

With one of these approaches, you will quickly be able to find out if the object has events that you can subscribe to.

Enough about that, it’s time for some demos!

Observable Collection

The first example shows how you can use the observable collection object that supplies an event called CollectionChanged which fires off whenever an item is added, removed or the collection is cleared out.

#region Create an observable collection that only accepts integers
$observableCollection = New-Object System.Collections.ObjectModel.ObservableCollection[object]

#Set up an event watcher
Register-ObjectEvent -InputObject $observableCollection -EventName CollectionChanged -Action {
    $Global:test = $Event
    Switch ($test.SourceEventArgs.Action) {
        "Add" {
            $test.SourceEventArgs.NewItems | ForEach {
                Write-Host ("{0} was added" -f $_) -ForegroundColor Yellow -BackgroundColor Black
            }
        }
        "Remove" {
            $test.SourceEventArgs.OldItems | ForEach {
                Write-Host ("{0} was removed" -f $_) -ForegroundColor Yellow -BackgroundColor Black
            }
        }
        Default {
            Write-Host ("The following action occurred: {0}" -f $test.SourceEventArgs.Action) -ForegroundColor Yellow -BackgroundColor Black
        }
    }
}
$observableCollection.Add(5) 
$observableCollection.Remove(5) 
$observableCollection.Clear()
#endregion

image

I went through the motions of adding, removing and clearing out the collection. Each time this happened, the event subscription fired off and reported what happened. This collection also happens to work great with a ListView in a UI as shown in this article.

When I am done with each of these examples, I want to make sure to remove the subscriptions and associated background jobs.

Get-EventSubscriber | Unregister-Event
Get-Job | Remove-Job -Force

Timer Object

One of the most common object events that I see used is a Timer object that performs an action for each and every tick based on the Interval set. This continues on until you disable the timer. Pretty nice as a monitoring tool to check a process or even a script that is running.

#Timer Event
$timer = New-Object timers.timer
# 1 second interval
$timer.Interval = 1000
#Create the event subscription
Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Timer.Output -Action {
    Write-Host "1 second has passed"
}
$timer.Enabled = $True

image

#Let run for a few seconds and then stop it
$timer.Enabled = $False

Pretty simple stuff here. I created a timer object and set the interval to 1000 milliseconds (1 second) and at every interval tick, it performs an action. In this case, the action is just a simple display to the console. Nothing spectacular, but gives you an idea about where you could run with this. If you only wanted to track the first 20 times that this action occurs, you can use the –MaxTriggerCount parameter to only allow the event subscription to run the action block the specified number of times. Note that after you reach the set number, the event does not unsubscribe itself; this must still be done by you or another automated approach.

Monitor a Background Job

Background jobs are used very frequently by everyone to handle tasks that are long running and would otherwise take up valuable console time. The problem with this is that you don’t know when the job finishes without having to continuously check on it manually with Get-Job. Luckily, there is an event associated with each PSJob object called StateChanged that can easily be subscribed to and alert you when the job state changes, hopefully for a finished job without issues.

#region Background job monitor
$job = Start-Job -Name TestJob -ScriptBlock {Start-Sleep -Seconds 5}
Register-ObjectEvent -InputObject $job -EventName StateChanged -MessageData $job.Id -SourceIdentifier Job.Monitor -Action {
    $Global:t = $event
    $voice = New-Object -com SAPI.SpVoice
    $voice.Rate = -5
    Write-Host ("Job ID {0} has changed from {1} to {2}" -f 
    $t.sender.id,$t.SourceEventArgs.PreviousJobStateInfo.State,$t.SourceEventArgs.JobStateInfo.state) -ForegroundColor Green -BackgroundColor Black
    $voice.Speak(("Job ID {0} has changed from {1} to {2}" -f 
    $t.sender.id,$t.SourceEventArgs.PreviousJobStateInfo.State,$t.SourceEventArgs.JobStateInfo.state))
}
#endregion Background job monitor

image

While you can see the message, there is also an audio cue in the form of a voice notification saying what you are seeing on the console. This is done using the SAPI.SpVoice COM object. Note that there is another way to use a voice with the .Net namespace: System.Speech.Synthesis.

Either way, this provides an excellent to track your background job’s status and alerts you when finished. If using multiple jobs, you will need to subscribe to each jobs state to ensure that you are alerted when each job finishes. For a more complex approach to this, check out my Hey, Scripting Guy! article I wrote that used this method to track a system as it was being rebooted.

Track the Progress of a File Download

Downloading a file with PowerShell can easily be done using PowerShell. What can be a little more difficult is tracking the progress of that download. Fortunately, we can solve this using 2 events from System.Net.WebClient: DownloadProgressChanged and DownloadFileCompleted.

#region Download file from website
$web = New-Object System.Net.WebClient
$web.UseDefaultCredentials = $True
$url = "http://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.0-KB2506146-x64.msu"
$Index = $url.LastIndexOf("/")
$file = $url.Substring($Index+1)
$newurl = $url.Substring(0,$index)
Register-ObjectEvent -InputObject $web -EventName DownloadFileCompleted `
-SourceIdentifier Web.DownloadFileCompleted -Action {    
    $Global:isDownloaded = $True
}
Register-ObjectEvent -InputObject $web -EventName DownloadProgressChanged `
-SourceIdentifier Web.DownloadProgressChanged -Action {
    $Global:Data = $event
}
$web.DownloadFileAsync($url,("C:\users\Administrator\desktop\{0}" -f $file))
While (-Not $isDownloaded) {
    $percent = $Global:Data.SourceArgs.ProgressPercentage
    $totalBytes = $Global:Data.SourceArgs.TotalBytesToReceive
    $receivedBytes = $Global:Data.SourceArgs.BytesReceived
    If ($percent -ne $null) {
        Write-Progress -Activity ("Downloading {0} from {1}" -f $file,$newurl) `
        -Status ("{0} bytes \ {1} bytes" -f $receivedBytes,$totalBytes)  -PercentComplete $percent
    }
}
Write-Progress -Activity ("Downloading {0} from {1}" -f $file,$newurl) `
-Status ("{0} bytes \ {1} bytes" -f $receivedBytes,$totalBytes)  -Completed
#endregion Download file from website

image

Setting up a subscription for both of the events allows this to work effectively. By monitoring the DownloadProgressChanged event we can figure out how much of the total file has been downloaded, which plays well into using a progress bar to track this change. The DownloadFileCompleted helps us to determine when the download has completed (obviously) and also to close out the progress bar.

Tracking File and Folder Changes

Another use of subscribing to events is by harnessing the System.IO.FileSystemWatcher to basically watch a specific location for the creation, deletion and modification of a file. Great if you want to track a specific location for any of the mentioned actions.

#region Filesystem Watcher
$fileWatcher = New-Object System.IO.FileSystemWatcher
$fileWatcher.Path = "C:\users\Administrator\desktop"
Register-ObjectEvent -InputObject $fileWatcher -EventName Created -SourceIdentifier File.Created -Action {
    $Global:t = $event
    Write-Host ("File Created: {0} on {1}" -f $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath))
} | Out-Null
Register-ObjectEvent -InputObject $fileWatcher -EventName Deleted -SourceIdentifier File.Deleted -Action {
    $Global:t = $event
    Write-Host ("File Deleted: {0} on {1}" -f $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath))
} | Out-Null
Register-ObjectEvent -InputObject $fileWatcher -EventName Changed -SourceIdentifier File.Changed -Action {
    $Global:t = $event
    Write-Host ("File Changed: {0} on {1}" -f $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath))
} | Out-Null
#endregion Filesystem Watcher

image

The only issues with this is that the event cannot tell you who has made the changes to the folder location that is being monitored. But it still provides a great reporting mechanism to audit a folder location and at least alert if a file has been modified.

Monitor System Event for a Time Change

You can also monitor specific system generated events for a number of things. In this case, I am going to track when the time changes on my system.

#region Local System
$sysEvent = [Microsoft.Win32.SystemEvents]
Register-ObjectEvent -InputObject $sysEvent -EventName TimeChanged -Action {
    Write-Host ("time changed to {0}" -f $event.TimeGenerated)
    $Global:session = $event
}
#endregion Local System

 

image

There are a number of other system events that you can supply to monitor from the system. This would be a nice use for tracking a session ending event such as a server being rebooted using the –Forward parameter on the remote system so it can send the events to the event queue on the local system. Of course, this requires you to use PowerShell remoting to make this happen and to keep the the session active in order to receive the event.

These are just a few of the possible examples of using Register-ObjectEvent to subscribe to various events that are generated by .Net objects. Also do not forget about using the –Support parameter to hide the subscription and use for a more complex event subscription as well as the –Forward parameter to run an event subscription from either a background job or a remote system.

This entry was posted in powershell, scripts and tagged , , , . Bookmark the permalink.