25

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).

David Ferenczy Rogožan
  • 23,966
  • 9
  • 79
  • 68
Marcelo De Zen
  • 9,439
  • 3
  • 37
  • 50
  • Considering PowerShell is a "shell", any reason why you just don't execute `PS> java.exe -jar tools\compiler.jar --compilation_level ...`? – Keith Hill Jul 18 '12 at 02:16
  • It actually works :) But I still don't understand why the script above doesn't print the output to console! – Marcelo De Zen Jul 18 '12 at 12:50
  • @dev use Write-OutPut $output, to see what is returned via the console – Ralph Willgoss Aug 17 '12 at 10:23
  • Any final solution with full source code sample application ? IMHO, better samples for minimize learning curve are real applications with full source code and good patterns. – Kiquenet Aug 28 '14 at 06:46

4 Answers4

45

The docs on the RedirectStandardError property suggests that it is better to put the WaitForExit() call after the ReadToEnd() call. The following works correctly for me:

$psi = New-object System.Diagnostics.ProcessStartInfo 
$psi.CreateNoWindow = $true 
$psi.UseShellExecute = $false 
$psi.RedirectStandardOutput = $true 
$psi.RedirectStandardError = $true 
$psi.FileName = 'ipconfig.exe' 
$psi.Arguments = @("/a") 
$process = New-Object System.Diagnostics.Process 
$process.StartInfo = $psi 
[void]$process.Start()
$output = $process.StandardOutput.ReadToEnd() 
$process.WaitForExit() 
$output
Keith Hill
  • 194,368
  • 42
  • 353
  • 369
  • Ah, I use the | Out-Null to prevent that a "true" is printed on console. – Marcelo De Zen Jul 19 '12 at 14:09
  • @devundef Ignore the comment about piping to Out-Null. It's OK here. The pipe to Out-Null trick can be used to hold off until a Windows exe you launch has closed i.e. `Notepad.exe | Out-Null`. This doesn't apply in your case. – Keith Hill Jul 19 '12 at 17:04
  • 1
    I'm pretty sure Windows also won't allow you to redirect standard input/output/error across the admin/non-admin security boundary. You'll have to find a different way to get output from the program running as admin - Reference: http://stackoverflow.com/a/8690661 – Kiquenet Aug 28 '14 at 06:46
  • i just opened an issue in the pwsh core repo if anyone's interested: https://github.com/PowerShell/PowerShell/issues/18026 – aetonsi Sep 03 '22 at 23:56
18

Small variation so that you can selectively print the output if needed. As in if your looking just for error or warning messages and by the way Keith you saved my bacon with your response...

$psi = New-object System.Diagnostics.ProcessStartInfo 
$psi.CreateNoWindow = $true 
$psi.UseShellExecute = $false 
$psi.RedirectStandardOutput = $true 
$psi.RedirectStandardError = $true 
$psi.FileName = 'robocopy' 
$psi.Arguments = @("$HomeDirectory $NewHomeDirectory /MIR /XF desktop.ini /XD VDI /R:0 /W:0 /s /v /np") 
$process = New-Object System.Diagnostics.Process 
$process.StartInfo = $psi 
[void]$process.Start()
do
{
   $process.StandardOutput.ReadLine()
}
while (!$process.HasExited)
javacavaj
  • 2,901
  • 5
  • 39
  • 66
Paul Viscovich
  • 181
  • 1
  • 2
  • I'm pretty sure Windows also won't allow you to redirect standard input/output/error across the admin/non-admin security boundary. You'll have to find a different way to get output from the program running as admin - Reference: http://stackoverflow.com/a/8690661 – Kiquenet Aug 28 '14 at 06:47
  • 1
    Your way of repeatedly reading output buffer is the key. Otherwise processes producing large text can fill the output buffer, and program will hand. Only change that is needed is to append the read line to a string to use later. – Adarsha Apr 27 '16 at 06:16
  • 3
    I have encountered trouble with this approach, when the process exits due to an error, say, and ReadLine() can't catch up, so the output is truncated. – Peaeater Jul 07 '16 at 18:56
  • This a great workaround to the deadlock that occurs doing this the "proper" way. – rollsch Oct 23 '19 at 12:51
4

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.

Justin
  • 1,303
  • 15
  • 30
  • In my testing this ends up in a deadlock scenario. The 'EndOfStream' is the culprit: https://stackoverflow.com/questions/2767496/standardoutput-endofstream-hangs – ash Apr 28 '22 at 05:29
1

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
    }
}
dislexic
  • 270
  • 3
  • 5