159

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
  • 30,738
  • 21
  • 105
  • 131
jzbruno
  • 1,698
  • 2
  • 12
  • 7
  • 7
    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
  • @mjsr Any way to get the output and ExitCode doing it without Start-process? I need to know if the command succeeded, but would be nice to pass through the output for error message. – scuba88 Apr 19 '22 at 21:00

11 Answers11

165

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
  • 4,031
  • 1
  • 27
  • 37
Andy Arismendi
  • 50,577
  • 16
  • 107
  • 124
  • 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
  • 5
    When u use the -verb runAs it does not allow theh -NoNewWindow or the Redirection Options – Maverick Jan 11 '13 at 18:25
  • 26
    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
  • 10
    @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
  • If I still wants to write the output to a file, what is the correct syntax? – rel.foo.fighters Jun 07 '21 at 16:49
  • 1
    yet another bookmark in my "why windows sucks" folder – Mike Jun 27 '22 at 23:04
  • 1
    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
32

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
  • 30,738
  • 21
  • 105
  • 131
JJones
  • 795
  • 6
  • 12
20

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
  • 30,738
  • 21
  • 105
  • 131
pserranne
  • 209
  • 2
  • 2
  • 7
    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
  • Is there an example somewhere showing the usage of this function? – Jonesome Reinstate Monica Jul 12 '22 at 17:54
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
  • 30,738
  • 21
  • 105
  • 131
LPG
  • 424
  • 4
  • 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
  • 1
    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
12

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
  • 30,738
  • 21
  • 105
  • 131
Rainer
  • 803
  • 10
  • 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
  • 2
    @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
5

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

start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'
import-clixml out.xml

Let me emphasize -nonewwindow to get the standardoutput and standarderror, at least on the local screen:

start-process -wait cmd '/c dir' -nonewwindow

 Volume in drive C is Windows
 Volume Serial Number is 2AC6-626F

 Directory of C:\users\me\foo

11/24/2022  11:40 AM    <DIR>          .
11/24/2022  11:40 AM    <DIR>          ..
11/24/2022  11:40 AM               330 file.json
               1 File(s)            330 bytes
               2 Dir(s)  25,042,915,328 bytes free
start-process -wait cmd '/c dir foo' -nonewwindow

 Volume in drive C is Windows
 Volume Serial Number is 2AC6-626F

 Directory of C:\users\me\foo

File Not Found
js2010
  • 23,033
  • 6
  • 64
  • 66
  • 2
    -NoNewWindow !!! nice !! thanks – Sergio Cabral Nov 14 '21 at 15:35
  • 1
    Whenever I add -NoNewWindow I get a "CategoryInfo : InvalidArgument: (:) [Start-Process], ParameterBindingException FullyQualifiedErrorId : AmbiguousParameterSet,Microsoft.PowerShell.Commands.StartProcessCommand" error. Without it the underlying powershell script works. – ashrasmun Jan 04 '22 at 16:13
  • @ashrasmun , it is because you cannot use `-NoNewWindow` with `-Verb` or `-WindowStyle` parameters in the same command. – Mavaddat Javid Oct 26 '22 at 20:04
3

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

2

Improved Answer - as long as you're OK with Start-Job instead of Start-Process

It turns out that the STDOUT and STDERR are accumulated in string arrays $job.ChildJobs[0].Output and $job.ChildJobs[0].Erroras the script runs. So you can poll these values and write them out periodically. Somewhat of a hack maybe, but it works.

It's not a stream though, so you have to manually keep track of the starting index into the array.

This code is simpler than my original answer, and at the end you have the entire STDOUT in $job.ChildJobs[0].Output. And as a little bonus for this demo, the calling script is PS7 and the background job is PS5.

$scriptBlock = {
  Param ([int]$param1, [int]$param2)
  $PSVersionTable
  Start-Sleep -Seconds 1
  $param1 + $param2
}

$parameters = @{
  ScriptBlock = $scriptBlock
  ArgumentList = 1, 2
  PSVersion = 5.1 # <-- remove this line for PS7
}

