Process Monitoring with PowerShell

If you’re like me, you run a lot of applications on your desktop. If you’re also like me, you let applications run for days at a time. There are consequences to letting applications run indefinitely, as some apps like to chip away at memory. Browsers are a perfect example of this, where I have to remind myself to restart my browser every few days because before I realize it, the application could be using almost a 1GB of memory. But this article isn’t about picking the best browser, it’s about how I decided to use PowerShell to keep track of applications using a lot of memory. To do this, I’m going to create a WMI subscriber using the CIM cmdlets. I used the same ideas to create my battery watcher tool.

A WMI subscriber is a special type of query that’s semi-persistent. You can configure the query to check for changes to the system every X number of seconds. This is referred to as the polling interval.

In this example, I’m going to check my computer every two minutes.

$Poll = 120

The query is a bit different than what you would normally see with WMI.

$query = "Select * from CIM_InstModification within $Poll where TargetInstance ISA 'Win32_Process' AND TargetInstance.WorkingSetSize>=$(500MB)"

Let’s break it down. I want to select all properties from the WMI class called CIM_InstModification. This is a special system class that’s triggered when an object is modified. I don’t want to find every object, as it is changed every second. So I tell WMI to check within a time frame of seconds. When this event fires, WMI will have a new object called the TargetInstance. This will be the changed object, and I only care about those that are Win32_Process objects.The ISA operator accomplishes that.
The last part of the query is to limit results to those process objects, which is what the TargetInstance is, with a WorkingSetSize property of greater or equal to 500 MB. I’m using a subexpression,$(500MB), so that PowerShell will expand the expression and plug in the value. Otherwise, I would have to know that 500 MB is 524288000 bytes. I’m using this value for the sake of my demonstration. In practice, I’ll bump this to 1 GB.
When you create the event subscriber, you can choose to simply record the events in your PowerShell session as matching events are detected. Or you can take action. In my case, I want to do a few things everytime a matching process is found. I want to create a log file, and I want to display a popup message.

$action={
#create a log file
$logPath= "C:\Work\HighMemLog.txt"
"[$(Get-Date)] Computername = $($Event.SourceEventArgs.NewEvent.SourceInstance.CSName)" | Out-File -FilePath $logPath -Append -Encoding ascii
"[$(Get-Date)] Process = $($Event.SourceEventArgs.NewEvent.SourceInstance.Name)" | Out-File -FilePath $logPath -Append -Encoding ascii
"[$(Get-Date)] Command = $($Event.SourceEventArgs.NewEvent.SourceInstance.Commandline)" | Out-File -FilePath $logPath -Append -Encoding ascii
"[$(Get-Date)] PID = $($Event.SourceEventArgs.NewEvent.SourceInstance.ProcessID)" | Out-File -FilePath $logPath -Append -Encoding ascii
"[$(Get-Date)] WS(MB) = $([math]::Round($Event.SourceEventArgs.NewEvent.SourceInstance.WorkingSetSize/1MB,2))" | Out-File -FilePath $logPath -Append -Encoding ascii
"[$(Get-Date)] $('*' * 60)" | Out-File -FilePath $logPath -Append -Encoding ascii
#create a popup
$wsh = New-Object -ComObject Wscript.shell
$Title = "$(Get-Date) High Memory Alert"
$msg = @"
Process = $($Event.SourceEventArgs.NewEvent.SourceInstance.Name)
PID = $($Event.SourceEventArgs.NewEvent.SourceInstance.ProcessID)
WS(MB) = $([math]::Round($Event.SourceEventArgs.NewEvent.SourceInstance.WorkingSetSize/1MB,2))
"@
#timeout in seconds. Use -1 to require a user to click OK.
$Timeout = 10
$wsh.Popup($msg,$TimeOut,$Title,16+32)
}

When the event occurs, I want this scriptblock to execute. The first part creates a log file. When the event fires, you will automatically get a few objects, which will represent the matching object.
The $Event.SourceEventArgs.NewEvent.SourceInstance will be the WMI Win32_Process object. As you can see, I’m selecting a few properties and writing the result to my log file. I’m including a time stamp and a string of 60 characters at the end to separate the entries.

Viewing the triggered event log file (Image Credit: Jeff Hicks)
Viewing the triggered event log file (Image Credit: Jeff Hicks)

Right now I only have one process that meets my filter. If there were multiple processes that matched the filter, then the action scriptblock would run for each one.
The other thing I’m doing is using the Wscript.Shell COM object, like we used in VBScript to display a popup message.
A pop-up alert (Image Credit: Jeff Hicks)
A pop-up alert (Image Credit: Jeff Hicks)

An advantage to this approach is that I can set an automatic timeout value, which I set to 10 seconds. Now that I have the action scriptblock all that remains is to register the subscription with the Register-CimIndicationEvent.

Register-CimIndicationEvent -Query $query -SourceIdentifier "HighProcessMemory" -Action $action

You can see the registration with the Get-EventSubscriber cmdlet.

Using PowerShell's Get-EventSubscriber cmdlet (Image Credit: Jeff Hicks)
Using PowerShell’s Get-EventSubscriber cmdlet (Image Credit: Jeff Hicks)

This subscription will run for as long as my PowerShell session is running. The corollary is that I will need to recreate it every time I want to start monitoring. If this is a daily task, I could put it in my PowerShell profile script. If you want to get rid of the subscriber, simply unregister it.

Get-EventSubscriber -SourceIdentifier "HighProcessMemory" | Unregister-Event


One final note, I’m running this locally, but you can just as easily create event subscriptions for things happening on remote machines. You might want to watch process utilization on a critical server. If there’s something you think you could use these techniques would be helpful, let me know, and it might become a future article.