Improving Get-Win32Process
In this issue:
- Filtering
- Using CIMSessions
- Adding a StopWatch
- Using the Information Stream
- Get-Win32Process Revised
- Summary
A few issues ago I started sharing my solution to the July 2026 PowerShell scripting challenge. I asked you to write a PowerShell function or script that used Get-CimInstance to query the Win32_Process class as a replacement for Get-Service. In PowerShell 7, Get-Service eliminated the -Computername parameter. As an exercise, I wanted to see what you could come up with using Get-CimInstance.
When we left this topic last, I showed you my Get-Win32Process function. As I was preparing to write about type and format extensions, I realized there were a few changes I could make to improve the function which makes for a great learning opportunity. That's why I offer these scripting challenges. So let's revisit my function and see what we can learn.
Filtering
My original function included CIM filtering. I showed you how the function filtered on a process name or process ID. Then, I realized a user, or me, might want to filter processes further such as by WorkingSetSize. I could just as easily use Where-Object.
PS C:\> Get-Win32Process | where WorkingSetSize -ge 500MB
ProcessId Name HandleCount WorkingSetSize VirtualSize PSComputerName
--------- ---- ----------- -------------- ----------- --------------
4360 Memory Compression 0 575221760 759955456 PROSPERO
15320 explorer.exe 6719 526581760 2205510574080 PROSPERO
26632 brave.exe 539 595791872 3886698864640 PROSPERO
29436 Dropbox.exe 7325 600989696 3695531700224 PROSPERO
20616 pwsh.exe 2098 642777088 2342485958656 PROSPERO
This is an example of late filtering. There's nothing wrong with this approach and sometimes it is the only solution. But if there is an option for early filtering, you should use it. The rule of thumb is to filter as early in your expression as possible. You may have also heard this described as filter left.
My function is technically already filtering with Get-CimInstance. What I need to add is a more general filter parameter and append it to my existing name or ID filter. Adding the parameter is simple enough.
[Parameter(
ParameterSetName = 'Name',
HelpMessage = 'Specify an additional filter using legacy operators like WorkingSetSize >= $(250MB)'
)]
[ValidateNotNullOrEmpty()]
[string]$Filter
> Remember that CIM/WMI filters use the legacy operators.
My original function has two parameter sets. One to find processes by name and one to find it by process ID. The latter will only return a single object so there's no need for additional filtering. I'll only want that when querying by name. This means I need to make sure the new parameter only belongs to the Name parameter set.
After loading the update function into my session I can verify this.
PS C:\> Get-Command Get-Win32Process -Syntax
Get-Win32Process [[-Name] <string>] [-IncludeUserName] [-Filter <string>] [-Computername <string>] [<commonparameters>]
Get-Win32Process -ID <int> [-IncludeUserName] [-Computername <string>] [<commonparameters>]
I still need to use the parameter. My code already defines a default filter, "Name LIKE '$name'". If there is an additional filter, I'll append it using the legacy AND operator.
switch ($PSCmdlet.ParameterSetName) {
'Name' {
$Name = $Name.Replace('*', '%')
#capitalizing the WQL operators so that they stands out
[string]$defaultFilter = "Name LIKE '$name'"
if ($filter) {
$defaultFilter += " AND $filter" #<-- note the beginning space
}
$cimParams['Filter'] = $defaultFilter
}
'ID' {
$cimParams['Filter'] = "ProcessID = $id"
}
}
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Using filter: WHERE $($cimParams.Filter)"
The subtle and critical change, which is easy to miss, is that I need a space when joining the filters together. Showing the filter in the verbose messaging also helps me identify potential syntax issues.
Now I can run the command with a more complex filter.
PS C:\> $r = Get-win32Process -filter "WorkingSetSize>=$(500MB)" -Verbose
VERBOSE: [16:37:49.2944633 BEGIN ] Starting Get-Win32Process v1.1.0
VERBOSE: [16:37:49.2950480 BEGIN ] Running under PowerShell v7.6.5
VERBOSE: [16:37:49.2953104 PROCESS] Using parameter set Name
VERBOSE: [16:37:49.2966046 PROCESS] Using filter: WHERE Name LIKE '%' AND WorkingSetSize>=524288000
VERBOSE: Perform operation 'Query CimInstances' with following parameters, ''queryExpression' = SELECT * FROM Win32_Process WHERE Name LIKE '%' AND WorkingSetSize>=524288000,'queryDialect' = WQL,'namespaceName' = root\cimv2'.
VERBOSE: Operation 'Query CimInstances' complete.
VERBOSE: [16:37:49.4520701 PROCESS] Found 5 matching processes
VERBOSE: [16:37:49.4528798 END ] Ending Get-Win32Process
My initial command using late filtering took 255ms. Using early filtering cut that down to 169ms. As I query multiple machines and remote machines, this difference will pay off. Especially if I take the extra step of getting the user name for each process.
One other thing I want to point out in my example is that I placed the 500MB value in a sub-expression. If I ran Get-win32Process -filter "WorkingSetSize>=500MB", PowerShell would treat 500MB as a string which obviously is useless. Putting it in a sub-expression means that PowerShell will "resolve" 500MB to an integer and plug that into the filter.
DateTime Values
Most of the properties you would want to filter are numeric so those should be rather straightforward. However, the CreationDate property is returned as a DateTimeValue. This is a bit more challenging to filter on.
Commands like this will fail.
PS C:\> $t = (Get-Date).AddMinutes(-30)
PS C:\> $r = Get-Win32Process -filter "CreationDate>=$t"
PS C:\> $r
Get-CimInstance:
Line |
85 | $get = Get-CimInstance @cimParams
| ~~~~~~~~~~~~~~~~~~~~~~~~~~
| The WS-Management service cannot process the request. The WQL query is invalid.
My goal is to find all processes started in the last 30 minutes. The problem is that the WQL query is expecting a string. Turning $t to string isn't enough. Even though we are using CIM cmdlets, they are still querying the legacy WMI repository on the backend. I like that the CIM cmdlets automatically convert datetime properties to DateTime objects. But that won't work in a filter.
For that, we need to return to WMI and convert the datetime object to a DMTF datetime string. In Windows PowerShell we could use the built-in ConvertFromDateTime and ConvertToDateTime functions on WMI objects. CIM instances lack these methods, but we can still access them even in PowerShell 7. We can invoke the same .NET method that the legacy convert methods were using:
$tt = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($t)
Now $t is a DMTF string, 20260826161841.220466-240, which I can use in a filter.
PS C:\> $r = Get-win32Process -filter "CreationDate>='$tt'" -Verbose
VERBOSE: [16:59:05.6971808 BEGIN ] Starting Get-Win32Process v1.1.0
VERBOSE: [16:59:05.6974793 BEGIN ] Running under PowerShell v7.6.5
VERBOSE: [16:59:05.6977121 PROCESS] Using parameter set Name
VERBOSE: [16:59:05.6981944 PROCESS] Using filter: WHERE Name LIKE '%' AND CreationDate>='20260826161841.220466-240'
VERBOSE: Perform operation 'Query CimInstances' with following parameters, ''queryExpression' = SELECT * FROM Win32_Process WHERE Name LIKE '%' AND CreationDate>='20260826161841.220466-240','queryDialect' = WQL,'namespaceName' = root\cimv2'.
VERBOSE: Operation 'Query CimInstances' complete.
VERBOSE: [16:59:05.8625887 PROCESS] Found 3 matching processes
VERBOSE: [16:59:05.8632738 END ] Ending Get-Win32Process
> Don't forget to wrap the string in single quotes.
You should be able to use this technique to filter on any WMI class that has a datetime property using Get-CimInstance.