Given the overall similarities and program flow of the various CMD and VBS scripts used in package deployment, I decided to combine their purposes into a single PowerShell script that uses a set of options to specify the action to be undertaken. Like the Install.cmd and Update.cmd scripts before this one, this script is designed to be run in a multiple of ways - interactively by clicking on it, via the command line, via MDT, and via SCCM.

This script will quit PowerShell and return an exit code to the calling process (i.e., SCCM or MDT). To call this script, the command line should be:

PowerShell.exe -ExecutionPolicy Bypass -File .\Install.ps1 -Install

When running from within SCCM, if the client policy is to bypass script signing anyway, you can skip the PowerShell.exe -ExecutionPolicy Bypass -File bit and just have the Install.ps1 -Install portion. The first part is, however, required under MDT since the execution policy, by default, requires signing (or is outright disabled).

Script Top

The script was designed so that regular updates to the application can be made easier by just updating the top portion of the script; everything else is, generally, going to stay the same. When you first create the script for a particular application, there are functions that you need to update (denoted by !!_UPDATE_THIS_!! tags).

However, the top of the script just has some very simple variables that end up getting used. For this particular example, we'll be repackaging the Java Runtime Environment. To do that, we've already obtained the MSI file from the web downloaded installer.

Parameters

param ([switch] $Install, [alias("Upgrade")][switch] $Update, [switch] $Uninstall, [switch] $CheckVersion)

We start off with the script command-line options. Only one of these can be selected. If none are (or more than one are), the script will prompt the user for which action to perform.

Global Variables

# Update these global variables, used throughout.
$AppName='Java Runtime Environment'
$AppVersion='8.0.1110'
$CompanyName='My Company'

Next, we have a pair of variables, $AppName, and $AppVersion, which are meant to be the two variables that you adjust regularly. These specify the name of the application and the version number that's used as a part of version checking. The $CompanyName variable is your own company's name. This is used in some branding, but, more importantly, is used to create a Windows event log that can be used for troubleshooting the deployment.

Calculated Variables

$AppMainVersion = $AppVersion.split('.')
$InstallPackage=("jre1." + $AppMainVersion[0] + "." + $AppMainVersion[1] + "_" + $AppMainVersion[2].substring(0,($AppMainVersion[2].Length-1)) + ".msi")

Next, we have a couple of calculated fields. Often the installation package is an amalgam of the application version and some regularly defined text. This breaks apart the $AppVersion variable and constitutes it into the $InstallPackage variable.

Ensure Administrator Privileges

# Be sure that we're running as an elevated admin account; if not, elevate us.
# Get the ID and security principal of the current user account
$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)

# Get the security principal for the admin role
$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator

# Check to see if we are currently running as admin
if ($myWindowsPrincipal.IsInRole($adminRole)) {
  # We are elevated - nothing more to do
} else {
  # We are not running as admin - relaunch as admin

  # Create a new process object that starts PowerShell
  $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell"

  # Specify the current script path and name as a parameter
  $newProcess.Arguments = $myInvocation.MyCommand.Definition

  # Indicate that the process should be elevated
  $newProcess.Verb = "runas"

  # Start the new process
  $Process = [System.Diagnostics.Process]::Start($newProcess);
  $Process.WaitForExit()
  $EC = $Process.ExitCode

  # Exit from the current, unelevated process
  $Host.SetShouldExit($EC)
  exit $EC
}

If the script is executed directly (i.e., right-click and select Run in PowerShell), it will run in the user's context. This code ensures that we run the script elevated. As written, however, any command-line options present will get stripped away, but the script will ask the user for the proper action to perform. As the user has to be there to acknowledge and approve the elevation anyway, this isn't a bit deal.

Since the script runs as a new process when it is elevated, we pass back the exit code returned from the elevated script and exit out so that we don't try to run the rest as the regular user account.

Ensure Working Directory

# Make sure that we're in the same directory as our script to start
Set-Location (Get-Item -Path $myInvocation.MyCommand.Path).DirectoryName

I found, that when the script is elevated to an admin account using the code above, that the working directory changes to C:\Windows\System32. Since we make calls to, what the script assumes, are local files, this snippet will ensure that we're in the path of the script itself. If the script is called via a UNC path, this could have issues, depending upon the installer and how they work.

Logging Functions

$EventMessageTop = ($AppName + ' ' + $AppVersion + "`r`n`r`n")

