Using PowerShell and PsExec to invoke expressions on remote computers
While eagerly awaiting PowerShell’s upcoming remoting functionality, many people turn to Sysinternals’ PsExec tool to build their own version. However, PowerShell seems to hang when called via PsExec on the remote machine. This has come up on the SysInternal forum (http://forum.sysinternals.com/forum_posts.asp?TID=10823) among other places, and is caused by the same issue outlined here: http://www.leeholmes.com/blog/UsingMshexeInteractivelyFromWithinOtherPrograms.aspx.
To work around this problem, you can give some input to the Powershell process. But to give it input, you need to use cmd.exe:
psexec \\server cmd /c "echo . | powershell dir 'c:\program files'"
Now, working around quote encoding and two levels of escape characters (cmd.exe and PowerShell) can be quite painful when crafting the PowerShell command this way. For that, you can use the –EncodedCommand parameter, which accepts a Base64-encoded version of your command.
$expression = "dir 'c:\program files'"
$commandBytes = [System.Text.Encoding]::Unicode.GetBytes($expression)
$encodedCommand = [Convert]::ToBase64String($commandBytes)
psexec \\server cmd /c "echo . | powershell -EncodedCommand $encodedCommand"
And to make it even more PowerShelly, the –OutputFormat of XML lets you get back an XML representation of your command’s output. On your local system, PowerShell converts your output back to deserialized objects. From there, you can continue to manipulate the output with the object-oriented goodness you’ve come to expect of us J In the example below, the server processes the Where-Object query, but the client sorts the result on Handles.
PS >$command = { Get-Process | Where-Object { $_.Handles -gt 1000 } }
PS >Invoke-RemoteExpression \\LEE-DESK $command | Sort Handles
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
1025 8 3780 3772 32 134.42 848 csrss
1306 37 50364 64160 322 409.23 4012 OUTLOOK
1813 39 54764 36360 321 340.45 1452 iTunes
2316 273 29168 41164 218 134.09 1244 svchost
Here’s a script that automates all of this for you:
Program: Start a Process on a Remote Machine
Example 21-4 lets you invoke PowerShell expressions on remote machines. It uses PsExec (from http://www.microsoft.com/technet/sysinternals/utilities/psexec.mspx) to support the actual remote command execution.
This script offers more power than just remote command execution, however. As Example 21-3 demonstrates, it leverages PowerShell’s capability to import and export strongly structured data, so you can work with the command output using many of the same techniques you use to work with command output on the local system. Example 21-3 demonstrates this power by filtering command output on the remote system but sorting it on the local system.
Example 21-3. Invoking a PowerShell expression on a remote machine
PS >$command = { Get-Process | Where-Object { $_.Handles -gt 1000 } }
PS >Invoke-RemoteExpression \\LEE-DESK $command | Sort Handles
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
1025 8 3780 3772 32 134.42 848 csrss
1306 37 50364 64160 322 409.23 4012 OUTLOOK
1813 39 54764 36360 321 340.45 1452 iTunes
2316 273 29168 41164 218 134.09 1244 svchost
Since this strongly structured data comes from objects on another system, PowerShell does not regenerate the functionality of those objects (except in rare cases). For more information about importing and exporting structured data, see “Easily Import and Export Your Structured Data.”
Example 21-4. Invoke-RemoteExpression.ps1
##############################################################################
##
## Invoke-RemoteExpression.ps1
##
## Invoke a PowerShell expression on a remote machine. Requires PsExec from
## http://www.microsoft.com/technet/sysinternals/utilities/psexec.mspx
##
## ie:
##
## PS >Invoke-RemoteExpression \\LEE-DESK { Get-Process }
## PS >(Invoke-RemoteExpression \\LEE-DESK { Get-Date }).AddDays(1)
## PS >Invoke-RemoteExpression \\LEE-DESK { Get-Process } | Sort Handles
##
##############################################################################
param(
$computer = "\\$ENV:ComputerName",
[ScriptBlock] $expression = $(throw "Please specify an expression to invoke."),
[switch] $noProfile
)
## Prepare the command line for PsExec. We use the XML output encoding so
## that PowerShell can convert the output back into structured objects.
$commandLine = "echo . | powershell -Output XML "
if($noProfile)
{
$commandLine += "-NoProfile "
}
## Convert the command into an encoded command for PowerShell
$commandBytes = [System.Text.Encoding]::Unicode.GetBytes($expression)
$encodedCommand = [Convert]::ToBase64String($commandBytes)
$commandLine += "-EncodedCommand $encodedCommand"
## Collect the output and error output
$errorOutput = [IO.Path]::GetTempFileName()
$output = psexec /acceptEula $computer cmd /c $commandLine 2>$errorOutput
## Check for any errors
$errorContent = Get-Content $errorOutput
Remove-Item $errorOutput
if($errorContent -match "Access is denied")
{
$OFS = "`n"
$errorMessage = "Could not execute remote expression. "
$errorMessage += "Ensure that your account has administrative " +
"privileges on the target machine.`n"
$errorMessage += ($errorContent -match "psexec.exe :")
Write-Error $errorMessage
}
## Return the output to the user
$output
Edit: Thanks to Otto’s suggestion, Added the /acceptEula switch to PsExec to accept the EULA automatically.