105

I'm trying to run a program from PowerShell, wait for the exit, then get access to the ExitCode, but I am not having much luck. I don't want to use -Wait with Start-Process, as I need some processing to carry on in the background.

Here's a simplified test script:

cd "C:\Windows"

# ExitCode is available when using -Wait...
Write-Host "Starting Notepad with -Wait - return code will be available"
$process = (Start-Process -FilePath "notepad.exe" -PassThru -Wait)
Write-Host "Process finished with return code: " $process.ExitCode

# ExitCode is not available when waiting separately
Write-Host "Starting Notepad without -Wait - return code will NOT be available"
$process = (Start-Process -FilePath "notepad.exe" -PassThru)
$process.WaitForExit()
Write-Host "Process exit code should be here: " $process.ExitCode

Running this script will cause Notepad to be started. After this is closed manually, the exit code will be printed, and it will start again, without using -wait. No ExitCode is provided when this is quit:

Starting Notepad with -Wait - return code will be available
Process finished with return code:  0
Starting Notepad without -Wait - return code will NOT be available
Process exit code should be here:

I need to be able to perform additional processing between starting the program and waiting for it to quit, so I can't make use of -Wait. How can I do this and still have access to the .ExitCode property from this process?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Richard
  • 29,854
  • 11
  • 77
  • 120

6 Answers6

148

There are two things to remember here. One is to add the -PassThru argument and two is to add the -Wait argument. You need to add the wait argument because of this defect.

-PassThru [<SwitchParameter>]
    Returns a process object for each process that the cmdlet started. By default,
    this cmdlet does not generate any output.

Once you do this a process object is passed back and you can look at the ExitCode property of that object. Here is an example:

$process = start-process ping.exe -windowstyle Hidden -ArgumentList "-n 1 -w 127.0.0.1" -PassThru -Wait
$process.ExitCode

# This will print 1

If you run it without -PassThru or -Wait, it will print out nothing.

The same answer is here: How do I run a Windows installer and get a succeed/fail value in PowerShell?

It's also worth noting that there's a workaround mentioned in the "defect report" link above, which is as following:

# Start the process with the -PassThru command to be able to access it later
$process = Start-Process 'ping.exe' -WindowStyle Hidden -ArgumentList '-n 1 -w 127.0.0.1' -PassThru

# This will print out False/True depending on if the process has ended yet or not
# Needs to be called for the command below to work correctly
$process.HasExited

# This will print out the actual exit code of the process
$process.GetType().GetField('exitCode', 'NonPublic, Instance').GetValue($process)
sp00n
  • 1,026
  • 1
  • 8
  • 12
Daniel McQuiston
  • 2,056
  • 2
  • 15
  • 15
  • 3
    Worth noting the workaround: $p.GetType().GetField("exitCode", "NonPublic,Instance").GetValue($p) – David Martin Jul 11 '13 at 11:56
  • 2
    The first link is (effectively) broken: *"Microsoft Connect Has Been Retired"* – Peter Mortensen Nov 06 '18 at 20:41
  • 1
    One thing to note about using -Wait is that it causes Start-Process to wait for all decedent processes to exit -- not just the one it starts. Often this is not an issue since it's relatively uncommon for a process to create children. But, I ran into this with msbuild.exe that spawns children that don't exit. Can't use -Wait since causes the script to hang. – steve Jul 22 '20 at 13:52
  • 1
    The poster specifically says that he cannot use -Wait, so this answer is not useful. – Nanki Nov 07 '21 at 11:13
72

While trying out the final suggestion above, I discovered an even simpler solution. All I had to do was cache the process handle. As soon as I did that, $process.ExitCode worked correctly. If I didn't cache the process handle, $process.ExitCode was null.

example:

$proc = Start-Process $msbuild -PassThru
$handle = $proc.Handle # cache proc.Handle
$proc.WaitForExit();

