Powershell: Capturing standard out and error with Process object

Viewed 23187

I want to start a Java program from PowerShell and get the results printed on the console.

I have followed the instructions of this question: Capturing standard out and error with Start-Process

But for me, this is not working as I expected. What I'm doing wrong?

This is the script:

$psi = New-object System.Diagnostics.ProcessStartInfo
$psi.CreateNoWindow = $true
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.FileName = 'java.exe'
$psi.Arguments = @("-jar","tools\compiler.jar","--compilation_level",   "ADVANCED_OPTIMIZATIONS", "--js", $BuildFile, "--js_output_file", $BuildMinFile)
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $psi
$process.Start() | Out-Null
$process.WaitForExit()
$output = $process.StandardOutput.ReadToEnd()
$output

The $output variable is always empty (and nothing is printed on the console of course).

4 Answers

Here is a modification to paul's answer, hopefully it addresses the truncated output. i did a test using a failure and did not see truncation.

function Start-ProcessWithOutput
{
    param ([string]$Path,[string[]]$ArgumentList)
    $Output = New-Object -TypeName System.Text.StringBuilder
    $Error = New-Object -TypeName System.Text.StringBuilder
    $psi = New-object System.Diagnostics.ProcessStartInfo 
    $psi.CreateNoWindow = $true 
    $psi.UseShellExecute = $false 
    $psi.RedirectStandardOutput = $true 
    $psi.RedirectStandardError = $true 
    $psi.FileName = $Path
    if ($ArgumentList.Count -gt 0)
    {
        $psi.Arguments = $ArgumentList
    }
    $process = New-Object System.Diagnostics.Process 
    $process.StartInfo = $psi 
    [void]$process.Start()
    do
    {


       if (!$process.StandardOutput.EndOfStream)
       {
           [void]$Output.AppendLine($process.StandardOutput.ReadLine())
       }
       if (!$process.StandardError.EndOfStream)
       {
           [void]$Error.AppendLine($process.StandardError.ReadLine())
       }
       Start-Sleep -Milliseconds 10
    } while (!$process.HasExited)

    #read remainder
    while (!$process.StandardOutput.EndOfStream)
    {
        #write-verbose 'read remaining output'
        [void]$Output.AppendLine($process.StandardOutput.ReadLine())
    }
    while (!$process.StandardError.EndOfStream)
    {
        #write-verbose 'read remaining error'
        [void]$Error.AppendLine($process.StandardError.ReadLine())
    }

    return @{ExitCode = $process.ExitCode; Output = $Output.ToString(); Error = $Error.ToString(); ExitTime=$process.ExitTime}
}


$p = Start-ProcessWithOutput "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x","-y","-oE:\PowershellModules",$NewModules.FullName -verbose
$p.ExitCode
$p.Output
$p.Error

the 10ms sleep is to avoid spinning cpu when nothing to read.

I was getting the deadlock scenario mentioned by Ash using Justin's solution. Modified the script accordingly to subscribe to async event handlers to get the output and error text which accomplishes the same thing but avoids the deadlock condition.

Seemed to resolve the deadlock issue in my testing without altering the return data.

# Define global variables used in the Start-ProcessWithOutput function.
$global:processOutputStringGlobal = ""
$global:processErrorStringGlobal = ""

# Launch an executable and return the exitcode, output text, and error text of the process.
function Start-ProcessWithOutput
{
    # Function requires a path to an executable and an optional list of arguments
    param (
        [Parameter(Mandatory=$true)] [string]$ExecutablePath,
        [Parameter(Mandatory=$false)] [string[]]$ArgumentList
    )

    # Reset our global variables to an empty string in the event this process is called multiple times.
    $global:processOutputStringGlobal = ""
    $global:processErrorStringGlobal = ""

    # Create the Process Info object which contains details about the process.  We tell it to 
    # redirect standard output and error output which will be collected and stored in a variable.
    $ProcessStartInfoObject = New-object System.Diagnostics.ProcessStartInfo 
    $ProcessStartInfoObject.FileName = $ExecutablePath
    $ProcessStartInfoObject.CreateNoWindow = $true 
    $ProcessStartInfoObject.UseShellExecute = $false 
    $ProcessStartInfoObject.RedirectStandardOutput = $true 
    $ProcessStartInfoObject.RedirectStandardError = $true 
    
    # Add the arguments to the process info object if any were provided
    if ($ArgumentList.Count -gt 0)
    {
        $ProcessStartInfoObject.Arguments = $ArgumentList
    }

    # Create the object that will represent the process
    $Process = New-Object System.Diagnostics.Process 
    $Process.StartInfo = $ProcessStartInfoObject 

    # Define actions for the event handlers we will subscribe to in a moment.  These are checking whether
    # any data was sent by the event handler and updating the global variable if it is not null or empty.
    $ProcessOutputEventAction = { 
        if ($null -ne $EventArgs.Data -and $EventArgs.Data -ne ""){
            $global:processOutputStringGlobal += "$($EventArgs.Data)`r`n"
        }
    }
    $ProcessErrorEventAction = { 
        if ($null -ne $EventArgs.Data -and $EventArgs.Data -ne ""){
            $global:processErrorStringGlobal += "$($EventArgs.Data)`r`n"
        }
    }

    # We need to create an event handler for the Process object.  This will call the action defined above 
    # anytime that event is triggered.  We are looking for output and error data received by the process 
    # and appending the global variables with those values.
    Register-ObjectEvent -InputObject $Process -EventName "OutputDataReceived" -Action $ProcessOutputEventAction
    Register-ObjectEvent -InputObject $Process -EventName "ErrorDataReceived" -Action $ProcessErrorEventAction

    # Process starts here
    [void]$Process.Start()

    # This sets up an asyncronous task to read the console output from the process, which triggers the appropriate
    # event, which we setup handlers for just above.
    $Process.BeginErrorReadLine()
    $Process.BeginOutputReadLine()
    
    # Wait for the process to exit.  
    $Process.WaitForExit()

    # We need to wait just a moment so the async tasks that are reading the output of the process can catch
    # up.  Not having this sleep here can cause the return values to be empty or incomplete.  In my testing, 
    # it seemed like half a second was enough time to always get the data, but you may need to adjust accordingly.
    Start-Sleep -Milliseconds 500


    # Return an object that contains the exit code, output text, and error text.
    return @{
        ExitCode = $Process.ExitCode; 
        OutputString = $global:processOutputStringGlobal; 
        ErrorString = $global:processErrorStringGlobal; 
        ExitTime = $Process.ExitTime
    }
}
Related