3

Consider the powershell command:

cmd.exe "/c start notepad.exe"

Using powershell.exe (console) this command completes immediately after starting the cmd/notepad process and does not wait for notepad/cmd to exit. If I want it to wait for completion I have to pipe the output to Out-Null.

Using the powershell ISE this command blocks execution until notepad/cmd is closed.

Further, if I use create a new process (of powershell.exe) in a script running in powershell.exe using System.Diagnostics.Process and redirect standard output the script now blocks execution until notepad/cmd is closed. If I don't redirect output it does not block execution.

But if I use c# to create a new process with the same settings/start info, and run the same script with redirected output it doesn't block execution.

I'm at a loss here. Obviously it has something to do with the setup of the execution and output and maybe "cmd.exe". I'm hoping someone understands what's fundamentally happening behind the scenes differently in these cases. I know the ISE and cmd.exe isn't fully supported but the I don't know why the other 3 aren't consistent.

Why do the scripts not run with the same behavior (especially the powershell console ones) and how do I get them to?


Edit:

After some troubleshooting I was able to get all the powershell.exe versions to run the same (the ISE isn't of importance to me). The odd ball where cmd.exe "/c start notepad.exe" blocks execution is when a new process is created in powershell to run powershell.exe using System.Diagnostics.Process. If output is redirected (the reason I was using System.Diagnostics.Process in the first place, Start-Process doesn't support redirecting except to a file) then calling WaitForExit() on the process object causes the blocking to occur.

Simply substituting WaitForExit() with Wait-Process (and the process ID) causes the powershell script running in the new powershell.exe to execute as expected and exit after running the command. Hopefully this information combined with @mklement0 answer will be sufficient for anyone else having similar problems.

playsted
  • 450
  • 3
  • 9

1 Answers1

3

