A proxy function or wrapper function allows you to deploy a custom version of a function or cmdlet while the internal functionality remains implemented. That means you can get a core cmdlet or function, add parameters (or remove), and write the code for these parameters, customizing a new version of this core cmdlet or function. Also, we get the begin, process and end block exposed from the PowerShell advanced functions, allowing control over the command steppable pipeline.
The SteppablePipeline allows you to implement a cmdlet (advanced function) by delegating most of the implementation to another cmdlet in a memory-efficient, streaming manner.
From this stackoverflow post by mklement0: “Specifically, a steppable pipeline allows you to delegate the implementation of your proxy function to a script block whose life cycle is kept in sync with the proxy function itself, in terms of initialization (begin block), per-object pipeline input processing (process block), and termination (end block), which means that the single instantiation of the wrapped cmdlet is in effect directly connected to the same pipeline as the proxy function itself.”
Let´s create a scenario with SQL Server. If you use xp_cmdshell
to call the notepad executable, your session will be hanging until you kill the process in the OS, which can be done with Task Manager. Execute the following if you want to try it:
1 |
xp_cmdshell 'NOTEPAD' |
You will see something similar to:
The app will not open a window on your machine, but if you go to task manager you will see it, and you can kill it.
Or, if you want to see the complete chain using Process Explorer:
But using PowerShell, Get-Process
:
1 |
Get-Process |
It´s not listed:
It would be fantastic if we could get the process chain in the Get-Process
return values, so we will add a new parameter called ShowChain
to Get-Process
to extend its functionality.
Note: the final code is very long, and the snippets are available in the article for browsing and understanding the process. You can download the final working code (in a .txt file) from the Simple Talk site here.
FIRST STEP – GET THE METADATA FROM GET-PROCESS
The first step is to get the metadata from Get-Process and saved to a file called Get-Process.txt in the temp folder to check how it works:
1 2 3 4 |
$Cmd = Get-Command Get-Process $CmdMetadata = New-Object System.Management.Automation.CommandMetaData $Cmd [System.Management.Automation.ProxyCommand]::Create($CmdMetadata) | Out-File C:\temp\Get-Process.txt |
The output file will be the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
[CmdletBinding(DefaultParameterSetName='Name', HelpUri='https://go.microsoft.com/fwlink/?LinkID=113324', RemotingCapability='SupportedByCommand')] param( [Parameter(ParameterSetName='NameWithUserName', Position=0, ValueFromPipelineByPropertyName=$true)] [Parameter(ParameterSetName='Name', Position=0, ValueFromPipelineByPropertyName=$true)] [Alias('ProcessName')] [ValidateNotNullOrEmpty()] [string[]] ${Name}, [Parameter(ParameterSetName='IdWithUserName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [Parameter(ParameterSetName='Id', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [Alias('PID')] [int[]] ${Id}, [Parameter(ParameterSetName='InputObjectWithUserName', Mandatory=$true, ValueFromPipeline=$true)] [Parameter(ParameterSetName='InputObject', Mandatory=$true, ValueFromPipeline=$true)] [System.Diagnostics.Process[]] ${InputObject}, [Parameter(ParameterSetName='NameWithUserName', Mandatory=$true)] [Parameter(ParameterSetName='IdWithUserName', Mandatory=$true)] [Parameter(ParameterSetName='InputObjectWithUserName', Mandatory=$true)] [switch] ${IncludeUserName}, [Parameter(ParameterSetName='Id', ValueFromPipelineByPropertyName=$true)] [Parameter(ParameterSetName='Name', ValueFromPipelineByPropertyName=$true)] [Parameter(ParameterSetName='InputObject', ValueFromPipelineByPropertyName=$true)] [Alias('Cn')] [ValidateNotNullOrEmpty()] [string[]] ${ComputerName}, [Parameter(ParameterSetName='Id')] [Parameter(ParameterSetName='InputObject')] [Parameter(ParameterSetName='Name')] [ValidateNotNull()] [switch] ${Module}, [Parameter(ParameterSetName='InputObject')] [Parameter(ParameterSetName='Id')] [Parameter(ParameterSetName='Name')] [Alias('FV','FVI')] [ValidateNotNull()] [switch] ${FileVersionInfo}) begin { try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-Process', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = {& $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } <# .ForwardHelpTargetName Microsoft.PowerShell.Management\Get-Process .ForwardHelpCategory Cmdlet #> |
Using the words of my good friend Shay Levy :
“Don’t get intimidated by the result. This is what a proxy function looks like. As you can see, the generated code starts by including all the cmdlet parameters and then defines the script blocks of the command. At the end, there are instructions for the Get-Help cmdlet.
In the Begin block, the Get-Process command ($wrappedCmd) is retrieved, and the parameters are passed to it (@PSBoundParameters). A steppable pipeline is initialized ($steppablePipeline), which invokes the cmdlet ($scriptCmd), and finally, the Begin starts. At the end of the code sample, you can find a multiline comment that is used to instruct the Help that Get-Help should display when you type Get-Help <ProxyFunction>.”
In this script, add the following:
1 |
Function Get-Process {</code> <code>} |
Our new cmdlet will have the same name of the core cmdlet Get-Process
. It´s not a problem as we will see later in this article. Now it’s time to create the Function that will retrieve the Process Chain :
SECOND STEP – CREATE THE PARAMETER
The next step is create the switch parameter ShowChain:
1 2 3 |
[ValidateNotNull()] [switch] ${ShowChain}) |
It is inserted the code thusly:
THIRD STEP – CREATE THE FUNCTION WITH THE PROCESS CHAIN.
The function gets the ProcesseId
column from Get-Process
and query the WMI win32_process
to get the child process because Get-Process
does not return the ParentID
column. Then it creates a loop to find the complete chain showing in the yellow color and with ‘-‘to hierarchical show.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
function Get-ProcessChain { [cmdletbinding()] Param( [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName = $true)] [System.Diagnostics.Process]$ProcessObject ) Begin { $DefaultFColor = $Host.UI.RawUI.ForegroundColor $ArrayContains = @() } process { if ($ArrayContains -notcontains $ProcessObject.ID ) { $ProcessObject if ($ProcessObject.name -ne ‘services’) { $ComputerName = $ProcessObject.MachineName $Beforecaption = ‘-‘ $ProcessMotherID = $ProcessObject.ID do { $Hasharguments = @{ Filter = “ParentProcessId='$($ProcessMotherID)'” Property = "ProcessID,Name" ErrorAction = "ProcessID,Name" ComputerName = $ComputerName } $ProcessChild = Get-WmiObject win32_process @Hasharguments if ($ProcessChild ) { $Beforecaption += ‘-‘ $Host.UI.RawUI.ForegroundColor = “Yellow” $ProcessChild | ForEach-Object { Get-Process -Id $_.Processid -ComputerName $ComputerName -ErrorAction SilentlyContinue | Add-Member -MemberType NoteProperty -name ProcessName -Value “$Beforecaption $($_.Name)” -Force -PassThru $ProcessMotherID = $_.Processid $ArrayContains+= $_.Processid } } else { $Beforecaption = ‘-‘ Break } } while($true) } $Host.UI.RawUI.ForegroundColor = $DefaultFColor } } } |
Using the same names
The Proxy Function using the same name of the core cmdlet is not a problem because the function takes precedence in the running, also having the same name, we have the capability of the command discovery process. Here´s the list of precedence :
- · Alias: All Windows PowerShell aliases in the current session
- · Filter/Function: All Windows PowerShell functions
- · Cmdlet: The cmdlets in the current session (“Cmdlet” is the default)
- · ExternalScript: All .ps1 files in the paths that are listed in the Path environment variable ($env:PATH)
- · Application: All non-Windows-PowerShell files in paths that are listed in the Path environment variable
- · Script: Script blocks in the current session
The magic happens in the begin block of the function where I check if the ShowChain
parameter was passed, remove it from the $psboundparameters
because this parameter does not exist in the core Get-Process
cmdlet. If I don’t remove it will have an error when piping the function Get-ProcessChain:
1 2 3 4 5 |
if ($PSBoundParameters[‘ShowChain’]) { [Void]$PSBoundParameters.Remove(“ShowChain”) $scriptCmd = {& $wrappedCmd @PSBoundParameters | Get-ProcessChain} } else { $scriptCmd = {& $wrappedCmd @PSBoundParameters} |
It is finished, and the final code follows:
Reminder: this code is available (in a .txt file), from the Simple Talk site here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
Function Get-Process { [CmdletBinding(DefaultParameterSetName=‘Name’, HelpUri=‘http://go.microsoft.com/fwlink/?LinkID=113324’' , RemotingCapability=‘SupportedByCommand’)] param( [Parameter(ParameterSetName=‘Name’, Position=0, ValueFromPipelineByPropertyName=$true)] [Alias(‘ProcessName’)] [ValidateNotNullOrEmpty()] [string[]] ${Name}, [Parameter(ParameterSetName=‘Id’, Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [Alias(‘PID’)] [int[]] ${Id}, [Parameter(ValueFromPipelineByPropertyName=$true)] [Alias(‘Cn’)] [ValidateNotNullOrEmpty()] [string[]] ${ComputerName}, [ValidateNotNull()] [switch] ${Module}, [Alias(‘FV’,‘FVI’)] [ValidateNotNull()] [switch] ${FileVersionInfo}, [Parameter(ParameterSetName=‘InputObject’, Mandatory=$true, ValueFromPipeline=$true)] [System.Diagnostics.Process[]] ${InputObject}, [ValidateNotNull()] [switch] ${ShowChain}) begin { function Get-ProcessChain { [cmdletbinding()] Param( [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName = $true)] [System.Diagnostics.Process]$ProcessObject ) Begin { $DefaultFColor = $Host.UI.RawUI.ForegroundColor $ArrayContains = @() } process { if ($ArrayContains -notcontains $ProcessObject.ID ) { $ProcessObject if ($ProcessObject.name -ne ‘services’) { $ComputerName = $ProcessObject.MachineName $Beforecaption = ‘-‘ $ProcessMotherID = $ProcessObject.ID do { $Hasharguments = @{ Filter = “ParentProcessId='$($ProcessMotherID)'” Property = "ProcessID,Name" ErrorAction = "SilentlyContinue" ComputerName = $ComputerName } $ProcessChild = Get-WmiObject win32_process @Hasharguments if ($ProcessChild ) { $Beforecaption += ‘-‘ $Host.UI.RawUI.ForegroundColor = “Yellow” $ProcessChild | ForEach-Object { Get-Process -Id $_.Processid -ComputerName $ComputerName -ErrorAction SilentlyContinue | Add-Member -MemberType NoteProperty -name ProcessName -Value “$Beforecaption $($_.Name)” -Force -PassThru $ProcessMotherID = $_.Processid $ArrayContains+= $_.Processid } } else { $Beforecaption = ‘-‘ Break } } while($true) $Host.UI.RawUI.ForegroundColor = $DefaultFColor } } } } try { $outBuffer = $null if ($PSBoundParameters.TryGetValue(‘OutBuffer’, [ref]$outBuffer)) { $PSBoundParameters[‘OutBuffer’] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand(‘Get-Process’, [System.Management.Automation.CommandTypes]::Cmdlet) if ($PSBoundParameters[‘ShowChain’]) { [Void]$PSBoundParameters.Remove(“ShowChain”) $scriptCmd = {& $wrappedCmd @PSBoundParameters | Get-ProcessChain} } else { $scriptCmd = {& $wrappedCmd @PSBoundParameters} } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } } <# .ForwardHelpTargetName Get-Process .ForwardHelpCategory Cmdlet #> |
Then, our Proxy Function is ready to use. To load it into memory we can dot-sourcing
1 2 |
. C:\temp\get-process.ps1 Get-Process -ShowChain |
Or load it into your profile. I have in my profile a module called Functions
where I store all the functions that I use in my day-to-day
And calling the function:
1 |
Get-Process -ShowChain |
You will now see the subordinate processes:
And calling only the SQL Server process the process chain will be visible
1 |
Get-Process sqlservr -ShowChain |
Which will cause the following to be shown (if you have not killed the notepad
instance from the start of the article):
And killing the process 10284, which in my connection, corresponds to Notepad the SQL Server session:
Causes the xp_cmdshell
to return with a NULL
output:
Proxy functions are very useful if you want to customize the core cmdlets instead of creating a new hoke function for that.
Note: Do not use PowerShell ISE. It messes with the colors, instead of dot-source (as we saw before), the function and calls in the PowerShell console
References
Proxy Functions: Spice Up Your PowerShell Core Cmdlets (Shay Levy)
Load comments