Evgeny
Evgeny

Reputation: 53

Powershell: capturing remote output streams in Invoke-Command + Invoke-Expression combination

As I didn't find a solution by searching the forum and spent some time for finding out how to do it properly, I'm placing here the issue along with the working solution.

Scenario: in Powershell, need to remotely execute a script block stored in a variable and capture its output for further processing. No output should appear on the screen unless the script generates it on purpose. The script block can contain Write-Warning commands.

Upvotes: 1

Views: 3352

Answers (2)

mklement0
mklement0

Reputation: 437278

Note that the behaviors of interest apply generally to PowerShell commands, not just in the context of Invoke-Command and the - generally to be avoided - Invoke-Expression; in your case, it is only needed to work around a bug.[1]


Your own answer shows how to redirect a single, specific output streams to the success output stream; e.g, 3>&1 redirects (>&) the warning stream (3) to the success (output) stream (1).

The & indicates that the redirection target is a stream, as opposed to a file; for more information about PowerShell's output stream, see about_Redirection.

If you want to redirect all output streams to the success output stream, use redirection *>&1

By redirecting all streams to the output stream, their combined output can be captured in a variable, redirected to a file, or sent through the pipeline, whereas by default only the success output stream (1) is captured.

Separately, you can use the common parameters named -*Variable parameters to capture individual stream output in variables for some streams, namely:

  • Stream 1 (success): -OutVariable
  • Stream 2 (error): -ErrorVariable
  • Stream 3 (warning): -WarningVariable
  • Stream 6 (information): -InformationVariable

Be sure to specify the target variable by name only, without the $ prefix; e.g., to capture warnings in variable $warnings, use
-WarningVariable warnings, such as in the following example:
Write-Warning hi -WarningVariable warnings; "warnings: $warnings"

Note that with -*Variable, the stream output is collected in the variable whether or not you silence or even ignore that stream otherwise, with the notable exception of -ErrorAction Ignore, in which case an -ErrorVariable variable is not populated (and the error is also not recorded in the automatic $Error variable that otherwise records all errors that occur in the session).

Generally, -{StreamName}Action SilentlyIgnore seems to be equivalent to {StreamNumber}>$null.


Note the absence of the verbose (4) and the debug (5) streams above; you can only capture them indirectly, via 4>&1 and 5>&1 (or *>&1), which then requires you to extract the output of interest from the combined stream, via filtering by output-object type:

Important:

  • The verbose (4) and debug (5) streams are the only two streams that are silent at the source by default; that is, unless these streams are explicitly turned on via -Verbose / -Debug or their preference-variable equivalents, $VerbosePreference = 'Continue' / $DebugPreference = 'Continue', nothing is emitted and nothing can be captured.

  • The information stream (5) is silent only on output by default; that is, writing to the information stream (with Write-Information) always writes objects to the stream, but they're not displayed by default (they're only displayed with -InformationAction Continue / $InformationPreference = 'Continue')

    • Since v5, Write-Host now too writes to the information stream, though its output does print by default, but can be suppressed with 6>$null or -InformationAction Ignore (but not -InformationAction SilentlyContinue).
# Sample function that produces success and verbose output.
# Note that -Verbose is required for the message to actually be emitted.
function foo { Write-Output 1; Write-Verbose -Verbose 4 }

# Get combined output, via 4>&1
$combinedOut = foo 4>&1

# Extract the verbose-stream output records (objects).
# For the debug output stream (5), the object type is
# [System.Management.Automation.DebugRecord]
$verboseOut = $combinedOut.Where({ $_ -is [System.Management.Automation.VerboseRecord] })

[1] Stream-capturing bug, as of PowerShell v7.0:

In a nutshell: In the context of remoting (such as Invoke-Command -Session here), background jobs, and so-called minishells (passing a script block to the PowerShell CLI to execute commands in a child process), only the success (1) and error (2) streams can be captured as expected; all other are unexpectedly passed through to the host (display) - see GitHub issue #9585.

Your command should - but currently doesn't - work as follows, which would obviate the need for Invoke-Expression:

# !! 3>&1 redirection is BROKEN as of PowerShell 7.0, if *remoting* is involved
# !! (parameters -Session or -ComputerName).
$RemoteOutput = 
  Invoke-Command -Session $Session $Commands 3>&1 -ErrorVariable RemoteError 2>$null

That is, in principle you should be able to pass a $Commands variable that contains a script block directly as the (implied) -ScriptBlock argument to Invoke-Command.

Upvotes: 2

Evgeny
Evgeny

Reputation: 53

Script block is contained in $Commands variable. $Session is an already established Powershell remoting session.

The task is resolved by the below command:

$RemoteOutput = 
  Invoke-Command -Session $Session {
    Invoke-Expression $Using:Commands 3>&1
  } -ErrorVariable RemoteError 2>$null

After the command is executed all output of the script block is contained in $RemoteOutput. Errors generated during remote code execution are placed in $RemoteError.

Additional clarifications. Write-Warning in Invoke-Expression code block generates its own output stream that is not captured by Invoke-Command. The only way to capture it in a variable is to redirect that stream to the standard stream of Invoke-Expression by using 3>&1. Commands in the code block writing to other output streams (verbose, debug) seems not to be captured even by adding 4>&1 and 5>&1 parameters to Invoke-Expression. However, stream #2 (errors) is properly captured by Invoke-Command in the way shown above.

Upvotes: 0

Related Questions