To get predictable behavior in both the regular console and in the ISE:

  • Start a GUI application asynchronously (return to the prompt right away / continue executing the script):

    • notepad.exe

    • Invoking Notepad directly makes it run asynchronously, because it is a GUI-subsystem application.

    • If you still want to track the process and later check whether it is still running and what its exit code was on termination, use -PassThru, which makes Start-Process return a [System.Diagnostic.Process] instance:

      • $process = Start-Process -PassThru notepad.exe
        • $process.HasExited later tells you whether the process is still running.

        • Once it has exited, $process.ExitCode tells you the exit code (which may not tell you much in the case of a GUI application).

        • To wait synchronously (at some point):

          • Use Wait-Process $process.ID to wait (indefinitely) for the process to terminate.
          • Add a -Timeout value in seconds to limit the waiting period; a non-terminating error is reported if the process doesn't terminate within the timeout period, causing $? to reflect $False.
  • Start a GUI application synchronously (block until the application terminates):

    • Start-Process -Wait notepad.exe

    • -Wait tells Start-Process to wait until the process created terminates; this is the equivalent of cmd /c 'start /wait notepad.exe'.

    • There's a non-obvious shortcut to Start-Process -Wait: you can repurpose the Out-Null cmdlet to take advantage of the fact that piping invocation of a program to it makes Out-Null to wait for the program's termination (a GUI application has no stdout or stderr output, so there's nothing for Out-Null to discard; the only effect is synchronous invocation):

      • notepad.exe | Out-Null

      • In fact, this approach has two advantages:

        • If arguments must be passed to the GUI application, they can be passed directly, as usual - rather than indirectly, via Start-Process's -ArgumentList parameter.
        • In the (rare) event that a GUI application reports a meaningful process exit code (e.g, msiexec.exe), the Out-Null approach (unlike Start-Process -Wait) causes it to be reflected in the automatic $LASTEXITCODE variable.

Note that for console-subsystem applications (e.g., findstr.exe), synchronous execution is the default; Start-Process is only needed for GUI applications (and for special situations, such as wanting to run an application in a new console window or with elevation (run as admin)).

To run a console application or shell command asynchronously (without opening a new console window), you have the following options:

  • [Preferred] Use Start-Job kick off the command, and Receive-Job to receive its output / success status later.

    • $j = Start-Job { sleep 2; 'hi' }

    • To synchronously wait for this job to finish (and remove it automatically), use
      Receive-Job -Wait -AutoRemoveJob $j

    • In PowerShell (Core) 6+:

      • You can use the simpler ... & syntax (as in POSIX-like Unix shells such as bash) in lieu of Start-Job; e.g.:

        • $j = & { sleep 2; 'hi!' } &
      • Better yet, you can use the lighter-weight, faster Start-ThreadJob cmdlet, which uses threads for concurrency, but otherwise seamlessly integrates with the other *-Job cmdlets (note that it has no short-hand syntax):

        • $j = Start-ThreadJob { sleep 2; 'hi' }
  • [Not advisable] You can use something like Start-Process -NoNewWindow powershell -Args ..., but it is ill-advised:

    • Passing a shell command to powershell via Start-Process requires intricate quoting based on obscure rules - see this answer of mine for background information.
    • Any stdout and stderr output produced by the application / shell command will by default arrive asynchronously in the current console, interspersed with what you're doing interactively.
    • While you can redirect these streams to files with RedirectStandardOutput and -RedirectStandardError (and stdin input via -RedirectStandardInput) and you can use -PassThru to obtain a process-information object to determine the status and exit code of the process, Start-Job and Receive-Job are a simpler, more convenient alternative.

P.S.: I don't have an explanation for why cmd.exe "/c start notepad.exe" is blocking in the ISE but not in the regular console. Given the solutions above, however, getting to the bottom of this discrepancy may not be needed.

mklement0
  • 312,089
  • 56
  • 508
  • 622
  • Thanks for the info, although doesn't get down to the differences I saw. If we throw out the powershell ISE I was able to get them all the behave the same. After troubleshooting more I determined using `$process.WaitForExit()` on a new powershell process caused some weird deadlock if output was redirected. If I instead did a `Wait-Process -Id $process.Id` it behaved the same as the other scenarios. – playsted Jul 16 '17 at 21:07
  • @playsted: My point was that with the solutions above the differences become _moot_. If you still see differences: please provide a concrete example. – mklement0 Jul 16 '17 at 21:25
  • I don't have the ability to modify the powershell I'm running. They are build scripts written and maintained by other people. I just need to make sure they behave the same as if they were being run directly. Spawning in a new process is necessary under some circumstances (eg. if it needs to run under a different user account). – playsted Jul 16 '17 at 21:37
  • @playsted: As a general rule: unless there are performance problems, stick with PowerShell's cmdlets (as opposed to calling methods on .NET object). – mklement0 Jul 16 '17 at 21:38
  • @playsted: Understood. I've incorporated `Wait-Process` in the answer. As the answer stands now: is your scenario not covered in a way that works predictably in both the ISE and the regular console? Note that for simply _running_ prepackaged functionality, I'd advise against using the ISE, given that repeated invocations of a given script run in the _same session_, with previous invocations potentially causing side effects. – mklement0 Jul 16 '17 at 21:42
  • @playsted: As for running under a different user account: both `Start-Job` and `Start-Process` have a `-Credential` parameter; beware of issues relating to what the current directory is. – mklement0 Jul 16 '17 at 21:54
  • I was only using the ISE for experimentation and noticed it behaved differently as well. I was using the .Net `Process` object as I needed custom output redirection. AFIAK all the other methods only support redirecting to a file. – playsted Jul 16 '17 at 22:11
  • @playsted: Re not needing files as redirection targets: If you use `Start-Job` / `... &`, `Receive-Job` actually returns _objects_ to you, as you would receive if you ran the command directly (almost - serialization and deserialization are involved). However, as of PSv6-beta.4, there is a [known bug](https://github.com/PowerShell/PowerShell/issues/3130) with respect to capturing the _error-stream_ output (among others) – mklement0 Jul 18 '17 at 19:25