128

Is there a bug in PowerShell's Start-Process command when accessing the StandardError and StandardOutput properties?

If I run the following I get no output:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.StandardOutput
$process.StandardError

But if I redirect the output to a file I get the expected result:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait -RedirectStandardOutput stdout.txt -RedirectStandardError stderr.txt
Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
jzbruno
  • 1,378
  • 2
  • 10
  • 7
  • 6
    In this specific case do you really need Start-process?...`$process= ping localhost `# would save the output in the process variable. – mjsr Jan 07 '12 at 01:00
  • 1
    True. I was looking for a cleaner way to handle return and arguments. I ended up writing the script like you showed. – jzbruno Jan 09 '12 at 16:34

9 Answers9

143

That's how Start-Process was designed for some reason. Here's a way to get it without sending to file:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
Clijsters
  • 3,240
  • 1
  • 22
  • 35
Andy Arismendi
  • 45,027
  • 16
  • 97
  • 114
  • 7
    I am accepting your answer. I wish they wouldn't have created properties that aren't used, it is very confusing. – jzbruno Jan 09 '12 at 16:36
  • 1
    @jzbruno The PowerShell team didn't create the StandardOutput/StandardError properties... They are part of the underlying [System.Diagnostics.Process](http://msdn.microsoft.com/en-us/library/system.diagnostics.process.aspx) object . However they are only available when the `UseShellExecute` property is set to false. So it depends on how the PowerShell team implemented `Start-Process` behind the scenes... Unfortunately I can't look at the source code :-( – Andy Arismendi Jan 09 '12 at 16:48
  • 7
    If you have trouble running a process this way, see accepted answer here http://stackoverflow.com/questions/11531068/powershell-capturing-standard-out-and-error-with-process-object, which has a slight modification to the WaitForExit and StandardOutput.ReadToEnd – Ralph Willgoss Aug 17 '12 at 10:25
  • 3
    When u use the -verb runAs it does not allow theh -NoNewWindow or the Redirection Options – Maverick Jan 11 '13 at 18:25
  • 17
    This code will deadlock under some conditions due to both StdErr and StdOut being synchronously read to the end. http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.redirectstandardoutput.aspx – codepoke Apr 04 '13 at 16:00
  • 8
    @codepoke - it's slightly worse than that - since it does the WaitForExit call first, even if it only redirected one of them, it could deadlock if the stream buffer gets filled up (since it doesn't attempt to read from it until the process has exited) – James Manning Jun 13 '13 at 05:29
  • 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 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:45
  • 1
    What if I don't want to wait for the process to end? – Rosberg Linhares Jan 04 '17 at 13:47
  • I wish stdout and stderr where merged line by line in time as the dos redirect would do. Also echoed live to console would be nice. – crokusek Feb 24 '17 at 00:46
  • @RosbergLinhares in case you want to see the output before the process exits, this answer may help: http://stackoverflow.com/a/14061481/411428 – Manfred Mar 20 '17 at 01:02
  • Any way to set this object up and still redirect the StandardError to a file? I'm interested in how you're accessing both the ExitCode and StandardError. – dornadigital Jan 17 '18 at 23:32
  • how i can run the targeted executable as adminstrator like what start-process let's you do with runAs – Eboubaker Feb 11 '20 at 14:36
  • @ZOLDIK use `$pinfo.Verb = "runas"` – Andy Arismendi Feb 12 '20 at 16:25
  • @AndyArismendi okay, using $pinfo.UseShellExecute = $true , makes it work but i can't redirect the output of the process (i have to remove the redirect part of the code) – Eboubaker Feb 14 '20 at 17:49
23

In the code given in the question, I think that reading the ExitCode property of the initiation variable should work.

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.ExitCode

Note that (as in your example) you need to add the -PassThru and -Wait parameters (this caught me out for a while).

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
JJones
  • 705
  • 5
  • 12
15

I also had this issue and ended up using Andy's code to create a function to clean things up when multiple commands need to be run.

It'll return stderr, stdout, and exit codes as objects. One thing to note: the function won't accept .\ in the path; full paths must be used.

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $commandPath
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $commandArguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    [pscustomobject]@{
        commandTitle = $commandTitle
        stdout = $p.StandardOutput.ReadToEnd()
        stderr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
    }
}

Here's how to use it:

$DisableACMonitorTimeOut = Execute-Command -commandTitle "Disable Monitor Timeout" -commandPath "C:\Windows\System32\powercfg.exe" -commandArguments " -x monitor-timeout-ac 0"
Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
LPG
  • 314
  • 2
  • 14
  • Good idea, but it seems the syntax isn't working for me. Shouldn't the parameter list use the param( [type]$ArgumentName ) syntax? can you add an example call to this function? – Lockszmith Jan 07 '16 at 03:38
  • Regarding "One thing to note: the function won't accept .\ in the path; full paths must be used.": You could use: > $pinfo.FileName = Resolve-Path $commandPath – Lupuz Jun 11 '20 at 08:30
14

IMPORTANT:

We have been using the function as provided above by LPG.

However, this contains a bug you might encounter when you start a process that generates a lot of output. Due to this you might end up with a deadlock when using this function. Instead use the adapted version below:

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
  Try {
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $commandPath
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $commandArguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    [pscustomobject]@{
        commandTitle = $commandTitle
        stdout = $p.StandardOutput.ReadToEnd()
        stderr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
    }
    $p.WaitForExit()
  }
  Catch {
     exit
  }
}

