Zorus / ZorusDeploymentScripts

This repository contains scripts to help out with agent deployments.
2 stars 4 forks source link

Script exit codes not set for Windows Install, Uninstall and Update scripts. #17

Open MichaelMcCool opened 1 month ago

MichaelMcCool commented 1 month ago

For the install, uninstall and update scripts, there is no explicit exit code returned to the spawning process. When these scripts are deployed via a RMM, the RMM can read the exit code to determine if the task executed successfully or not. This will require changes to the start-process lines as well as adding an exit command as the last line of the script.

Take the following line as an example:

Start-Process -FilePath $destination -ArgumentList "-s" -Wait

This should be changed to:

$Process = Start-Process -FilePath $destination -ArgumentList "-s" -Wait -NoNewWindow -PassThru

The NoNewWindow switch tells powershell not to spawn a separate process to run the program. This allows anything that might be written to the StdOut to be made available to the script output (not necessarily useful here but a good habit for writing scripts that will execute via a RMM). More importantly, the PassThru switch captures the process status and passes that back through to the $Process variable. This allows the referencing of the $Process variable to determine various things about the install/uninstall/updater process. The most important part here is the $Process.ExitCode value. This should be 0 if the installer was successful, and a non-zero value if the process encountered an error.

Lastly, the final line of the script should be:

exit $Process.ExitCode

This will pass the exit code value of the install/uninstall/update process as the value of the script itself. So if the program had an error, the RMM will get that value. If the exit code value is not explicitly defined, it will roll up from the end of the script and the last exit code generated by a command would be what is passed at the termination of the script. This can cause scripts not to terminate with an expected exit code

There are various places in the scripts where an exit command is sent in response to the installer failing to download or a token not being supplied to the script.

Write-Host "Downloading Zorus Deployment Agent..."
try
{
    $WebClient = New-Object System.Net.WebClient
    $WebClient.DownloadFile($source, $destination)
}
catch
{
    Write-Host "Failed to download installer. Exiting."
    Exit
}

In these cases the exit command should have a non-zero value supplied to it. i.e.:

Write-Host "Downloading Zorus Deployment Agent..."
try
{
    $WebClient = New-Object System.Net.WebClient
    $WebClient.DownloadFile($source, $destination)
}
catch
{
    Write-Host "Failed to download installer. Exiting."
    Exit 1
}

This will let the spawning process (most likely the RMM) see the script as failed rather than successful.

puntor commented 1 month ago

Agree with your recommendations @MichaelMcCool. I do think that to build on this we should uniquely identify error conditions within the script such as:

This would enable automation that could wait a few minutes and re-try in the case of a -2 Unable to Download error code, or submitting error logs for a -3 Installer Failure error code.

Returning the installer's exit code however is a bit trickier if we do this, and I would recommend returning a unique error code belonging to the script rather than one returned by the installer. This way the meaning of an error code is never ambiguous (an exit code of 1 could mean a deployment token isn't installed, or ERROR_INVALID_FUNCTION if returned by the installer for example).

if ($Process.ExitCode -ne 0)
{
    Write-Host "Failed to install with error code $($Process.ExitCode). Check the installer logs for further information"
    Exit -3
}
MichaelMcCool commented 1 month ago

In most cases the actual (non-zero) value for exit code probably doesn't matter too much as the RMMs I am familiar with don't actually look at the exit codes from a script other than seeing if it is a zero or non-zero value. To the RMM, a -3 is the same as a 1. However, when you are working with installers, the exit code can be interpreted by the script. For example, MSI installers use 3010 for a reboot required result and a 0 for success with no reboot required. In these instances, instead of the exit code simply being passed as the script exit code, a 3010 value needs to be captured and then have the script use 0 for the value, while any other exit code is passed as it. In these cases I would generally use something like the following:

$result = Start-Process -FilePath $destination -ArgumentList "/qn /norestart" -Wait -NoNewWindow -PassThru
if ($result.exitcode -eq 3010){
    write-output "`nSuccessfully installed MSI. A reboot is required to complete the process."
    if (($null -eq $exitcode) -or ($exitcode -eq 0)){
        $exitcode=0
    }
}
elseif ($result.exitcode -eq 0){
    write-output "`nSuccessfully installed MSI."
    if (($null -eq $exitcode) -or ($exitcode -eq 0)){
        $exitcode=$result.exitcode
    }
}
else {
    write-output "`nInstall of MSI failed. Exit code is $($result.exitcode)."
    if (($null -eq $exitcode) -or ($exitcode -eq 0)){
        $exitcode=$result.exitcode
    }
}

Exit $exitcode

So, if the installer, uninstaller or updater use unique exit codes for different error states, those would be good to identify and have the script display the correct error message, but for the script itself, it likely doesn't matter.