4

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
  • 490
  • 3
  • 10

2 Answers2

6

tl;dr

  • cmd.exe "/c start notepad.exe" (conceptually clearer: cmd.exe /c 'start notepad.exe') should result in asynchronous execution, given that cmd.exe's internal start command launches applications asynchronously by default.

    • The fact that it doesn't in the Windows PowerShell ISE - unlike in any other environment - should be considered an anomaly, but at this time this is somewhat of a moot point, given the obsolescent status of the ISE.
  • By contrast, cmd.exe "/c notepad.exe" (conceptually clearer: cmd.exe /c notepad.exe) should and does result in synchronous (blocking) execution, even in the PowerShell ISE, because cmd.exe /c as well as batch files invoke even GUI-subsystem applications synchronously (unlike when launched from an interactive cmd.exe session).

    • By contrast, PowerShell launches GUI-subsystem applications consistently asynchronously (whether interactively or from a script file), unless additional actions are taken - see below.

A note on Start-Process's syntax, given that the sample calls below do not show how to pass arguments to the target application: The best approach is to pass them in a single string, e.g.
Start-Process -Wait msiexec '/i some.msi' - see this answer

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

  • To start a GUI application asynchronously (return to the prompt right away / continue executing the script), execute it directly; e.g.:

    • 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 Start-Process with the -PassThru switch , which returns a [System.Diagnostic.Process] instance:

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

          • To wait synchronously for the process to terminate at some later point, use $process | Wait-Process or $process.WaitForExit() (to wait right away, use Start-Process' -Wait switch - see below).

            • Both Wait-Process and .WaitForExit() optionally support a timeout. With Wait-Process, you can specify a -Timeout value in seconds; a non-terminating error is reported if the process doesn't terminate within the timeout period, causing $? to reflect $False.
        • Once the process has exited, $process.ExitCode tells you the exit code (which may not tell you much in the case of a GUI application).

  • To start a GUI application synchronously (block until the application terminates), use Start-Process -Wait; e.g.:

    • Start-Process -Wait notepad.exe

      • -Wait tells Start-Process to wait until the process created terminates; this is the equivalent of start /wait notepad.exe in a cmd.exe session / batch file.

      • In the rare event that the GUI application reports a meaningful exit code (e.g, msiexec) , you can obtain it as follows:

        • $exitCode = (Start-Process -PassThru -Wait notepad.exe).ExitCode
    • There's a non-obvious alternative to Start-Process -Wait:

      • Pipe to Write-Output: This takes advantage of the fact that using a command in the non-final segment of a pipeline forces its synchronous execution. That is, if you pipe the GUI-subsystem application call to another command, PowerShell will wait for its completion. While which specific command you pipe to isn't important, the most versatile choice is Write-Output, though if you know that the GUI application produces no stdout output (GUI applications rarely do), you can also use Out-Null:[1]; e.g.:

        • notepad.exe | Write-Output
      • 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), this approach (unlike Start-Process -Wait) causes it to be reflected in the automatic $LASTEXITCODE variable.
    • Yet another alternative is to call via cmd /c: /c causes cmd.exe to wait even for GUI-subsystem applications to exit (even though it doesn't do so from a cmd.exe command prompt); $LASTEXITCODE is also properly set in this case.

      • While slightly slower due to the extra process (cmd.exe), the advantage is that you get more explicit control over the exact process command line in terms of quoting.

      • This can be helpful with applications that require nonstandard escaping of embedded " chars. or require partial double-quoting of arguments, such as msiexec - see this answer for more information.

    • Note that none of these approaches work for GUI applications that delegate launching to a different, preexisting process and then exit, which applies to applications such as:

      • Web browsers (e.g. Microsoft Edge, Chrome, Brave)
      • Some editors, such as Visual Studio Code
      • UWP applications such as Calculator (calc.exe)
      • In such cases, control is returned as soon as the originally launched process exits.

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.


[1] In rare cases, a GUI application may explicitly attach to the caller's console and write information to it; Write-Output surfaces this information. If you don't want to surface it, use Out-Null.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 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
1

Take out the start and it waits cmd /c notepad. Otherwise cmd /c start /wait notepad waits. cmd's start builtin runs in the background by default.

js2010
  • 23,033
  • 6
  • 64
  • 66