Function myEnsureLogExists {
  if ((Get-EventLog -List | where { $_.Log -eq ($CompanyName + ' Mgmt') }).Length -eq 0) {
    New-EventLog -LogName ($CompanyName + ' Mgmt') -Source 'Install'
    Limit-EventLog -LogName ($CompanyName + ' Mgmt') -OverflowAction OverwriteAsNeeded -MaximumSize 524288
  }
  if (!([System.Diagnostics.EventLog]::SourceExists("Install"))) {
    New-EventLog -LogName ($CompanyName + ' Mgmt') -Source 'Install'
  }
}

Function myLogThis {
  param ([Int32]$EventId=0, [String]$Message="", [String]$EntryType="Information")

  Write-EventLog -LogName ($CompanyName + ' Mgmt') -Source 'Install' -EventId $EventId -EntryType $EntryType -Message ($EventMessageTop + $Message)
}

To ensure that we have logging available throughout the script, there are a couple of functions in place to facilitate this. The $EventMessageTop variable is just the textual "header" within the event message that indicates that the script was trying to perform an action for the given application/version.

The myEnsureLogExists function is called only once, near the beginning of the script's actual flow, to ensure that the log is created on the computer. This shows up in Windows Event Viewer under Applications and Services Logs.

The myLogThis function is the one called to actually add entries to that log. It takes parameters for the event ID and message body and, optionally, for the event type (defaulting to Information if nothing is specified).

User Defined Application Install Functions

In the following sections, there are three functions that must be defined and are specific to the application being installed. From a practical point of view, once defined, these functions shouldn't need to change version to version as you re-use the script for the application.

  • myUninstallOldApplication - This function closes processes and uninstalls the application.
  • myInstallNewApplication - This function handles the basic installation of the new application.
  • myConfigureApplication - If the installation succeeded, this function will allow you to add additional files or registry settings in order to configure the application to meet your needs

myUninstallOldApplication Function

Function myUninstallOldApplication {
  $FunctionTop = "myUninstallOldApplication`r`n`r`n"
  myLogThis -EventId 40 -Message ($FunctionTop + 'Starting function')
  Write-Host "`r`nUninstalling old version of $AppName..."

  # Terminate Firefox processes
  myLogThis -EventId 41 -Message ($FunctionTop + 'Searching for firefox.exe processes that need to be terminated')
  $Processes = Get-Process -Name firefox -ErrorAction SilentlyContinue
  if ($Processes.Length -ne $null) {
    Write-Host -ForegroundColor Yellow "  Terminating running Firefox processes..."
    myLogThis -EventId 42 -Message ($FunctionTop + "Terminating " + $Processes.Length + " processes")
    foreach ($Process in $Processes) {
      myLogThis -EventId 43 -Message ($FunctionTop + "Terminating process ID " + $Process.Id + "`r`n" + $Process.MainModule.FileName + "`r`n" + $Process.MainWindowTitle + "`r`n" + $Process.Product + "`r`n" + $Process.ProductVersion)
      $Process.Kill()
    }
  }

  # Terminate Chrome processes
  myLogThis -EventId 41 -Message ($FunctionTop + 'Searching for chrome.exe processes that need to be terminated')
  $Processes = Get-Process -Name chrome -ErrorAction SilentlyContinue
  if ($Processes.Length -ne $null) {
    Write-Host -ForegroundColor Yellow "  Terminating running Chrome processes..."
    myLogThis -EventId 42 -Message ($FunctionTop + "Terminating " + $Processes.Length + " processes")
    foreach ($Process in $Processes) {
      myLogThis -EventId 43 -Message ($FunctionTop + "Terminating process ID " + $Process.Id + "`r`n" + $Process.MainModule.FileName + "`r`n" + $Process.MainWindowTitle + "`r`n" + $Process.Product + "`r`n" + $Process.ProductVersion)
      $Process.Kill()
    }
  }

  # Terminate Internet Explorer processes
  myLogThis -EventId 41 -Message ($FunctionTop + 'Searching for iexplore.exe processes that need to be terminated')
  $Processes = Get-Process -Name iexplore -ErrorAction SilentlyContinue
  if ($Processes.Length -ne $null) {
    Write-Host -ForegroundColor Yellow "  Terminating running Internet Explorer processes..."
    myLogThis -EventId 42 -Message ($FunctionTop + "Terminating " + $Processes.Length + " processes")
    foreach ($Process in $Processes) {
      myLogThis -EventId 43 -Message ($FunctionTop + "Terminating process ID " + $Process.Id + "`r`n" + $Process.MainModule.FileName + "`r`n" + $Process.MainWindowTitle + "`r`n" + $Process.Product + "`r`n" + $Process.ProductVersion)
      $Process.Kill()
    }
  }

  Start-Sleep -Seconds 1

  # Look through the registry and gather all installations of Java and their UninstallString
  myLogThis -EventId 44 -Message ($FunctionTop + 'Looking for MSI registered instances to uninstall...')
  Write-Host "  Looking for installed instances..."
  $qtVer = Get-ChildItem -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall,HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | Get-ItemProperty | Where-Object { $_.DisplayName -match ".*Java.*Update.*" } | Select-Object -Property DisplayName,UninstallString
  if ($qtVer -eq $null) {
    Write-Host "    No instances found"
    myLogThis -EventId 49 -Message ($FunctionTop + "No installed instances found")
    Return 0
  }

  # Set a default return value to compare against
  $Result = 0

  # Iterate through all of the installed instances and call their uninstallstring,
  # modified to execute an uninstall quietly and without restarting.
  foreach ($ver in $qtVer) {
    if ($ver.UninstallString) {
      $uninst = $ver.UninstallString
      $uninst = $uninst -replace "/I", "/x "
      myLogThis -EventId 45 -Message ($FunctionTop + "Uninstalling " + $ver.DisplayName + " using $uninst")
      $Process = Start-Process cmd -ArgumentList "/c $uninst /quiet /norestart" -NoNewWindow -Wait -PassThru
      myLogThis -EventId 46 -Message ($FunctionTop + "Uninstall resulted in exit code " + $Process.ExitCode)
      
      # If the process exited with a code higher than our base (such as 1619 or 3010), use that as our function exit code
      if ($Process.ExitCode -gt $Result) { $Result = $Process.ExitCode }
    }
  }
  
  # Return our final result
  Return $Result
}

