1

Hello i'm having issues with this line of code I been working on, for some reason is not working anymore I'm trying to invoke the MSI installer silently and wait till it's done so I can execute next line of code I had it working but now is not, I tried executing start-process and using the -wait parameter but it's returning with an error message, and when I call "msiexec.exe" it doesn't have a -wait parameter

msiexec.exe /i "C:\MagtekCC\Files\Java\jre1.8.0_144.msi" /QN TRANSFORMS="jre1.8.0_144.mst" /L*V "C:\Temp\msilog.log"
Write-Host -NoNewLine "Done.`n" -ForegroundColor Cyan
Start-Sleep -s 2



  start-process msiexec.exe  "/i C:\MagtekCC\Files\Java\jre1.8.0_144.msi" -wait /QN TRANSFORMS="jre1.8.0_144.mst" /L*V "C:\Temp\msilog.log" 
     Write-Host -NoNewLine "Done.`n" -ForegroundColor Cyan
      Start-Sleep -s 2

> Start-Process : A positional parameter cannot be found that accepts argument '/QN'.
At line:1 char:1

    + start-process msiexec.exe  "/i C:\MagtekCC\Files\Java\jre1.8.0_144.ms ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidArgument: (:) [Start-Process], ParameterBindingException
        + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.StartProcessCommand
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Does this answer your question? [Java Silent installer command](https://stackoverflow.com/questions/68914609/java-silent-installer-command) – codewario Sep 08 '21 at 15:28

2 Answers2

1

msiexec.exe is a GUI tool, and Windows CLI (Command Prompt or PowerShell) will not block by default. Conversely, console applications will block by default. In PowerShell, you can alter this default behavior (as well as gain additional control and information about the child process) by using Start-Process.

Note: This answer focuses on Start-Process but the equivalent way to force a program to wait before continuing in the Command Prompt or .bat script is

start "title" /WAIT [command/program] [parameters]

Note that the "title" can be a blank string "".


Since you've updated your question with your Start-Process attempt, it failed because you provided the -Wait parameter in the middle of the -ArgumentList arguments. -ArgumentList is defined specially, so all remaining positional arguments following it are considered part of it. The problem is when you stick another named parameter there, you've now broken the chain of arguments. The solution is shown below, but make sure you provide the -ArgumentList arguments without separating them with other parameters to Start-Process:

$msiExecArgs = @(
  '/i',
  'C:\MagtekCC\Files\Java\jre1.8.0_144.msi',
  '/QN',
  '/L*V', 'C:\Temp\msilog.log',
  'TRANSFORMS="jre1.8.0_144.mst'
) -join ' '

Start-Process msiexec -Wait -ArgumentList $msiExecArgs

This works in the following way:

  • Create an array of msiexec arguments. This can be an array of each space-separated argument you would normally pass to msiexec.exe in the same order you would specify interactively. Alternatively, you can also provide it as a single string. Read the sub-point below to understand why I declare this as an array only to -join it to a string.
    • Due to this bug providing arguments as a string is recommended, and is why I took the array arguments and use -join to create a space-delimited string out of them. The array code is a bit easier to manage as now we don't have to use += (gross and not recommended) or StringBuilder (recommended over += but overkill for building an array of arguments) and we still get the string which avoids the bug.
  • -Wait tells Start-Process to block further execution until the program exits.
  • -ArgumentList is used to pass our array of $msiExecArgs arguments to msiexec.

Note: A prior revision of this answer incorrectly stated that Start-Process needs
-Wait:$false in order not to block on execution for CLI applications. While the default behavior for external programs is to block for CLI and not block for GUI, Start-Process actually ignores this and does not block by default regardless of the program type. Use the -Wait parameter to block, and omit or provide -Wait:$false not to block.

Conversely, if you wanted to run a CLI program in the background, do the same thing as above but omit the -Wait parameter instead (you will probably want to check the result as well which requires a little extra elbow-grease with -PassThru so we can check whether it's done and its exit code):

$p = Start-Process ping -PassThru -ArgumentList www.google.com

# Do other stuff, then check the result when ready
Write-Host "Checking that ping.exe has finished"
while( !$p.HasExited ) {

  # Wait 1 second before checking again
  Start-Sleep 1
}

if( $p.ExitCode -ne 0 ) {
  Write-Error "ping failed with exit code $($p.ExitCode)"
}
  • Using ping.exe as an example, we run it through Start-Process. ping.exe is a console application, and would normally block until execution completes.
  • -PassThru is used to tell Start-Process to return the Process object. Start-Process does not return anything by default, but in this case we later want to check that this is done running and whether it succeeded. This is optional but required to check the result later on.
  • The while loop waits for $p.HasExited to return true. Process.HasExited is used to determine whether the process execution has finished, regardless of success or failure.
  • The if statement checks that the exit code of the Process object returned 0. As a matter of standard, a program can normally be considered successful if it returns 0, though you can specify an array of acceptable exit codes should a program use other codes for different end states.

There are other things you can do with the process object as well, play around with it. However, while Start-Process can be a powerful tool it is often overused, unnecessarily complicating code which does not need it. While your use case for MSIEXEC is a perfect example of when to use it, oftentimes you can simplify the use of Start-Process by omitting it altogether and relying instead on the call operator & to execute programs.

codewario
  • 19,553
  • 20
  • 90
  • 159
  • thanks for the lengthy response as well as being informative very helpful I appreciated a lot, I went ahead and test it and it seems to be working as intended but I'm curious to know why mine using the start process msiexec.exe didn't. I have uploaded the error code on the main post. –  Sep 07 '21 at 21:23
  • Two reasons: you stuck -Wait between msiexec arguments, it should be a direct parameter off of Start-Process, and also you did not provide the -ArgumentList parameter as an array of strings. See the code block where I set $msiExecArgs and the explanation following it, and compare it to your code. If you still don't understand, I will try to explain further when I'm not on mobile. – codewario Sep 08 '21 at 02:18
1

msiexec.exe is unusual in that, while it has an extensive CLI (command-line interface) with many supported parameters, it is a GUI-subsystem executable that runs independently of any calling console.

PowerShell runs GUI-subsystem executables asynchronously when you invoke them directly; that is, unlike with console-subsystem executables, it doesn't wait for the executable to terminate before continuing.

While Start-Process -Wait is indeed the typical way to invoke a GUI-subsystem executable synchronously, there is also a convenient shortcut:

If you pipe to Write-Output:[1]

  • PowerShell will wait for the executable('s process) to terminate, even if it is a GUI-subsystem application.

    • Note: If you know that the GUI application produces no stdout output - and not doing so is typical - you can also pipe to Wait-Process for better conceptual clarity or, if you want to suppress any stdout output, pipe to Out-Null (again, see footnote [1])
  • Additionally, the automatic $LASTEXITCODE variable will properly reflect the process' exit code (which not many GUI-subsystem executables meaningfully set, but misexec.exe does), allowing you to infer success (exit code 0) vs. failure (any nonzero exit code), by convention.

    • That said, msiexec.exe uses two nonzero exit codes that also indicate - qualified - success (see the docs for a full list of exit / error codes and their description):
      • 3010 (ERROR_SUCCESS_REBOOT_REQUIRED)
      • 1641 (ERROR_SUCCESS_REBOOT_INITIATED)

Therefore, you can use the following:

# Note the `| Write-Output` at the end of the line, which ensures *synchronous* execution.
msiexec.exe /i "C:\MagtekCC\Files\Java\jre1.8.0_144.msi" /QN TRANSFORMS="jre1.8.0_144.mst" /L*V "C:\Temp\msilog.log" | Write-Output
$exitCode = $LASTEXITCODE

Write-Host -ForegroundColor Cyan "Installation finished with exit code $exitCode."

Alternatively, pass the msiexec command line to cmd.exe via its /c parameter, which:

  • implicitly makes the call synchronous (and also sets $LASTEXITCODE)
  • preserves the original quoting, which is necessary if you pass property values with embedded spaces, e.g. MYPROP="foo bar" is then passed exactly as such, whereas PowerShell would convert this argument to "MYPROP=foo bar" when it rebuilds the actual command line behind the scenes, which msiexec.exe doesn't recognize.
cmd /c @"
msiexec.exe /i "C:\MagtekCC\Files\Java\jre1.8.0_144.msi" /QN TRANSFORMS="jre1.8.0_144.mst" /L*V "C:\Temp\msilog.log"
"@
$exitCode = $LASTEXITCODE

Write-Host -ForegroundColor Cyan "Installation finished with exit code $exitCode."

Note the use of a here-string (@"<newline>...<newline>"@ in this example to make embedded quoting easier.


The above solutions are more convenient alternatives to the following Start-Process -Wait solution, which additionally uses -PassThru to pass an object representing the launched process through, so that its .ExitCode property can be checked:

$exitCode = (Start-Process -Wait -NoNewWindow -PassThru msiexec.exe @'
/i "C:\MagtekCC\Files\Java\jre1.8.0_144.msi" /QN TRANSFORMS="jre1.8.0_144.mst" /L*V "C:\Temp\msilog.log"
'@).ExitCode

Write-Host -ForegroundColor Cyan "Installation finished with exit code $exitCode."

Note how all arguments for misexex.exe are passed as a single string to Start-Process (using a here-string for readability here), which positionally binds to the -ArgumentList parameter.

While the -ArgumentList parameter technically accepts an array of arguments - i.e. allows you to pass arguments individually - a long-standing bugs makes that unadvisable - see this answer.


As for what you tried:

Your Start-Process -Wait call failed, because you ended up passing more than two positional arguments:

start-process msiexec.exe  "/i ..." -wait /QN ...
  • msiexec.exe positionally binds to the -FilePath parameter.
  • Only the "/i ..." argument positionally argument binds to the -ArgumentList parameter.
  • Since no other Start-Process parameters support positional binding, Start-Process doesn't know what to do with /QN (and all subsequent positional (unnamed) arguments), which is what the error message in your question indicates.

Note: If you did want to pass parameters individually - which is best avoided, as stated above - you'd have to pass them as an array, separated by ,
(Start-Process -Wait msiexec.exe '/i', ...)


[1] Note that any command in a subsequent pipeline segment works to ensure synchronous execution, because PowerShell always has to wait for the preceding segment's command to terminate, so as to ensure that all input has been received; While it ultimately doesn't matter what command you choose, Write-Output, which in this case will typically be a no-op, has the advantage of passing stdout output through so that it becomes visible / can be captured, if the GUI application at hand produces such output, which is rare (append 2>&1 to the GUI-application call to also capture stderr output). As Bender the Greatest points out, some GUI applications do so in order to produce debugging output, by explicitly attaching to the caller's console. If you'd rather suppress this information, pipe to Out-Null instead.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Neat trick with piping to `Out-Null` to block execution. Is this behavior only with `Out-Null` or does this extend to piping the down the pipeline in general? E.g. would piping to `Write-Output` or `Write-Verbose` work? – codewario Sep 08 '21 at 14:26
  • That's not necessarily true. GUI applications can be made to write to STDOUT and STDERR, and if run in the console will write output to the console. I used to work on software which did this. Although admittedly I may be remembering this backwards, that project might have been built as a console application that then invoked GUI components; which would support what you said as it would still technically be built as a console application. – codewario Sep 08 '21 at 14:38
  • However, [is it still possible](https://stackoverflow.com/a/9943162/584676) to write to the console using an executable built Windows Application – codewario Sep 08 '21 at 14:41
  • I've done this in two cases: programs which have already been defined as GUI applications which then need to later provide console output. Changing the program type could be considered a breaking change in some contexts in part due to the differing execution behavior. The second case is to provide debug or trace level output to a program already defined as a GUI application. Usually I would agree and not recommend this approach, but the suite of products I was working on at the time was somewhat bespoke functionality and changing the program to console broke some things for key customers. – codewario Sep 08 '21 at 14:53
  • So while uncommon, and maybe not recommended, piping to something other than `Out-Null` would catch these (technically correct, the best kind of "correct" `;)`) edge cases. – codewario Sep 08 '21 at 14:53
  • I don't understand; external programs STDOUT get written to the success stream. Why would it matter whether it's a GUI or console application as long as you either redirect process output (not possible directly with `Start-Process`, you would need to go full `[Thread]::Start(ProcessStartInfo)` for this) or specify `Start-Process -NoNewWindow`? – codewario Sep 08 '21 at 15:04
  • @BendertheGreatest, I stand corrected: stdout output from a directly invoked GUI that is forced into synchronous execution with an additional pipeline segment _is_ passed through the success output stream, so I've changed the answer to recommend `Write-Output` instead - please see the footnote. Note that with `Start-Process` you'd use `-RedirectStandardOutput` / `-RedirectStandardErr` to _capture_ such output; to simply _see_ it in the current console (without being able to capture it), yes, `-NoNewWindow` works. – mklement0 Sep 08 '21 at 16:08
  • Now I recall; `-RedirectStandard*` outputs to a file. This works but I prefer variables especially if I need to run a process multiple times. Oftentimes I absolutely don't want output going to a file and would rather keep it in memory. `[Process]::Start(ProcessStartInfo)` lets you read STDOUT and STDERR from the returned `Process` object instead. I can't recall, but I *think* I've tried outputting to a `Variable:\ ` path and was met with an error. – codewario Sep 08 '21 at 20:03
  • Confirmed, using `Start-Process ping -ArgumentList google.com -RedirectStandardOutput Variable:\pingOutput` is met with an error: `Cannot open file because the current provider (Microsoft.PowerShell.Core\Variable) cannot open a file`. My guess is `Start-Process` explicitly opens a `FileStream` which the `Variable` provider doesn't support. – codewario Sep 08 '21 at 20:11
  • 1
    @BendertheGreatest, yes, these parameters only support capturing stream output _in files_ (and you can't _merge_ streams unless you call _via a shell_ and make `2>&1` part of the shell command). It would indeed be nice to be able to capture in _variables_, as direct execution can do (but not separately with `2>`);allowing namespace notation (`variable:output`) wherever file paths are expected would make for a great _future enhancement_, discussed in [GitHub issue #5184](https://github.com/PowerShell/PowerShell/issues/5184) and [#4332](https://github.com/PowerShell/PowerShell/issues/4332) – mklement0 Sep 08 '21 at 20:32