$timeoutSec = 5
$job = Start-Job @parameters
$job.ChildJobs[0].Output
$index = $job.ChildJobs[0].Output.Count

while ($job.JobStateInfo.State -eq [System.Management.Automation.JobState]::Running) {
  Start-Sleep -Milliseconds 200
  $job.ChildJobs[0].Output[$index]
  $index = $job.ChildJobs[0].Output.Count
  if (([DateTime]::Now - $job.PSBeginTime).TotalSeconds -gt $timeoutSec) {
    throw "Job timed out."
  }
}

As pointed out, my original answer can interleave the output. This is a limitation of event handling in PowerShell. It's not a fixable problem.

Original Answer, don't use - just leaving it here for interest

If there's a timeout, ReadToEnd() is not an option. You could do some fancy looping, but IMO the 'cleanest' way to do this is to ignore the streams. Hook the OutputDataReceived/ErrorDataReceived events instead, collecting the output. This approach also avoids the threading issues mentioned by others.

This is straightforward in C#, but it's tricky and verbose in Powershell. In particular, add_OutputDataReceived is not available for some reason. (Not sure if this is a bug or a feature, at least this seems to be the case in PowerShell 5.1.) To work around it you can use Register-ObjectEvent.

$stdout = New-Object System.Text.StringBuilder
$stderr = New-Object System.Text.StringBuilder

$proc = [System.Diagnostics.Process]@{
  StartInfo = @{
    FileName = 'ping.exe'
    Arguments = 'google.com'
    RedirectStandardOutput = $true
    RedirectStandardError = $true
    UseShellExecute = $false
    WorkingDirectory = $PSScriptRoot
  }
}

$stdoutEvent = Register-ObjectEvent $proc -EventName OutputDataReceived -MessageData $stdout -Action {
  $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}

$stderrEvent = Register-ObjectEvent $proc -EventName ErrorDataReceived -MessageData $stderr -Action {
  $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
}

$proc.Start() | Out-Null
$proc.BeginOutputReadLine()
$proc.BeginErrorReadLine()
Wait-Process -Id $proc.Id -TimeoutSec 5

if ($proc.HasExited) {
  $exitCode = $proc.ExitCode
}
else {
  Stop-Process -Force -Id $proc.Id
  $exitCode = -1
}

# Be sure to unregister.  You have been warned.
Unregister-Event $stdoutEvent.Id
Unregister-Event $stderrEvent.Id
Write-Output $stdout.ToString()
Write-Output $stderr.ToString()
Write-Output "Exit code: $exitCode"
  • The code shown is the happy path (stderr is empty)
  • To test the timeout path, set -TimeoutSec to .5
  • To test the sad path (stderr has content), set FileName to 'cmd' and Arguments to /C asdf
Paul Williams
  • 3,099
  • 38
  • 34
  • 1
    This script works, except that output generated by the external process can be displayed in an out-of-order fashion. I built a small console app that simply echos out the parameters passed into it, in the order they appear on its command-line. When I execute it via this script, the output generated is displayed out of order. – STLDev Jan 11 '22 at 16:49
  • 1
    @STLDev - you're correct. Please see my updated answer. In our application we were only using it for visual monitoring as the job ran, not capturing output, so it tolerable. – Paul Williams Jun 04 '22 at 00:29
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
  • 7,027
  • 11
  • 57
  • 81
1

You may want to also consider using the & operator combined with --% instead of start-process - that lets you easily pipe and process the command and/or error output.

  • put the escape parameter into a variable
  • put the arguments into a variable
$deploy= "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$esc = '--%'
$arguments ="-source:package='c:\temp\pkg.zip' -verb:sync"
$output = & $deploy $esc $arguments 

That passes the parameters to the executable without interference and let me get around the issues with start-process.

Combine Stderr and Stdout into one variable:

$output = & $deploy $esc $arguments 2>&1

Get separate variables for Stderr and Stdout

$err = $( $output = & $deploy $esc $arguments) 2>&1
mabene
  • 101
  • 4
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
}