Note that this example is for the uninstallation of the Java Runtime Environment.

The function works to close out browsers that may be using the JRE (Firefox, Chrome, and Internet Explorer), then it looks to see if it can find, in the registry, the MSI reference for the installed version. It does this in order to obtain the manufacturer's uninstall string.

Then, we iterate through the results (in case you have both 32-bit and 64-bit JRE's installed or more than one instance of JRE) and proceeds to call out to msiexec to uninstall them. If any of the results are non-zero, it returns the greatest result code back to the calling process (i.e., if one of them fails or asks for a reboot, then that will make its way back to the calling process).

myInstallNewApplication Function

Function myInstallNewApplication {
  $FunctionTop = "myInstallNewApplication`r`n`r`n"
  myLogThis -EventId 50 -Message ($FunctionTop + 'Starting function')

  # Ensure that we can get to the actual installer file
  if ((Test-Path -Path $InstallPackage) -eq $False) {
    myLogThis -EventId 59 -Message ($FunctionTop + "Unable to find $InstallPackage") -EventType "Error"
    Return 2
  }

  # Perform the main app installation and return the result code
  myLogThis -EventId 51 -Message ($FunctionTop + "Calling out to $InstallPackage to install $AppName")
  Write-Host "`r`nInstalling $AppName..."
  $Process = Start-Process -FilePath cmd -ArgumentList "/c msiexec /i $InstallPackage /quiet /norestart JU=0 JAVAUPDATE=0 AUTOUPDATECHECK=0 RebootYesNo=No WEB_JAVA=1" -Wait -PassThru -NoNewWindow
  myLogThis -EventId 52 -Message ($FunctionTop + "Install resulted in exit code " + $Process.ExitCode)
  Write-Host ("  Install completed with exit code " + $Process.ExitCode)
  Return $Process.ExitCode
}

This is the application that does one thing - install the application. If there are additional applications to be installed, replicate this function or at least make dependencies based on result code to ensure that the installation succeeds completely and not just partially.

myConfigureApplication Function

Function myConfigureApplication {
  $FunctionTop = "myConfigureApplication`r`n`r`n"
  myLogThis -EventId 60 -Message ($FunctionTop + 'Starting function')

  Write-Host "`r`nConfiguring $AppName..."
  
  # Add policy to prevent update to 64-bit registry
  $RegistryPath = "HKLM:\SOFTWARE\JavaSoft\Java Update\Policy"; $Name = "EnableJavaUpdate"; $Value = 0
  if (!(Test-Path $RegistryPath)) { New-Item -Path $RegistryPath -Force | Out-Null }
  New-ItemProperty -Path $RegistryPath -Name $Name -Value $Value -PropertyType DWORD -Force | Out-Null

  # Add policy to prevent update to 32-bit registry
  $RegistryPath = "HKLM:\SOFTWARE\Wow6432Node\JavaSoft\Java Update\Policy"
  if (!(Test-Path $RegistryPath)) { New-Item -Path $RegistryPath -Force | Out-Null }
  New-ItemProperty -Path $RegistryPath -Name $Name -Value $Value -PropertyType DWORD -Force | Out-Null

  # Remove update reminder applet from 64-bit registry
  $RegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; $Name = "SunJavaUpdateSched"
  if (Test-Path $RegistryPath) {
    $Task = Get-ItemProperty -Path $RegistryPath -Name $Name -ErrorAction SilentlyContinue
    if ($Task -ne $null) { Remove-ItemProperty -Path $RegistryPath -Name $Name -Force }
  }

  # Remove update reminder applet from 32-bit registry
  $RegistryPath = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Run"
  if (Test-Path $RegistryPath) {
    $Task = Get-ItemProperty -Path $RegistryPath -Name $Name -ErrorAction SilentlyContinue
    if ($Task -ne $null) { Remove-ItemProperty -Path $RegistryPath -Name $Name -Force }
  }
}

Once we know that the main application install is completed successfully, we call this function to configure any settings or add files necessary to ensure that the application operates as desired.

Again, since this is the JRE, we add some policy statements to prevent self update (which requires elevation and our users don't have that right) and remove the update reminder applet from the task tray.

Installation Status and Version

As a part of the script, we check to see if the application is installed and, if so, what version is installed. This script will not attempt a downgrade - only upgrades. If the version that we're installing is already installed, do nothing (no repairs).

To do that, we have a selection of helper functions that are called in the script's main body to get version data from the computer and then evaluate it.

myVersionCleanup Function

Function myVersionCleanup {
  param ([Parameter(Mandatory=$True)][String]$Version)
  $FunctionTop = "myVersionCleanup`r`n`r`n"
  myLogThis -EventId 21 -Message ($FunctionTop + 'Version : ' + $Version)

  myLogThis -EventId 22 -Message 'Converting spaces to periods'
  $Cleaned = $Version -replace(" ", ".")

  myLogThis -EventId 29 -Message ('Returning cleaned version string : ' + $Cleaned)
  Return $Cleaned
}

This function acts as a consistent way to clean up version strings into comparable sections. When we compare versions, we break them apart at periods and compare those as number strings (more on that later). To facilitate that, this function converts other separators into periods.

myGetCurrentVersionFromRegistry Function

Function myGetCurrentVersionFromRegistry {
  param ([Parameter(Mandatory=$True)][String]$KeyName, [Parameter(Mandatory=$True)][String]$VersionValue)
  $FunctionTop = "myGetCurrentVersionFromRegistry`r`n`r`n"
  myLogThis -EventId 11 -Message ($FunctionTop + 'KeyName : ' + $KeyName + "`r`n" + 'VersionValue : ' + $VersionValue)
  Write-Host -NoNewLine "Looking for installation evidence in registry under software key "
  Write-Host -ForegroundColor White $KeyName ":" $VersionValue

  # Attempt to determine if we're 64-bit, 32-bit, or not installed at all
  myLogThis -EventId 12 -Message ($FunctionTop + 'Testing for 64-bit registry key existence')
  $Result = Test-Path ('HKLM:\SOFTWARE\' + $KeyName)
  if ($Result) {
    myLogThis -EventId 13 -Message ($FunctionTop + 'Found key in 64-bit registry')
    Write-Host "  Found installation evidence in 64-bit registry"
    $Base = ('HKLM:\SOFTWARE\' + $KeyName)
  } else {
    myLogThis -EventId 12 -Message ($FunctionTop + 'Testing for 32-bit registry key existence')
    $Result = Test-Path ('HKLM:\SOFTWARE\Wow6432Node\' + $KeyName)
    if ($Result) {
      myLogThis -EventId 13 -Message ($FunctionTop + 'Found key in 32-bit registry')
      Write-Host "  Found installation evidence in 32-bit registry"
      $Base = ('HKLM:\SOFTWARE\Wow6432Node\' + $KeyName)
    } else {
      myLogThis -EventId 13 -Message ($FunctionTop + 'Unable to find key') -EntryType "Warning"
      Write-Host "  Could not find installation evidence in registry"
      Return $null
    }
  }

  myLogThis -EventId 14 -Message ($FunctionTop + 'Getting version from registry')
  $RawValue = (Get-ItemProperty -Path $Base -ErrorAction SilentlyContinue).$VersionValue
  myLogThis -EventId 15 -Message ($FunctionTop + 'Raw Value : ' + $RawValue)

  myLogThis -EventId 16 -Message ($FunctionTop + 'Calling out to myVersionCleanup')
  $CleanValue = myVersionCleanup -Version $RawValue

  myLogThis -EventId 19 -Message ($FunctionTop + 'Returning found version number as string : ' + $CleanValue)
  Write-Host -NoNewLine "  Version number from registry: "
  Write-Host -ForegroundColor White $RawValue
  Write-Host -NoNewLine "                 Cleaned up to: "
  Write-Host -ForegroundColor White $CleanValue
  Return $CleanValue
}

Given a key name off of the HKLM\SOFTWARE root, as well as a value name, this function will search both the 64-bit and 32-bit hives to see if that key and value are present. If so, it will read and return the version number, ready for comparison. If not, it will return $null, which can be used to determine that the application is not installed.

myGetCurrentVersionFromMSI

Function myGetCurrentVersionFromMSI {
  param ([Parameter(Mandatory=$True)][String]$Query, [Parameter(Mandatory=$True)][String]$VersionValue)
  $FunctionTop = "myGetCurrentVersionFromMSI`r`n`r`n"
  myLogThis -EventId 11 -Message ($FunctionTop + "Query : " + $Query + "`r`n" + "VersionValue : " + $VersionValue)
  Write-Host -NoNewLine "Looking for installation evidence in MSI database, matching query "
  Write-Host -ForegroundColor White $Query ":" $VersionValue

  # Attempt to determine if we're 64-bit, 32-bit, or not installed at all
  myLogThis -EventId 12 -Message ($FunctionTop + "Testing for 64-bit MSI database existence")
  $Apps = Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | Get-ItemProperty -Name DisplayName,$VersionValue -ErrorAction SilentlyContinue | where { $_.DisplayName -like $Query }
  if ($Apps) {
    myLogThis -EventId 13 -Message ($FunctionTop + 'Found key in 64-bit registry')
    Write-Host "  Found installation evidence in 64-bit MSI database"
    $RawValue = $Apps.$VersionValue
  } else {
    myLogThis -EventId 12 -Message ($FunctionTop + "Testing for 32-bit MSI database existence")
    $Apps = Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | Get-ItemProperty -Name DisplayName,$VersionValue -ErrorAction SilentlyContinue | where { $_.DisplayName -like $Query }
    if ($Apps) {
      myLogThis -EventId 13 -Message ($FunctionTop + 'Found key in 32-bit registry')
      Write-Host "  Found installation evidence in 32-bit MSI database"
      $RawValue = $Apps.$VersionValue
    } else {
      myLogThis -EventId 13 -Message ($FunctionTop + 'Unable to find key') -EntryType "Warning"
      Write-Host "  Could not find installation evidence in MSI database"
      Return $null
    }
  }
  
  myLogThis -EventId 15 -Message ($FunctionTop + "Raw Value : " + $RawValue)

  myLogThis -EventId 16 -Message ($FunctionTop + 'Calling out to myVersionCleanup')
  $CleanValue = myVersionCleanup -Version $RawValue

  myLogThis -EventId 19 -Message ($FunctionTop + 'Returning found version number as string : ' + $CleanValue)
  Write-Host -NoNewLine "  Version number from registry: "
  Write-Host -ForegroundColor White $RawValue
  Write-Host -NoNewLine "                 Cleaned up to: "
  Write-Host -ForegroundColor White $CleanValue
  Return $CleanValue
}

This function searches the MSI registry database (really, just add/remove programs), looking for an entry whose DisplayName value is like the $Query value (meaning that you can use wildcard characters such as *). It then returns the version from the specified $VersionValue value.

myCompareVersionStrings Function

Function myCompareVersionStrings {
  param ([Parameter(Mandatory=$True)][String]$MinVer, [Parameter(Mandatory=$True)][String]$TestVer)
  $FunctionTop = "myCompareVersionStrings`r`n`r`n"
  myLogThis -EventId 30 -Message ($FunctionTop + 'MinVer : ' + $MinVer + "`r`n" + 'TestVer : ' + $TestVer)

  # Split the two version strings into version parts, based on the period as a separator
  $VMin = $MinVer.split('.')
  $VTest = $TestVer.split('.')
  myLogThis -EventId 31 -Message ($FunctionTop + 'Split MinVer into ' + $VMin.Length + ' parts - ' + $MinVer + "`r`n" + 'Split TestVer into ' + $VTest.Length + ' parts - ' + $TestVer)

  # Iterate through each part of MinVer, stacking on padded string values
  $SMin = ""; $STest = ""
  for ($i = 0; $i -lt $VMin.Length; $i++) {
    if ($i -lt $VTest.Length) {
      myLogThis -EventId 32 -Message ($FunctionTop + "Padding version parts from both strings`r`n`r`nVMin[" + $i + "] : " + $VMin[$i] + "`r`nVTest[" + $i + "] : " + $VTest[$i])
      $PartLength=[Math]::Max($VMin[$i].Length,$VTest[$i].Length)
      $SMin+=(' ' * ($PartLength - $VMin[$i].Length)) + $VMin[$i]
      $STest+=(' ' * ($PartLength - $VTest[$i].Length)) + $VTest[$i]
      myLogThis -EventId 35 -Message ($FunctionTop + "Padding Progress`r`n`r`nSMin : |" + $SMin + "|`r`nSTest : |" + $STest + "|")
    } else {
      myLogThis -EventId 33 -Message ($FunctionTop + "Padding version parts that don't exist in Test`r`n`r`nVMin[" + $i + "] : " + $VMin[$i])
      $SMin+=$VMin[$i]
      $STest+=(' ' * $VMin[$i].Length)
      myLogThis -EventId 35 -Message ($FunctionTop + "Padding Progress`r`n`r`nSMin : |" + $SMin + "|`r`nSTest : |" + $STest + "|")
    }
  }

  # Iterate through any additional parts of TestVer if there are more of them than MinVer parts
  for ($i = $VMin.Length; $i -lt $VTest.Length; $i++) {
    myLogThis -EventId 34 -Message ($FunctionTop + "Padding version parts that don't exist in Min`r`n`r`nVTest[" + $i + "] : " + $VTest[$i])
    $SMin+=(' ' * $VTest[$i].Length)
    $STest+=$VTest[$i]
    myLogThis -EventId 35 -Message ($FunctionTop + "Padding Progress`r`n`r`nSMin : |" + $SMin + "|`r`nSTest : |" + $STest + "|")
  }

  Write-Host ("  Comparing Minimum Version : |" + $SMin + "|")
  Write-Host ("       to Installed Version : |" + $STest + "|")
  $Result = [String]::Compare($SMin,$STest,$True)
  myLogThis -EventId 36 -Message ($FunctionTop + "Comparing SMin to STest resulted in code " + $Result)
  if ($Result -lt 1) {
    Write-Host -ForegroundColor Green "  The current or a newer version is installed."
  } else {
    Write-Host -ForegroundColor Yellow "  An older version is installed."
  }
  Return $Result
}

This function takes two strings, $MinVer and $TestVer and compares them using the [String]::Compare method. Before this, it parses the version strings to create two strings of equal length for the comparison. It does this by separating each string into sections based on the periods (.) in them and then right-justifies each section. So, for instance, the versions 8.0.111 and 8.0.1110.14 would be compared as |80 111 | and |80111014|. As the comparison goes, it goes left to right, character by character. Here, the 8's and 0's match for the first two characters. The third character has a space in one string and the numeral 1 in the other. This would just that the space is less than the 1 and so the 8.0.111 string is less than the 8.0.1110.14 string. It would then return a -1 to indicate that the first string is less than the second string (in our judgment, $MinVer is less than $TestVer, so a newer version of the app is installed). A 0 means that the two version strings are the same and a 1 indicates that $TestVer is less than $MinVer, so we need to upgrade the app.

Other Functions

myStampRegistry Function

Function myStampRegistry {
  $FunctionTop = "myStampRegistry`r`n`r`n"
  myLogThis -EventId 70 -Message ($FunctionTop + 'Start of function')

  $Path = ("HKLM:\SOFTWARE\PrivateData\" + $AppName)
  if (Test-Path -Path $Path) { 
    myLogThis -EventId 71 -Message ($FunctionTop + 'Removing old stamp at ' + $Path)
    Remove-Item -Path $Path -Recurse -Force
  }

  myLogThis -EventId 72 -Message ($FunctionTop + 'Stamping registry at ' + $Path)
  New-Item -Path $Path -Force | Out-Null

  $Path+="\" + $AppVersion
  myLogThis -EventId 73 -Message ($FunctionTop + 'Stamping registry at ' + $Path)
  New-Item -Path $Path -Force | Out-Null

  myLogThis -EventId 74 -Message ($FunctionTop + 'Setting (default) value')
  New-ItemProperty -Path $Path -Name '(Default)' -Value 'InTune' | Out-Null
}

This function adds a key to the HKLM\SOFTWARE\PrivateData tree that indicates, essentially, that this script has been run. You can use this as a part of SCCM application installation detection when running the script with the -Update tag. In this case, you just target the script against all systems and let it run - the script determines if it actually needs to do anything or not and then stamps the registry when it is finished. If the SCCM app installation query is just looking for this registry flag, then SCCM is happy and everyone is upgraded quickly. Much faster than trying to do targeted deployment collections for only those machines that return inventory data that indicate that the upgrade is necessary.

Main Script Body

Now that we've got the functions out of the way, we come to the main script body.

Preliminaries

# Preliminaries
$Host.UI.RawUI.WindowTitle=($CompanyName + ' Management of ' + $AppName + ' ' + $AppVersion)
Clear-Host
Write-Host
Write-Host -ForegroundColor Blue (' Computer Management Script for ' + $CompanyName + ' computers')
Write-Host
Write-Host -ForegroundColor Green (' **' + ('*' * $AppName.Length) + '*' + ('*' * $AppVersion.Length) + '**')
Write-Host -ForegroundColor Green -NoNewline ' * '
Write-Host -ForegroundColor Yellow -NoNewline ($AppName + ' ' + $AppVersion)
Write-Host -ForegroundColor Green ' *'
Write-Host -ForegroundColor Green (' **' + ('*' * $AppName.Length) + '*' + ('*' * $AppVersion.Length) + '**')
Write-Host

# Ensure that we have an event log available
myEnsureLogExists

# Record startup
myLogThis -EventId 1 -Message 'Start of Install.ps1'

Put up a banner and title the window. Start logging.

Determine Action

# Check if more than one action is specified on the command line, then set them to none
if (($Install.ToBool() + $Update.ToBool() + $Uninstall.ToBool() + $CheckVersion.ToBool()) -ne 1) { $Install = $False; $Update = $False; $Uninstall = $False; $CheckVersion = $False }

# If no options are specified (or more than one were specified), ask the user what to do
if (!($Install -or $Update -or $Uninstall -or $CheckVersion)) {
  myLogThis -EventId 7 -Message 'No startup switch specified - asking user'
  $title = "Application Maintenance"
  $message = "Please select an action to accomplish for ${AppName}:"
  $option1 = New-Object System.Management.Automation.Host.ChoiceDescription "&Install","Install $AppName"
  $option2 = New-Object System.Management.Automation.Host.ChoiceDescription "&Update","Update $AppName if it is already installed"
  $option3 = New-Object System.Management.Automation.Host.ChoiceDescription "U&ninstall","Uninstall $AppName from the system"
  $option4 = New-Object System.Management.Automation.Host.ChoiceDescription "&Check Version","Check the version of $AppName against $AppVersion"
  $options = [System.Management.Automation.Host.ChoiceDescription[]]($option1,$option2,$option3,$option4)
  $result = $host.ui.PromptForChoice($title,$message,$options,0)
  switch ($result) {
    0 { $Install = $True }
    1 { $Update = $True }
    2 { $Uninstall = $True }
    3 { $CheckVersion = $True }
  }
  Write-Host "`r`n`r`n"
}

# Note in the log what we're doing
if ($Install) { myLogThis -EventId 2 -Message 'Install switch specified'; Write-Host "Working to install $AppName $AppVersion`r`n`r`n" }
if ($Update) { myLogThis -EventId 2 -Message 'Update switch specified'; Write-Host "Working to update $AppName to $AppVersion`r`n`r`n" }
if ($Uninstall) { myLogThis -EventId 2 -Message 'Uninstall switch specified' -EventType "Warning"; Write-Host -ForegroundColor Red "Working to uninstall $AppName`r`n`r`n" }
if ($CheckVersion) { myLogThis -EventId 2 -Message 'CheckVersion switch specified'; Write-Host "Checking the installed version of $AppName against $AppVersion`r`n`r`n" }

Determine what our action is to be. If we have more than one flag on the command line, force them off so that we can then ask the user what to do. Log what our action will be.

Defaults

# Set our return values to 0 to start off with so that we have something definitive to return at the end.
$UninstallResult = 0
$InstallResult = 0

Default return codes.

Get Existing Install Version

# Get existing version number
myLogThis -EventId 10 -Message 'Calling out to myGetCurrentVersionFromMSI to get currently installed version'
$TestVer = myGetCurrentVersionFromMSI -Query "*Java*Update*" -VersionValue "DisplayVersion"

Call out to the appropriate myGetCurrentVersion... function in order to determine if we are installed and, if so, what version is installed. In this example, we're working to update/install the JRE, so that's what we look for.

Handle Case where Not Installed

if ($TestVer -eq $null) {

  # The app doesn't appear to be installed
  # NOTE: If further tests for other locations are necessary, perform them here until we get the version data

  myLogThis -EventId 3 -Message 'No application version currently installed'
  Write-Host -ForegroundColor Green "  The application does not appear to be installed."

  if ($CheckVersion) { $InstallResult = 2 }		#Return 2 like the old CheckVersion.vbs script to indicate not installed

  if ($Update) {
    Write-Host -ForegroundColor Yellow ("We're only performing updates to systems where " + $AppName + " is already installed.")
    myLogThis -EventId 4 -Message 'UPDATE parameter specified; nothing major to do'
    myLogThis -EventId 15 -Message 'Calling out to myStampRegistry'
    myStampRegistry

  } else {

    if ($Install) {
      myLogThis -EventId 13 -Message 'Calling out to myInstallNewApplication'
      $InstallResult = myInstallNewApplication

      if (($InstallResult -eq 0) -or ($InstallResult -eq 3010)) {
        myLogThis -EventId 14 -Message 'Calling out to myConfigureApplication'
        myConfigureApplication
      }

      myLogThis -EventId 15 -Message 'Calling out to myStampRegistry'
      myStampRegistry
    } else {
      Write-Host -ForegroundColor Yellow "$AppName isn't installed; nothing to do."
    }
  }
}

If the application was found to be not installed, handle the results to produce based on whether we're doing -CheckVersion, -Update, or -Install. No need to handle -Uninstall since the app is found to not be installed (this we return an exit code of 0).

Handle Case where App is Installed

# Compare to minimum version to determine what we need to do
if ($TestVer -ne $null) {
  Write-Host ("`r`nComparing " + $TestVer + " to the package version of " + $AppVersion)
  myLogThis -EventId 11 -Message 'Calling out to myCompareVersionStrings to determine if the installed version is newer than the one we wish to install'
  $CompareResult = myCompareVersionStrings -MinVer $AppVersion -TestVer $TestVer

  if ($CompareResult -lt 1) {

    if ($Uninstall) {
      # Uninstall the installed version
      myLogThis -EventId 12 -Message 'Calling out to myUninstallOldApplication'
      $UninstallResult = myUninstallOldApplication
      $InstallResult = $UninstallResult
    } else {
      if ($CheckVersion) {
        $InstallResult = 4	# Return 4 like the old CheckVersion.vbs script to indicate that the current or newer version is installed
      }
      # The current or a newer version is installed, so just place the registry marker and "go home"
      myLogThis -EventId 5 -Message 'Current or newer version is installed; nothing to do'
      if ($Install -or $Update) {
        myLogThis -EventId 15 -Message 'Calling out to myStampRegistry'
        myStampRegistry
      }
    }

  } else {

    if ($Uninstall) {
      # Uninstall the installed version
      myLogThis -EventId 12 -Message 'Calling out to myUninstallOldApplication'
      $UninstallResult = myUninstallOldApplication
      $InstallResult = $UninstallResult
    } else {

      if ($CheckVersion) {
        $InstallResult = 3	# Return 3 like the old CheckVersion.vbs script to indicate that an older version is installed
      }

      # An existing version is getting replaced with the latest version, so uninstall it and then install and configure the new one.
      if ($Install -or $Update) {
        myLogThis -EventId 12 -Message 'Calling out to myUninstallOldApplication'
        $UninstallResult = myUninstallOldApplication

        myLogThis -EventId 13 -Message 'Calling out to myInstallNewApplication'
        $InstallResult = myInstallNewApplication

        if (($InstallResult -eq 0) -or ($InstallResult -eq 3010)) {
          myLogThis -EventId 14 -Message 'Calling out to myConfigureApplication'
          myConfigureApplication
        }

        myLogThis -EventId 15 -Message 'Calling out to myStampRegistry'
        myStampRegistry
      }
    }
  }
}

So, if we found out that the app is installed, compare the version number to our minimum version and determine what to do from there.

If the app is a good enough version, then, if -Uninstall, uninstall it. If -CheckVersion, return the proper code. For all other instances, stamp the registry.

If the app is the wrong version, then, if -Uninstall, uninstall it. If -CheckVersion, return the proper code. For all other instances, uninstall the old app, install the new one, and then configure it.

Final Result

if ($UninstallResult -eq 3010) {
  $FinalResult = 3010
} else {
  $FinalResult = $InstallResult
}

Write-Host "`r`n`r`nFinal result being returned is code $FinalResult"
myLogThis -EventId 9 -Message "Final result being returned is $FinalResult"

Start-Sleep -Seconds 5

$Host.SetShouldExit($FinalResult)
exit $FinalResult

If the uninstall demanded a reboot, return that; otherwise return the result code from the installation of the main app.

Report what code is going to be returned and hold the screen open for five seconds before terminating to allow an interactive user to see the final result.