if ($proc.ExitCode -ne 0) {
    Write-Warning "$_ exited with status code $($proc.ExitCode)"
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
Adrian
  • 729
  • 5
  • 2
  • 6
    I would love to know why this works if anyone has any thoughts I'd appreciate them. – CarlR Jun 21 '16 at 13:41
  • 14
    @CarlR This is a quirk of the implementation of the .NET Process object. The implementation of the ExitCode property first checks if the process has exited. For some reason, the code that performs that check not only looks at the HasExited property but also verifies that the proces handle is present in the proces object and [throws an exception if it is not](https://github.com/Microsoft/referencesource/blob/e458f8df6ded689323d4bd1a2a725ad32668aaec/System/services/monitoring/system/diagnosticts/Process.cs#L1400). PowerShell intercepts that exception and returns null. – Jakub Berezanski Feb 11 '17 at 01:26
  • 10
    (cont.) Accessing the Handle property causes the process object to retrieve the process handle and [store it internally](https://github.com/Microsoft/referencesource/blob/e458f8df6ded689323d4bd1a2a725ad32668aaec/System/services/monitoring/system/diagnosticts/Process.cs#L1742). Once the handle is stored in the process object, the ExitCode property works as expected. – Jakub Berezanski Feb 11 '17 at 01:29
  • 5
    that is insane, but as a workaround it does work perfectly. weird! – Mark Hughes Mar 29 '18 at 12:47
53

Two things you could do I think...

  1. Create the System.Diagnostics.Process object manually and bypass Start-Process
  2. Run the executable in a background job (only for non-interactive processes!)

Here's how you could do either:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "notepad.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = ""
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
#Do Other Stuff Here....
$p.WaitForExit()
$p.ExitCode

OR

Start-Job -Name DoSomething -ScriptBlock {
    & ping.exe somehost
    Write-Output $LASTEXITCODE
}
#Do other stuff here
Get-Job -Name DoSomething | Wait-Job | Receive-Job
Andy Arismendi
  • 50,577
  • 16
  • 107
  • 124
  • the 1st snippet is more bulletproof and did the trick, even if the process abruptly terminates. – Hasan Nov 23 '16 at 17:02
  • How can I get the full Output of the Process started here? – r4d1um Aug 15 '17 at 06:53
  • 2
    I had to come here to learn that `Start-Process` creates an object of type `System.Diagnostics.Process`. Why don't they include this information in the powershell documentation? – birgersp Feb 23 '21 at 12:20
23

The '-Wait' option seemed to block for me even though my process had finished.

I tried Adrian's solution and it works. But I used Wait-Process instead of relying on a side effect of retrieving the process handle.

So:

$proc = Start-Process $msbuild -PassThru
Wait-Process -InputObject $proc

if ($proc.ExitCode -ne 0) {
    Write-Warning "$_ exited with status code $($proc.ExitCode)"
}
Ben T
  • 4,656
  • 3
  • 22
  • 22
  • It looks to me using `$proc.HasExited` is not reliable either. I had several cases where it would intermittently be stuck when waiting for that value to become true when being using in a while-loop. I didn't know about `Wait-Process` but this appears to do a much better job. Thanks, @blt – Manfred Jun 28 '17 at 09:49
  • 1
    This is the best answer! Seems to me that it _should_ also work to use WaitForExit or -Wait, but this answer is the best alternative. So much simpler than using System.Diagnostics.Process. – steve Jul 22 '20 at 14:05
  • Using PS 5.1 (because I'm forced to), I have tried many of the above suggestions and @Ben T's is the only one that worked for me, on a consistent basis. At first Adrian's ans seem to work, however, I soon realized the exit code was not getting set properly/consistently. So, agree here...This is the best answer. – BentChainRing Oct 27 '21 at 21:37
5

Or try adding this...

$code = @"
[DllImport("kernel32.dll")]
public static extern int GetExitCodeProcess(IntPtr hProcess, out Int32 exitcode);
"@
$type = Add-Type -MemberDefinition $code -Name "Win32" -Namespace Win32 -PassThru
[Int32]$exitCode = 0
$type::GetExitCodeProcess($process.Handle, [ref]$exitCode)

By using this code, you can still let PowerShell take care of managing redirected output/error streams, which you cannot do using System.Diagnostics.Process.Start() directly.

Greg Vogel
  • 91
  • 7
  • 2
    Tip: Cache the process handle `($process.Handle)` after starting the process with `Start-Process`. It won't be set anymore after process exit. – Heinrich Ulbricht Dec 09 '13 at 12:29
0

Here's a variation on this theme. I want to uninstall Cisco Amp, wait, and get the exit code. But the uninstall program starts a second program called "un_a" and exits. With this code, I can wait for un_a to finish and get the exit code of it, which is 3010 for "needs reboot". This is actually inside a .bat file.

If you've ever wanted to uninstall folding@home, it works in a similar way.

rem uninstall cisco amp, probably needs a reboot after

rem runs Un_A.exe and exits

rem start /wait isn't useful
"c:\program files\Cisco\AMP\6.2.19\uninstall.exe" /S

powershell while (! ($proc = get-process Un_A -ea 0)) { sleep 1 }; $handle = $proc.handle; 'waiting'; wait-process Un_A; exit $proc.exitcode
js2010
  • 23,033
  • 6
  • 64
  • 66