I spend my entire working day in a PowerShell prompt. It is often a combination of Windows PowerShell and PowerShell 7. Sometimes I'm in a session with a loaded profile, sometimes not. Sometimes I have a PowerShell 7 Preview session running. And then there are the scheduled jobs which also run PowerShell. Over the years, I've written a number of variations of a PowerShell script or function to discover all of the running PowerShell sessions on my computer. But I think I have finally written the final version of a PowerShell process detection tool.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Find Processes
I wanted a tool that would run in both Windows PowerShell and PowerShell 7 on a Windows platform. I also wanted to be able to discover information such as the process owner, the command used to start the process, and the parent process. Querying the Win32_Process class with Get-CimInstance was the best choice. Although, as you'll see I also still need Get-Process.
Because we always want to filter early, I'll use the -Filter parameter to get only the pwsh.exe processes.
Get-CimInstance -ClassName win32_process -filter "name = 'pwsh.exe'" |
Select-Object -property "ProcessID", "HandleCount", "CommandLine", "ParentProcessID", "CreationDate","WorkingSetSize","Name"
Remember WMI/CIM filters use the legacy operators. This gives me a number of processes.
The CommandLine property looks very useful. I also have the parent process ID. I can use Get-Process to grab that process. Even though I'm the only user on my desktop, I can also get the process owner by invoking the GetOwner() method with Invoke-CimMethod. In other words, I have the foundation of what kind of result I want. The Get-CimInstance command is the core of my eventual tool. I think it is important when building PowerShell tools that you have a working expression that you can run at a PowerShell prompt. Now I can dress it up.
Objects are Fundamental
The goal of most PowerShell commands is to write or work with objects. Certainly a "get" command, which is what I'm building, should write an object to the pipeline. My initial expression with Select-Object achieves that result. But for a script, that kind of code can get cumbersome. Especially as I start adding custom properties. Instead, I like to use code like this.
$items = Get-CimInstance -ClassName win32_process -filter "name = 'pwsh.exe'"
foreach ($item in $items) {
#get owner
$owner = Invoke-CimMethod -InputObject $item -MethodName GetOwner
$parent = Get-Process -Id $item.ParentprocessID
[PSCustomObject]@{
PSTypename = "PowerShellProcess"
ProcessID = $item.ProcessID
Name = $item.Name
Handles = $item.HandleCount
WorkingSet = $item.WorkingSetSize
ParentProcessID = $item.ParentProcessID
ParentProcess = $parent.Name
ParentPath = $parent.Path
Started = $item.CreationDate
Owner = "$($owner.Domain)\$($owner.user)"
CommandLine = $item.Commandline
}
}
I could have piped Get-CimInstance to Select-Object and used custom properties defined with hashtables, but I find the approach above easier to write, read, and debug. Each process object serves as the basis for a new custom object. I'm adding additional information The PSTypeName key defines the object type. Without it, I would have a generic PSCustomobject which isn't necessarily a bad thing. But I have plans which I'll get to momentarily. This code gives me an object result like this:
ProcessID : 11828
Name : pwsh.exe
Handles : 1410
WorkingSet : 197029888
ParentProcessID : 25596
ParentProcess : WindowsTerminal
ParentPath : C:\Program Files\WindowsAppsMicrosoft.WindowsTerminal_1.6.10571.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe
Started : 4/15/2021 10:28:24 AM
Owner : PROSPERO\Jeff
CommandLine : "C:\Program Files\PowerShell\7\pwsh.exe" -nologo
Extending the Object
I like creating commands that give me a rich result. I can't always foresee how I might use a result. I prefer not to have to go back and re-write a function. I'd have rather have too much detail than not enough. In this situation, I'm also thinking about how I might use the results and what would make it even easier. In looking at the output, I can think of a few things.
First, since I know when the process started, it is very easy to calculate a run time. But I want this to be dynamic and not statically assigned in the ForEach loop. I need a ScriptProperty. PowerShell objects can have properties whose values are calculated by a scriptblock everytime you access the property. I want that. One of the reasons I defined a type name is so that I can use Update-TypeData and define an additional property.
Update-TypeData -TypeName PowerShellProcess -MemberType ScriptProperty -MemberName Runtime -Value { (Get-Date) - $this.Started } -Force
I also would like to save some typing when accessing a few properties I know I will access often. For example, instead of having to type WorkingSet, how about an alias of WS. You see this often in PowerShell. Yes, I could have used WS in my object definition, but I prefer to use meaningful property names. If I want a shortcut, I can define an alias property.
Update-TypeData -TypeName PowerShellProcess -MemberType AliasProperty -MemberName ID -Value ProcessID -Force
Update-TypeData -TypeName PowerShellProcess -MemberType AliasProperty -MemberName WS -Value WorkingSet -Force
Update-TypeData -TypeName PowerShellProcess -MemberType AliasProperty -MemberName cli -Value Commandline -Force
Now I can run a command like this (where $p is the collection of PowerShellProcess objects.
$p | sort ParentProcess | format-table -GroupBy ParentProcess -Property ID,Name,WS,Owner,Runtime
Formatting the Results
A moment ago I mentioned I like rich objects. It is easier to not show something by default than have to rewrite code to get more information. I'm mostly happy with the raw object I'm getting now. I have a few other things that might be nice to have, but I think the code to get them is going to be a bit more complicated so I'll hold off for now. Instead, I need to focus on what I want the default output to be. I don't have to see all properties. It would be nice to see them in a more structured manner. For that I will need a custom format .ps1xml file.
I've written about this before, but using New-PSFormatXML from the PSScriptTools module makes this very easy. All I need is a sample object with all properties defined.
(Get-PSPowerShell)[0] | New-PSFormatXML -Path c:\scripts\powershellprocess.format.ps1xml -FormatType List -ViewName default
(Get-PSPowerShell)[0] | New-PSFormatXML -Path c:\scripts\powershellprocess.format.ps1xml -append -FormatType table -ViewName cmd -GroupBy Commandline -Properties ProcessID,ParentProcessID,Owner,Started,Runtime
(Get-PSPowerShell)[0] | New-PSFormatXML -Path c:\scripts\powershellprocess.format.ps1xml -append -FormatType table -ViewName parent -GroupBy ParentProcess -Properties ID,Owner,Started,Runtime -Wrap
I then edited the ps1xml file to adjust the formatting and add some customizations. For example, in the default display I thought it would be nice to highlight the current process. The function I ended up with also includes a parameter to exclude the current process. But if I do include it, I want to show it in green.
<Label>ProcessID</Label>
<ScriptBlock>
<!-- highlight the current process ID using ANSI-->
if ($host.name -match "console|code" -AND $_.processID -eq $pid) {
"$([char]27)[92m$($_.ProcessID)$([char]27)[0m"
}
else {
$_.ProcessID
}
</ScriptBlock>
I added similar code for the process owner. If the owner is someone other than me (or the person running the command, I want to show that in red.
Once the format file is complete, I can load it into my session.
Update-FormatData $PSScriptRoot\powershellprocess.format.ps1xml
The first view in the file, will be the default. This is a screen shot from a few days ago.
But I also knew I would want to view the results in a few ways. That's why I created additional views. The additional views in the format file are tables. The first view is the default when piping to Format-Table.
You can see the "other" process owner highlighted in red. I also added second table view named "cmd".
Want to Try?
The end result is that I know have a command which provides rich detail. Yet, I can manage what I see and how I see it with custom type and format extensions. It isn't difficult to add these extensions and they add tremendous value to your work. Think of them as "force multipliers".
I've posted the function and format ps1xml as a GitHub gist. Save both files to the same directory. The ps1 file will load the format file so make sure you keep the file names or edit accordingly. Dot source the .ps1 file and then you are ready to go running Get-PSPowerShell or its alias gpsp. The command has comment-based help with examples. I've added comments throughout the script file to explain what I am doing and why.
Even if you don't have a need for this function, I hope you'll pay attention to the PowerShell scripting patterns and techniques. If you are going to take the time to create a PowerShell tool, you might as well make it the best it can be.