Further information on this issue can be found at MSDN:

A deadlock condition can result if the parent process calls p.WaitForExit before p.StandardError.ReadToEnd and the child process writes enough text to fill the redirected stream. The parent process would wait indefinitely for the child process to exit. The child process would wait indefinitely for the parent to read from the full StandardError stream.

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
pserranne
  • 149
  • 1
  • 2
  • 5
    This code still deadlocks due to the synchronous call to ReadToEnd(), which your link to MSDN describes as well. – bergmeister Oct 13 '17 at 09:41
  • 1
    This now seems to have solved my issue. I must admit that I do not fully understand why it did hang, but it seems that empty stderr blocked the process to finish. Strange thing, since it did work for a long period of time, but suddenly right before Xmas it started failing, causing a lot of Java-processes to hang. – rhellem Jan 04 '18 at 08:58
10

I really had troubles with those examples from Andy Arismendi and from LPG. You should always use:

$stdout = $p.StandardOutput.ReadToEnd()

before calling

$p.WaitForExit()

A full example is:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
Rainer
  • 685
  • 7
  • 8
  • Where did you read that "You should always use: $p.StandardOutput.ReadToEnd() before $p.WaitForExit()"? If there's output on the buffer that is exhausted, following more output at a later time, that will be missed if the line of execution is on WaitForExit and the process hasn't finished (and subsequently outputs more stderr or stdout).... – CJBS Nov 30 '17 at 17:09
  • Regarding my comment above, I later saw the comments on the accepted answer regarding deadlocking and buffer overflow in cases of large output, but that aside, I would expect that just because the buffer is read to the end, it doesn't mean the process has completed, and there could thus be more output that's missed. Am I missing something? – CJBS Nov 30 '17 at 17:43
  • 1
    @CJBS: _"just because the buffer is read to the end, it doesn't mean the process has completed"_ -- it does mean that. In fact, that's why it can deadlock. Reading "to the end" doesn't mean "read whatever's there _now_". It means start reading, and don't stop until the stream is closed, which is the same as the process terminating. – Peter Duniho May 26 '20 at 20:05
1

Here's a kludgy way to get the output from another powershell process:

start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'; import-clixml out.xml
js2010
  • 13,551
  • 2
  • 28
  • 40
1

To get both stdout and stderr, I use:

Function GetProgramOutput([string]$exe, [string]$arguments)
{
    $process = New-Object -TypeName System.Diagnostics.Process
    $process.StartInfo.FileName = $exe
    $process.StartInfo.Arguments = $arguments

    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.RedirectStandardOutput = $true
    $process.StartInfo.RedirectStandardError = $true
    $process.Start()

    $output = $process.StandardOutput.ReadToEnd()   
    $err = $process.StandardError.ReadToEnd()

    $process.WaitForExit()

    $output
    $err
}

$exe = "cmd"
$arguments = '/c echo hello 1>&2'   #this writes 'hello' to stderr

$runResult = (GetProgramOutput $exe $arguments)
$stdout = $runResult[-2]
$stderr = $runResult[-1]

[System.Console]::WriteLine("Standard out: " + $stdout)
[System.Console]::WriteLine("Standard error: " + $stderr)
Fidel
  • 5,691
  • 9
  • 41
  • 65
0

Here is my version of function that is returning standard System.Diagnostics.Process with 3 new properties

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
    Try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo
        $pinfo.FileName = $commandPath
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WindowStyle = 'Hidden'
        $pinfo.CreateNoWindow = $True
        $pinfo.Arguments = $commandArguments
        $p = New-Object System.Diagnostics.Process
        $p.StartInfo = $pinfo
        $p.Start() | Out-Null
        $stdout = $p.StandardOutput.ReadToEnd()
        $stderr = $p.StandardError.ReadToEnd()
        $p.WaitForExit()
        $p | Add-Member "commandTitle" $commandTitle
        $p | Add-Member "stdout" $stdout
        $p | Add-Member "stderr" $stderr
    }
    Catch {
    }
    $p
}
0

Here's what I cooked up based on the examples posted by others on this thread. This version will hide the console window and provided options for output display.

function Invoke-Process {
    [CmdletBinding(SupportsShouldProcess)]
    param
        (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ArgumentList,

        [ValidateSet("Full","StdOut","StdErr","ExitCode","None")]
        [string]$DisplayLevel
        )

    $ErrorActionPreference = 'Stop'

    try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo
        $pinfo.FileName = $FilePath
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WindowStyle = 'Hidden'
        $pinfo.CreateNoWindow = $true
        $pinfo.Arguments = $ArgumentList
        $p = New-Object System.Diagnostics.Process
        $p.StartInfo = $pinfo
        $p.Start() | Out-Null
        $result = [pscustomobject]@{
        Title = ($MyInvocation.MyCommand).Name
        Command = $FilePath
        Arguments = $ArgumentList
        StdOut = $p.StandardOutput.ReadToEnd()
        StdErr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
        }
        $p.WaitForExit()

        if (-not([string]::IsNullOrEmpty($DisplayLevel))) {
            switch($DisplayLevel) {
                "Full" { return $result; break }
                "StdOut" { return $result.StdOut; break }
                "StdErr" { return $result.StdErr; break }
                "ExitCode" { return $result.ExitCode; break }
                }
            }
        }
    catch {
        exit
        }
}

Example: Invoke-Process -FilePath "FQPN" -ArgumentList "ARGS" -DisplayLevel Full