VDI-GoldenImage.ps1

<#PSScriptInfo
.VERSION 1.5
.GUID d25f8ad6-47fc-4b0c-a239-74ef3be16d2c
.AUTHOR FaraJan
.COMPANYNAME Data Protection Delivery Center s.r.o.
.COPYRIGHT 2023 DPDC CZ. All rights reserved.
.LICENSEURI https://mit-license.org
.TAGS VDI VMware Horizon Golden Image MasterPC Tools DynamicEnvironmentManager AppVolumes Microsoft Teams FSLogix OneDrive OSOT automation maintenance finalize
.PROJECTURI
.RELEASENOTES
[v1.5 - 20231220] Added optional param DisableSpoolerRestart to App Volumes
[v1.4 - 20231130] Revisions and Bug Fixes
[v1.3 - 20231107] Added task to install/update Microsoft FSLogix & OneDrive with downloading of latest version from web
[v1.2 - 20231027] Added task to install/update Horizon Agent, VMware Dynamic Environment Manager and App Volumes
[v1.1 - 20230926] Added task to install/update Microsoft Teams for VDI with downloading of latest version from web
[v1.0 - 20230906] First version with management of OS/Office updates, OSOT finalize action and VM Tools install/update task
#>


<#
.SYNOPSIS
   Maintenance script for easier programmable management of VDI Golden Image (Master PC) running VMware Horizon
.DESCRIPTION
   Script with basic sets of maintenance task for VDI Golden Image running on VMware Horizon.
   Typical tasks include updating OS, Office and other software as well as finalizing and cleaning up the image before final snapshot and deployment to virtual desktops.
   Other tasks are installing/updating agents for running VDI (Horizon, Dynamic Environment manager, App Volumes, FSlogix, MS Teams for VDI and OneDrive).
.NOTES
   Version: 1.5
   Author: Fara Jan
   Creation Date: 2023-09-06
   Last Update: 2023-12-20
.PARAMETER Action
   Specifies the action/task of the script. If not specified a menu with a list of actions is displayed.
.INPUTS
   System.String or Empty
.OUTPUTS
   System.String Console/Log
.EXAMPLE
  # Run script and display menu list of all script actions
     powershell C:\ProgramData\VDI\VDI-GoldenImage.ps1
.EXAMPLE
  # Enable and runs OS/Office/SW updates in Golden Image
     PS> VDI-GoldenImage.ps1 -Action Update
.EXAMPLE
  # Disable OS/Office/SW updates, runs system clean-up tasks and prepare Golden Image for Horizon
     PS> VDI-GoldenImage.ps1 -Action Finalize
.EXAMPLE
  # Install/update VM Tools in Golden Image - comparing current version and version in InstallSrcDir
     PS> VDI-GoldenImage.ps1 -Action vmtools
.EXAMPLE
  # Install/update Microsoft Teams for VDI - comparing current version and version in InstallSrcDir or latest online version
     PS> VDI-GoldenImage.ps1 -Action msteams
.NOTES
   Recommended location for script: C:\ProgramData\VDI
#>



#----------------[ Script param ]----------------
param (
    [Parameter(Mandatory=$false)]
    [ValidateSet("update", "finalize", "finalizefast", "defrag", "vmtools", "horizon", "dem", "appvolumes", "fslogix", "msteams", "onedrive", "exit")]
    [string] $Action
)

#----------------[ Settings ]----------------
$VAR = @{
 # Script Name
  ScriptName = "VDI Golden Image Maintenance"
 # Script Path ($PSScriptRoot for CurrentPath)
  ScriptPath = $PSScriptRoot

 # --- VDI Image - Updates Settings ---
 # Windows/Office (365/2019) updates managed/controlled by this script - disabled by default & run only during Golden Image maintenance/update
  ManageWindowsUpdates = $true
  ManageOfficeUpdates = $true
 # Other updates Settings (Web browser and other SW updates)
  ManageMsEdgeUpdate = $true
  ManageGoogleUpdates = $true
  ManageAdobeUpdates = $true
  ManageOneDriveUpdates = $true

 # --- Finalize Settings (VMware OSOT) ---
  # OSOT info web: https://techzone.vmware.com/resource/windows-os-optimization-tool-vmware-horizon-guide
  # Path to the OSOT executable file (automatic getting of latest OSOT version)
   OsotPath = "C:\Program Files\VMware OSOT"
  # Finalize Argument Settings
   OsotFinalizeArg = "-v -f 0 1 2 3 4 5 9 10 11" #All excepts: '(6) Clears Default user profile', '(7) Zero empty disk space', '(8) Creates local group policies'
   #OsotFinalizeArg = "-v -f 3 4" #demo
  # Finalize Argument Settings for Fast cleanup
   OsotFinalizeArgFast = "-v -f 3 4 9 10 11" # Disk Cleanup, EventLog Cleanup, Clears KMS Settings, Flush DNS cache, Releases IP Address
  # Shutdown VM after Finalize
   OsotShutdownAfterFinalize = $true
   OsotShutdownAfterFinalizeFast = $true
  # SCCM: Clear Configuration during finalize (eliminate duplicates)
   SccmClearConfig = $false

 # --- VM Tools Settings ---
  # Enable Carbon Black Helper
   VmToolsCarbonBlack = $false
  # Enable NSX Guest Instrospection Drivers
   VmToolsNsxIntrospection = $false

 # --- Horizon Agent Install options ---
  # Install Options DOC: https://docs.vmware.com/en/VMware-Horizon/2309/virtual-desktops/GUID-3096DA8B-034B-435B-877E-5D2B18672A95.html
  # Horizon Agent Features, if HorizonAgentAddLocal = ALL then HorizonAgentRemove is also used
   HorizonAgentAddLocal = "ALL" #"Core,PCoIP,USB,NGVC,RTAV,ClientDriveRedirection,GEOREDIR,V4V,VmwVaudio,VmwVidd,TSMMR,BlastUDP,PerfTracker,HelpDesk,PrintRedir,PSG"
   HorizonAgentRemove = "SerialPortRedirection,ScannerRedirection,SmartCard,SdoSensor"
  # Delete Horizon Peformance Tracker Icon from all users desktop
   DelPerfTrackerDesktopIcon = $true
  # Increasing a default timeout limit for Post-Sync/ClonePrep Customization Scripts (default 20000 ms = 20 s)
   HorizonAgentExecScriptTimeout = 90000

 # --- VMware DEM Config share Path (Empty if no VMware DEM) ---
  DemConfigPath = "\\domain.local\DFS\DEMConfig\general"

 # --- VMware App Volumes Agent Configuration ---
  # AppVol Manager(s) (DNS hostname/IP) - multiple array items for HA configuration; Empty Array if No App Volumes
   AppVolManager = @("vdi-avm01.domail.local", "vdi-avm02.domail.local")
   # Deactivate Restarting the Spooler Service When Using VMware Integrated Printing: https://docs.vmware.com/en/VMware-App-Volumes/2309/app-volumes-admin-guide/GUID-B7CABC0E-9313-4BA8-A718-569E5665D4E9.html
   AppVolDisableSpoolerRestart = $true
   # The maximum wait for a response from the AppVol Manager, in seconds. If set to 0, the wait for response is forever (default 300 sec = 5 min)
   AppVolMaxDelayTimeOutS = 30

 # --- Microsoft Teams Settings ---
  MsTeamsDisableAutoStart = $false

 # --- VDI Agents Install Source Directory (VM Tools, Horizon Agent, ...)---
  InstallSrcDir = "Install"  # relative path to $_.ScriptPath

 # --- LOG settings ---
  LogDir = "Logs" # relative path to $_.ScriptPath
  LogFileName = "VDI-GI-Maintenance-{0:yyyyMMdd_HHmmss}.txt"
  LogArchiveFiles = 14
}

# --- Script Main Menu ---
$MENU = @()
$menuStr1 = $menuStr2 = ""
$menuStrDownload = " (with downloading the installation from the Internet)"
if($VAR.ManageWindowsUpdates -or $VAR.ManageOfficeUpdates){
    $menuStr1 = "Update: Enable & runs updates for "
    $menuStr1 += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office" } elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" }
    $menuStr2 = "Disable "
    $menuStr2 += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office" } elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" }
    $menuStr2 += " updates & "
}
if($menuStr1 -notlike ""){ $MENU += $menuStr1 }
$MENU += "Finalize: $($menuStr2)Runs system clean-up tasks and prepare Golden Image for Horizon"
$MENU += "FinalizeFast: Runs clean-up tasks without NGEN optimization, DISM cleanup and without CompactOS (faster)"
$MENU += "Defrag: Disk defragmentation of the Golden Image"
$MENU += "VmTools: Install/Update VMware VM Tools"
$MENU += "Horizon: Install/Update VMware Horizon Agent"
if($VAR.DemConfigPath -notlike ""){ $MENU += "DEM: Install/Update VMware Dynamic Environment Agent" }
if($VAR.AppVolManager.Count){ $MENU += "AppVolumes: Install/Update VMware AppVolumes Agent" }
$MENU += "FSLogix: Install/Update Microsoft FSLogix Agent" + $menuStrDownload
$MENU += "MsTeams: Install/Update Microsoft Teams for VDI" + $menuStrDownload
$MENU += "OneDrive: Install/Update Microsoft OneDrive" + $menuStrDownload
$MENU += "Exit"

# --- Settings for SW Install/Update (SW Name, SW Source Install File Name, Installation Log File Name, Ask for install of newer version) ---
$SwSet = @{
    vmtools =    @{Name = "VMware Tools";                       SrcFile = "VMware-tools-*-x86_64.exe";                             LogFile = "Log_VmTools_install_{0:yyyyMMdd_HHmmss}.txt";      AskIfNewer = $true}
    horizon =    @{Name = "VMware Horizon Agent";               SrcFile = "VMware-Horizon-Agent-x86_64-*.exe";                     LogFile = "Log_HorizonAgent_install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true}
    dem =        @{Name = "VMware Dynamic Environment Manager"; SrcFile = "VMware Dynamic Environment Manager Enterprise*x64.msi"; LogFile = "Log_DEMAgent_install_{0:yyyyMMdd_HHmmss}.txt";     AskIfNewer = $true}
    appvolumes = @{Name = "App Volumes Agent";                  SrcFile = "App Volumes Agent*.msi";                                LogFile = "Log_AppVolAgent_install_{0:yyyyMMdd_HHmmss}.txt";  AskIfNewer = $true}
    fslogix =    @{Name = "Microsoft FSLogix Apps";             SrcFile = "FSLogix_Apps_*.zip";                                    LogFile = "Log_MsFSLogix_install_{0:yyyyMMdd_HHmmss}.txt";    AskIfNewer = $true}
    msteams =    @{Name = "Microsoft Teams for VDI";            SrcFile = "Teams_windows_x64.msi";                                 LogFile = "Log_MsTeams_{1}install_{0:yyyyMMdd_HHmmss}.txt";   AskIfNewer = $true}
    onedrive =   @{Name = "Microsoft OneDrive";                 SrcFile = "OneDriveSetup.exe";                                     LogFile = "";                                                 AskIfNewer = $true}
}

# --- Windows/Office Update Registry Settings Parameters ---
$regArr = @()
$regDelTag = "***DELETE***"
# Windows Update
if($VAR.ManageWindowsUpdates){
    $regPath = "HKLM:\\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
    $regArr += [pscustomobject] @{Path = $regPath; Name = "BranchReadinessLevel"; PropertyType = "Dword"; ValueOn = 16; ValueOff = "32" }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "DeferQualityUpdatesPeriodInDays"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 30 }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "DisableWindowsUpdateAccess"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "DoNotConnectToWindowsUpdateInternetLocations"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "PauseFeatureUpdatesStartTime"; PropertyType = "String"; ValueOn = $("{0:yyyy-MM-dd}" -f (Get-Date)); ValueOff = "2021-10-15" }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "PauseQualityUpdatesStartTime"; PropertyType = "String"; ValueOn = "2015-12-31"; ValueOff = "2021-10-31" }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "SetDisableUXWUAccess"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 }
    $regPath += "\AU"
    $regArr += [pscustomobject] @{Path = $regPath; Name = "AllowMUUpdateService"; PropertyType = "Dword"; ValueOn = 1; ValueOff = $regDelTag }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "AUOptions"; PropertyType = "Dword"; ValueOn = 4; ValueOff = $regDelTag }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "NoAutoUpdate"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "ScheduledInstallDay"; PropertyType = "Dword"; ValueOn = 0; ValueOff = $regDelTag }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "ScheduledInstallEveryWeek"; PropertyType = "Dword"; ValueOn = 1; ValueOff = $regDelTag }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "ScheduledInstallTime"; PropertyType = "Dword"; ValueOn = 3; ValueOff = $regDelTag }
}
# Office Update
if($VAR.ManageOfficeUpdates){
    $regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Office\16.0\Common\OfficeUpdate"
    $regArr += [pscustomobject] @{Path = $regPath; Name = "EnableAutomaticUpdates"; PropertyType = "Dword"; ValueOn = 1; ValueOff = 0 }
    $regArr += [pscustomobject] @{Path = $regPath; Name = "HideEnableDisableUpdates"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 }
}

# --- Windows Service Settings ---
$svcArr = @()
if($VAR.ManageWindowsUpdates){
    $svcArr += [pscustomobject] @{Name = "wuauserv"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Windows Update
    $svcArr += [pscustomobject] @{Name = "UsoSvc"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Update Orchestrator Service
    $svcArr += [pscustomobject] @{Name = "StorSvc"; StartupTypeOn = "Automatic"; SvcActionOn = "Start"; StartupTypeOff = "Manual"; SvcActionOff = "Stop" } # Storage Service
    if($VAR.AppVolManager.Count){
        $svcArr += [pscustomobject] @{Name = "svservice"; StartupTypeOn = "Disabled"; SvcActionOn = "Stop"; StartupTypeOff = "Automatic"; SvcActionOff = "Start" } # AppVolumes
        $svcArr += [pscustomobject] @{Name = "svdriver"; StartupTypeOn = "Disabled"; SvcActionOn = "Stop"; StartupTypeOff = "Automatic"; SvcActionOff = "Start" } # AppVolumes
    }
}
if($VAR.ManageGoogleUpdates){
    $svcArr += [pscustomobject] @{Name = "gupdate"; StartupTypeOn = "Manual"; SvcActionOn = "None"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Google update service
    $svcArr += [pscustomobject] @{Name = "gupdatem"; StartupTypeOn = "Manual"; SvcActionOn = "None"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Google update service
}
if($VAR.ManageAdobeUpdates){
    $svcArr += [pscustomobject] @{Name = "AdobeARMservice"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Adobe Reader update service
}

# --- Array of Scheduled tasks to disable during finalize ---
$schtaskArr = @()
if($VAR.ManageMsEdgeUpdate){
    $schtaskArr += "MicrosoftEdgeUpdateTaskMachineCore"
    $schtaskArr += "MicrosoftEdgeUpdateTaskMachineUA"
}
if($VAR.ManageGoogleUpdates){
    $schtaskArr += "GoogleUpdateTaskMachineCore*"
    $schtaskArr += "GoogleUpdateTaskMachineUA*"
}
if($VAR.ManageAdobeUpdates){ $schtaskArr += "Adobe Acrobat Update Task" }
if($VAR.ManageOneDriveUpdates){ $schtaskArr += "OneDrive Per-Machine Standalone Update Task" }

# --------
Clear-Host
# --------
#----------------[ Declarations ]----------------
$InstallSrcDir = $VAR.ScriptPath + "\" + $VAR.InstallSrcDir
$LogDir = $VAR.ScriptPath + "\" + $VAR.LogDir
$LogFile = $LogDir + "\" + $VAR.LogFileName -f (Get-Date)

#----------------[ Initilization ]----------------
# Install source & Log folder
if(!(Test-Path $InstallSrcDir -PathType Container)){ New-Item -Path $InstallSrcDir -ItemType Directory | out-null }
if(!(Test-Path $LogDir -PathType Container)){ New-Item -Path $LogDir -ItemType Directory | out-null }

#----------------[ Logging ]----------------
# Transcript Log - Start
if($Host.Name -match "ConsoleHost"){ Start-Transcript -Path $LogFile -append }

#----------------[ Functions ]------------------
# Write Empty Lines
Function EmptyLines(){
    Param ( [int] $lines = 1)
    for($i=1; $i -le $lines; $i++){ Write-Host "`r`n" }
}

# Script Messages FCE
# Write the message to the console output and also add it to the body of the email (global VAR)
# Param $StripHtml to clear HTML formating to console output
# Param $NoAddToEmailBody to skip concat message with global variable $VAR.EmailBody
Function MsgFce(){
    param (
        [Parameter(Position=0,Mandatory=$True)] [string] $Msg,
        [ValidateSet("host","warn","verbose")] [string] $Output="host",
        [switch] $StripHtml,
        [switch] $NoAddToEmailBody
    )

    $Msg0 = if($StripHtml){ $Msg -replace "<[^>]*?>|<[^>]*>" } else { $Msg }
    if($Output -ilike "host"){
        Write-Host $Msg0
    } elseif($Output -ilike "warn"){
        Write-Warning -Message $Msg0 -Verbose
    } elseif($Output -ilike "verbose"){
        Write-Verbose -Message $Msg0 -Verbose
    }

    if(($Script:VAR.EmailBody -is [array]) -and !$NoAddToEmailBody){
        $Script:VAR.EmailBody += $Msg
    }
}

# Simple choice menu FCE
# Writes an output of array items to select
# Example of use: $MenuItems = @("Yes", "No"); $Title = "Contine?"
function MenuSimple(){
    Param(
        [Parameter(Position=0, Mandatory=$True)]
        [string[]] $MenuItems,
        [string] $Title,
        [boolean] $Cls
    )

    $header = $null
    if(![string]::IsNullOrWhiteSpace($Title)) {
        $len = [math]::Max(($MenuItems | Measure-Object -Maximum -Property Length).Maximum, $Title.Length)
        $header = "{0}{1}{2}" -f $Title, [Environment]::NewLine, ("-" * $len)
    }
    # menu items and space align if more than 9 items
    $len = if($MenuItems.Count -gt 9){ 2 } else { 1 }
    $items = ($MenuItems | ForEach-Object{ "[{0}]{1}{2}" -f ++$i, $(if($i -lt 10){" " * $len} else{" "}), $_ }) -join [Environment]::NewLine

    # display the menu and return the chosen option
    while($true){
        if($Cls){ Clear-Host } else { Write-Host }
        if($header){ Write-Host $header -ForegroundColor Yellow }
        Write-Host $items
        Write-Host

        $index = (Read-Host -Prompt 'Please make your choice')
        $index = $index -as [int]

        if((1..$MenuItems.Count) -contains $index){
            return $MenuItems[$index-1]
        } else {
            Write-Warning "Invalid choice.. Please try again."
            Start-Sleep -Seconds 2
        }
    }
}

# Registry settings FCE for enable/disable updates (enable = ValueOn / disable = ValueOff)
# Example of param: [pscustomobject] @{Path = ""; Name = ""; PropertyType = "Dword/String/..."; ValueOn = ; ValueOff = }
Function SetRegistry(){
    param (
        [Parameter(Mandatory=$true)]
            [array] $RegArr,
        [Parameter(Mandatory=$true)]
            [ValidateSet("On","Off")]
            [string] $UpdateAction

    )
    $regValueKey = "Value$($UpdateAction)"
    foreach($reg in $RegArr){
        if(!(Test-Path $reg.Path)){ New-Item -Path $reg.Path -Force | Out-Null }
        # $regDelTag = variable with specific tag value to remove registry value
        if($reg.$regValueKey -notlike $Script:regDelTag){
            MsgFce "Set registry '$($reg.Name)' = '$($reg.$regValueKey)' ($($reg.PropertyType)) at '$($reg.Path)'"
            New-ItemProperty -Path $reg.Path -Name $reg.Name -PropertyType $reg.PropertyType -Value $reg.$regValueKey -Force | Out-Null
        } else{
            MsgFce "Removed registry '$($reg.Name)' ($($reg.PropertyType)) at '$($reg.Path)'"
            Remove-ItemProperty -Path $reg.Path -Name $reg.Name -ErrorAction SilentlyContinue | Out-Null
        }
    }
}

# Windows service settings FCE for enable/disable updates (enable = StartupTypeOn | SvcActionOn / disable = StartupTypeOff | SvcActionOff)
# Example of param: [pscustomobject] @{Name = ""; StartupTypeOn = "Manual/Automatic/Disabled"; SvcActionOn = "Start/Stop"; StartupTypeOff = "Manual/Automatic/Disabled"; SvcActionOff = "Start/Stop" }
Function SetService(){
    param (
        [Parameter(Mandatory=$true)]
            [array] $SvcArr,
        [Parameter(Mandatory=$true)]
            [ValidateSet("On","Off")]
            [string] $UpdateAction
    )
    $svcStartupTypeKey = "StartupType$($UpdateAction)"
    $svcActionKey = "SvcAction$($UpdateAction)"
    foreach($svc in $SvcArr){
        $service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue
        if($service.Length){
            MsgFce "Set service '$($svc.Name)' (Startup Type '$($svc.$svcStartupTypeKey)')"
            $service | Set-Service -StartupType $svc.$svcStartupTypeKey
            MsgFce "Service '$($svc.Name)' Action '$($svc.$svcActionKey)'"
            if($svc.$SvcActionKey -ilike "start"){
                $service | Start-Service
            } elseif($svc.$SvcActionKey -ilike "stop"){
                $service | Stop-Service
            } elseif($svc.$SvcActionKey -ilike "none"){
                # skip
            } else{
                MsgFce "...unknown service action..." -Output warn
            }
        } else{
            MsgFce "Skipping service '$($svc.Name)' (doesn't exist)" -Output warn
        }
    }
}

# Windows Task Scheduler - Enabling/Disabling schtask ($Schtask = Scheduled Task Name mask)
Function SetSchtaskState(){
    param (
        [Parameter(Mandatory=$true)]
            [string] $Schtask,
        [Parameter(Mandatory=$true)]
            [ValidateSet("Enable","Disable")]
            [string] $State
    )

    $task = Get-ScheduledTask -TaskName $Schtask
    if(($task | Measure-Object).Count -eq 1){
        $msg = "Scheduled task '$($task.TaskName)' - state set to "
        if($State -like "Enable"){
            $msg += "'Enabled'"
            $task | Enable-ScheduledTask | Out-Null
        }
        if($State -like "Disable"){
            $msg += "'Disabled'"
            $task | Disable-ScheduledTask | Out-Null
        }
        MsgFce $msg
    } else{
        MsgFce "Fce 'Get-ScheduledTask' for task '$($Schtask)' returned more than one item => cannot set state" -Output warn
   }
}

# Getting latest version of VMware OSOT and call the OSOT exe with specified arguments
Function OsotCmd{
    param (
        [Parameter(Mandatory=$true)]
            [string] $OsotArg,
        [Parameter(Mandatory=$false)]
            [boolean] $OsotShutdown = $false
    )

    # Getting of latest version VMware OSOT Executable
    $osotFileFilter = "VMwareHorizonOSOptimizationTool-x86_64-*"
    $osotExe = Get-ChildItem -Path $Script:VAR.OsotPath -Filter $osotFileFilter -ErrorAction SilentlyContinue | Select-Object -Property Name,FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object Version | Select-Object -Last 1
    if($osotExe){
        # add Shutdown param
        if($OsotShutdown){ $OsotArg += " -shutdown" }
        # split arguments to array
        $params = $OsotArg.Split(" ")
        # call Osot
        & $($OsotExe.FullName) $params
    } else{
        MsgFce "No VMware OSOT executable ((filter mask: $($OsotFileFilter))) found in OSOT path ($($VAR.OsotPath)) => cannot proceed finalize" -Output warn
    }
}

# Getting MSI file properties
Function Get-MsiInformation(){
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [System.IO.FileInfo[]] $MSI
    )

    if($MSI.Count -ne 1){
        Write-Warning "ERROR: Only ONE MSI file for fce Get-MsiInformation!"
        break
    }

    try{
        $file = Get-ChildItem $MSI -ErrorAction Stop
    } catch{
        Write-Warning "Unable to get file $($MSI) $($_.Exception.Message)"
        return
    }

    $dataMSI = [ordered] @{
        FileName         = $file.Name
        FileFullName     = $file.FullName
        "FileLength(MB)" = $file.Length / 1MB
    }

    # Read property from MSI database
    $winInstaller = New-Object -ComObject WindowsInstaller.Installer

    # open MSI DB read only
    $msiDbOpenReadOnly = 0
    try{
        $msiDb = $winInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $winInstaller, @($file.FullName, $msiDbOpenReadOnly))
    } catch{
        Write-Debug $_.Exception.Message
    }

    if($msiDb){
        $properties = @("ProductName", "ProductVersion", "Manufacturer", "ProductCode", "UpgradeCode")
        foreach($property in $properties){
            $query = "SELECT Value FROM Property WHERE Property = '$($property)'"
            $view = $msiDb.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $msiDb, ($query))
            $view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, $null)
            $record = $view.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $view, $null)
            try{
                $value = $record.GetType().InvokeMember("StringData", "GetProperty", $null, $record, 1)
            } catch {
                Write-Debug "Unable to get '$property' $($_.Exception.Message)"
                $value = ""
            }
            $dataMSI.$property = $value
        }

        # Other MSI Details
        # https://docs.microsoft.com/en-us/windows/win32/msi/summary-information-stream-property-set
        $msiInfo = $msiDb.GetType().InvokeMember("SummaryInformation", "GetProperty",$null , $msiDb, $null)
        $propertyIndex = @{"Title"=2; "Subject"=3; "Author"=4; "Comment"=6; "CreationDate"=12; "RevisionNumber"=9;"ApplicationName"=18}
        foreach($key in $propertyIndex.Keys){
            $dataMSI.$key = $msiInfo.Property($propertyIndex.$key)
        }
    }
    # Close MSI Run garbage collection and release ComObject
    $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($winInstaller)
    [System.GC]::Collect()

    return [pscustomobject] $dataMSI
}

# Function of clearing SCCM Configuration
Function SccmClearConfig{
    # Reset SCCM client on computer before taking snapshot for cloning
    $svc = "ccmexec"
    $service = Get-Service -Name $svc -ErrorAction SilentlyContinue
    if($service.Length){
        $service | Stop-Service
    } else{
        MsgFce "Service '$($svc)' (doesn't exist)" -Output warn
    }

    # Remove SCCM agent ini config file
    $sccmCfg = "$($Env:WinDir)\SMSCFG.ini"
    if(Test-Path $sccmCfg -PathType Leaf){
        Remove-Item -Path $sccmCfg -Force
    } else{
        MsgFce "SCCM config file ('$($sccmCfg)') not found => skipped" -Output warn
    }

    # Delete certificate by subject/serialnumber/issuer/whatever
    Get-ChildItem Cert:\LocalMachine\SMS -ErrorAction SilentlyContinue | Where-Object { $_.Subject -match "SMS" } | Remove-Item

    # Clear SCCM cached files
    Remove-Item "$($Env:WinDir)\ccmcache\*" -recurse -ErrorAction SilentlyContinue
}


#----------------[ Main Execution ]---------------
$msg = "$($VAR.ScriptName) --- The beginning of the script --- [{0:yyyy-MM-dd HH:mm:ss}]" -f (Get-Date)
MsgFce $msg
EmptyLines

# UAC required
if(!([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
    MsgFce "ERROR => Administrator rights are required to run this script!" -Output warn
    break
}

# Get Action from Menu
if($Action -like ""){
    $selAction = MenuSimple -MenuItems $MENU -Title "Please select a script action"
    EmptyLines
    $Action = $($selAction.Split(":")[0])
}
$Action = $Action.ToLower()

# Param info
MsgFce "Script 'Action': $($Action)"
EmptyLines

# if switch by Action param
# --- Enables & runs Windows/Office updates ---
if($Action -ilike "update"){
    # Set Update Registry
    if($regArr.Count){
        $str += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office"} elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" }
        MsgFce "INFO: --- Enable $($str) Update registry & run ---"
        SetRegistry -RegArr $regArr -UpdateAction On
    }
    # Set Update Services
    if($svcArr.Count){
        MsgFce "INFO: --- Enable updating services ---"
        SetService -SvcArr $svcArr -UpdateAction On
    }

    # Run GPupdate
    if($regArr.Count -or $svcArr.Count){
        & gpupdate
    }

    # Start Windows OS Update GUI
    if($VAR.ManageWindowsUpdates){
        MsgFce "INFO: Starting Windows Update process..."
        Start-Process "ms-settings:windowsupdate"
    }

    # Start Office Update GUI
    if($VAR.ManageOfficeUpdates){
        $msoClientExe = "C:\Program Files\Common Files\microsoft shared\ClickToRun\OfficeC2RClient.exe"
        $msoClientArg = "/update user"
        if(Test-Path $msoClientExe -PathType Leaf){
            MsgFce "INFO: Starting Microsoft Office update process..."
            Start-Process $msoClientExe $msoClientArg
        } else{
            MsgFce "Microsoft Office update client not found ('$($msoClientExe)') => skipped" -Output warn
        }
    }
}

# --- OSOT Finalize ---
elseif($Action -ilike "finalize"){
    # Disable Update Services & Registry
    if($svcArr.Count){
        MsgFce "INFO: --- Disable updating services ---"
        SetService -SvcArr $svcArr -UpdateAction Off
    }

    # Set Update Registry
    if($regArr.Count){
        $str += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office"} elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" }
        MsgFce "INFO: --- Disable $($str) update registry ---"
        SetRegistry -RegArr $regArr -UpdateAction Off
    }

    # Run GPupdate
    if($regArr.Count -or $svcArr.Count){
        & gpupdate
    }

    # Other maintenance - clear old windows updates files, log files etc.
    $dirArr = @()
    $dirArr += [pscustomobject] @{Path = "C:\Windows\SoftwareDistribution\Download\"; FileMask = "*.*"; OlderThanXDays = 0 }
    $dirArr += [pscustomobject] @{Path = "C:\ProgramData\VMware\VDM\Logs\"; FileMask = "*.*"; OlderThanXDays = 0 }
    $dirArr += [pscustomobject] @{Path = "C:\Program Files (x86)\CloudVolumes\Agent\Logs"; FileMask = "*.log"; OlderThanXDays = 0 }

    if($dirArr.Count){
        MsgFce "INFO: --- Cleaning up some directories ---"
        foreach($dir in $dirArr){
            # select items to delete
            $items = Get-ChildItem -Path $dir.Path -Recurse -force -ErrorAction SilentlyContinue | Where-Object {-not $_.PsIsContainer -and ($_.Name -ilike $dir.FileMask)}
            if($dir.OlderThanXDays){ $items = $items | Where-Object {($_.LastwriteTime -lt (Get-Date).AddDays(-$dir.OlderThanXDays))} }
            if($items.Count){ $items | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue }
            # info msg
            $str = if($dir.OlderThanXDays){ ", older than $($dir.OlderThanXDays) day(s)"} else{ "" }
            $str += " => Total number of deleted files: " + $items.Count
            MsgFce "INFO: Directory: '$($dir.Path)', File Mask: '$($dir.FileMask)'$($str)"
        }
    }

    # Set/Disable Scheduled tasks
    if($schtaskArr.Count){
        MsgFce "INFO: --- Disabling some scheduled tasks ---"
        foreach($schtask in $schtaskArr){
            SetSchtaskState -Schtask $schtask -State Disable
        }
    }

    # SCCM Configuration Cleanup
    if($VAR.SccmClearConfig){
        MsgFce "INFO: --- Clearing SCCM Configuration ---"
        SccmClearConfig
    }

    # OSOT Finalize Cmd
    MsgFce "INFO: --- VMware OSOT Finalize ---"
    OsotCmd -OsotArg $VAR.OsotFinalizeArg -OsotShutdown $VAR.OsotShutdownAfterFinalize
}

# --- OSOT Finalize fast ---
elseif($Action -ilike "finalizefast"){
    # SCCM Configuration Cleanup
    if($VAR.SccmClearConfig){
        MsgFce "INFO: --- Clearing SCCM Configuration ---"
        SccmClearConfig
    }

    # OSOT Finalize Cmd
    MsgFce "INFO: --- VMware OSOT Finalize FAST ---"
    OsotCmd -OsotArg $VAR.OsotFinalizeArgFast -OsotShutdown $VAR.OsotShutdownAfterFinalizeFast
}

# --- Disk defragmentation ---
elseif($Action -ilike "defrag"){
    MsgFce "INFO: --- Disk defragmentation ---"
    $defragsvc = @([pscustomobject] @{Name = "defragsvc"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" })
    #$defragsvc | Out-GridView
    SetService -SvcArr $defragsvc -UpdateAction On
    MsgFce "... running defragmentation ..."
    $params = "C: /U /V"
    Start-Process defrag -Wait -ArgumentList $params.Split(" ")
    SetService -SvcArr $defragsvc -UpdateAction Off
}

# --- VMware Tools ---
elseif($Action -ilike "vmtools"){
    MsgFce "INFO: --- Install/Update VMware VM Tools ---"
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1
    if($swSrc){
        $sw = Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}}
        if($sw.VersionBuild -lt $swSrc.VersionBuild){
            MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))"
            $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "yes" }
            if($continue -like "yes"){
                # Installation
                MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..."
                $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date)
                # remove some VM tools components (not necessary for VDI)
                $remove = "VmwTimeProvider,ServiceDiscovery,VSS"
                if(!$VAR.VmToolsCarbonBlack){ $remove += ",CBHelper" }
                if(!$VAR.VmToolsNsxIntrospection){ $remove += ",NetworkIntrospection,FileIntrospection" }
                $params = '/S /v "/qb /l* ""{0}"" REBOOT=R ADDLOCAL=ALL REMOVE={1}"' -f $log, $remove
                Start-Process -FilePath $swSrc.FullName -Wait -ArgumentList $params.Split(" ")
            } else{
                MsgFce "INFO: The installation has been cancelled"
            }
        } else{
            MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))"
        }
    } else{
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    }
}

# --- VMware Horizon Agent ---
elseif($Action -ilike "horizon"){
    MsgFce "INFO: --- Install/Update VMware Horizon Agent ---"
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={[int] $_.VersionInfo.ProductVersion}}, @{N='VersionBuild';E={[int] $($_.BaseName.Split("-")[6])}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1
    if($swSrc){
        $sw = Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='Version';E={[int] $($_.DisplayVersion.Substring($_.DisplayVersion.length-5,4))}}, @{N='VersionBuild';E={[int] $($($_.DisplayVersion.Split(".")[3]).Split(" ")[0])}}
        if($sw.Version -lt $swSrc.Version){
            MsgFce "INFO: A newer version of $($SwSet.$Action.Name) was found in the installation source path (Install source | current version ::: $($swSrc.Version) | $($sw.Version))"
            $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "yes" }
            if($continue -like "yes"){
                # Installation
                MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..."
                $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date)
                $options = if(($VAR.HorizonAgentAddLocal -like "ALL") -and ($VAR.HorizonAgentRemove -notlike "")){ " REMOVE=$($VAR.HorizonAgentRemove)" } else { "" }
                $params = '/S /v "/qb /l* ""{0}"" VDM_VC_MANAGED_AGENT=1 SUPPRESS_RUNONCE_CHECK=1 ADDLOCAL={1}{2} REBOOT=ReallySuppress"' -f $log, $VAR.HorizonAgentAddLocal, $options
                Start-Process -FilePath $swSrc.FullName -Wait -ArgumentList $params.Split(" ")
            } else{
                MsgFce "INFO: The installation has been cancelled"
            }
        } else{
            MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.Version) | $($sw.Version))"
        }
        # Remove PerfTracker icon from public desktop (if exist)
        if($VAR.DelPerfTrackerDesktopIcon){
            $ico = "$($env:PUBLIC)\Desktop\VMware Horizon Performance Tracker.lnk"
            if(Test-Path $ico -PathType Leaf){
                MsgFce "INFO: Public desktop icon of Horizon Performance Tracker removed"
                Remove-Item -Path $ico -Force | out-null
            }
        }
        # Configure Horizon Agent ExecScriptTimeout registry
        if([int] $VAR.HorizonAgentExecScriptTimeout -gt 2000){
            MsgFce "INFO: Setting a new Timeout Limit for ClonePrep Customization Scripts (default limit: 2000 ms => new limit: $($VAR.HorizonAgentExecScriptTimeout) ms)"
            New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\vmware-viewcomposer-ga" -Name "ExecScriptTimeout" -Value $VAR.HorizonAgentExecScriptTimeout -PropertyType Dword -Force | Out-Null
        }
    } else{
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    }
}

# --- VMware DEM Agent ---
elseif($Action -ilike "dem"){
    MsgFce "INFO: --- Install/Update VMware DEM Agent ---"
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={[int] $($_.BaseName.Split(" ")[5])}}, @{N='VersionBuild';E={[System.Version] $($_.BaseName.Split(" ")[6])}}, LastWriteTime | Sort-Object VersionBuild,LastWriteTime | Select-Object -Last 1
    if($swSrc){
        $msiData = Get-MsiInformation $swSrc.FullName
        $sw = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $msiData.ProductName } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString
        if($sw.VersionBuild -lt $msiData.ProductVersion){
            MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))"
            $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "yes" }
            if($continue -like "yes"){
                # Installation
                MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..."
                $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date)
                $options = if($VAR.DemConfigPath){ ' COMPENVCONFIGFILEPATH="'+$VAR.DemConfigPath+'"' } else {''}
                $params = '/i "{0}" /qb /l* "{1}"{2}' -f $msiData.FileFullName, $log, $options
                Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ")
            } else{
                MsgFce "INFO: The installation has been cancelled"
            }
        } else{
            MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))"
        }
    } else{
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    }
}

# --- VMware AppVolumes Agent ---
elseif($Action -ilike "appvolumes"){
    MsgFce "INFO: --- Install/Update VMware AppVolumes Agent ---"
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild,LastWriteTime | Select-Object -Last 1
    if($swSrc){
        $msiData = Get-MsiInformation $swSrc.FullName
        $sw = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $msiData.ProductName } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString
        if($sw.VersionBuild -lt $msiData.ProductVersion){
            MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))"
            $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "yes" }
            if($continue -like "yes"){
                # Check install param
                if($VAR.AppVolManager.Count -and ($VAR.AppVolManager[0] -notlike "")){
                    # Installation
                    MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..."
                    $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date)
                    # primary AppVol Manager
                    $options = " MANAGER_ADDR="+$VAR.AppVolManager[0]
                    # default port 443 and without validation of SSL Cert (for Golden Image not joined AD)
                    $options += " MANAGER_PORT=443 EnforceSSLCertificateValidation=0"
                    $params = '/i "{0}" /qb /l* "{1}"{2} REBOOT=ReallySuppress' -f $msiData.FileFullName, $log, $options
                    Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ")

                    # Configuration of other(s) AppVol Manager(s)
                    if($VAR.AppVolManager.Count -gt 1){
                        for($i=1; $i -lt $VAR.AppVolManager.Count; $i++){
                            $paramName = "Manager$($i+1)"
                            $paramVal = $VAR.AppVolManager[$i]+":443"
                            MsgFce "INFO: AppVolumes Manager '$($paramVal)' was set to registry as '$($paramName)'"
                            New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\svservice\Parameters" -Name $paramName -Value $paramVal -PropertyType String -Force | Out-Null
                        }
                    }
                    # Next AppVol registry configuration
                    if($VAR.AppVolDisableSpoolerRestart){
                        $paramName = "DisableSpoolerRestart"
                        $paramVal = 1
                        MsgFce "INFO: AppVolumes registry settings for deactivating restarting of the spooler service"
                        New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\svservice\Parameters" -Name $paramName -Value $paramVal -PropertyType Dword -Force | Out-Null
                    }
                    if($VAR.AppVolMaxDelayTimeOutS -ne 300){
                        $paramName = "MaxDelayTimeOutS"
                        $paramVal = $VAR.AppVolMaxDelayTimeOutS
                        MsgFce "INFO: AppVolumes registry settings for deactivating restarting of the spooler service"
                        New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\svservice\Parameters" -Name $paramName -Value $paramVal -PropertyType Dword -Force | Out-Null
                    }
                } else{
                    MsgFce "Primary AppVolumes Manager not configured!"
                }
            } else{
                MsgFce "INFO: The installation has been cancelled"
            }
        } else{
            MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))"
        }
    } else{
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    }
}

# --- Microsoft FSLogix Agent ---
elseif($Action -ilike "fslogix"){
    MsgFce "INFO: --- Install/Update Microsoft FSLogix ---"
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("_")[2])}}, @{N='VersionBuild';E={[System.Version] $($_.BaseName.Split("_")[2])}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1
    if(!$swSrc){
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    } else{
        MsgFce "Installation package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($swSrc.Name); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose
    }

    # Download MS FSLogix?
    $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the '$($SwSet.$Action.Name)' installer from the web?"
    EmptyLines
    if($sel -like "yes"){
        $swDownloadUrl = "https://aka.ms/fslogix_download"
        MsgFce "...downloading the '$($SwSet.$Action.Name)' installation package (link: $($swDownloadUrl))" -Output verbose
        # download header for getting fileName
        $downHead = Invoke-WebRequest -UseBasicParsing -Method Head $swDownloadUrl
        $destFile = $InstallSrcDir +"\"+ $downHead.BaseResponse.ResponseUri.Segments[-1]
        Invoke-WebRequest $swDownloadUrl -OutFile $destFile
        #(New-Object System.Net.WebClient).DownloadFile($swDownloadUrl, $destFile)
        $swSrc = @{"FullName" = $destFile}
    }
    $FSLogixAppsSetup = "FSLogixAppsSetup.exe"
    $zipPath = "*x64/Release/$($FSLogixAppsSetup)"
    # Unzip FSLogix Agent Installer
    if(Test-Path $swSrc.FullName -PathType Leaf){
        Add-Type -Assembly System.IO.Compression.FileSystem
        $zip = [IO.Compression.ZipFile]::OpenRead($swSrc.FullName)
        $zip.Entries | Where-Object { $_.FullName -like $zipPath } | ForEach-Object {[System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, "$($InstallSrcDir)\$($_.Name)", $true)}
        $zip.Dispose()
    }

    # ReNew $swSrc by install file from ZIP
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $FSLogixAppsSetup -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1
    if($swSrc){
        $sw = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}}
        if($sw.VersionBuild -lt $swSrc.VersionBuild){
            MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))"
            $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "yes" }
            if($continue -like "yes"){
                # Installation
                MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..."
                $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date)
                $params = "/install /passive /norestart /log {0}" -f $log
                Start-Process -FilePath $swSrc.FullName -Wait -ArgumentList $params.Split(" ")
            } else{
                MsgFce "INFO: The installation has been cancelled"
            }
        } else{
            MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))"
        }
    } else{
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    }
}

# --- Microsoft Teams for VDI ---
elseif($Action -ilike "msteams"){
    MsgFce "INFO: --- Install/Update Microsoft Teams for VDI ---"
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1
    if(!$swSrc){
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    } else{
        MsgFce "Installation package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($swSrc.Name); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose
    }

    # Download MS Teams for VDI?
    $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the '$($SwSet.$Action.Name)' installer from the web?"
    EmptyLines
    if($sel -like "yes"){
        $swDownloadUrl = "https://teams.microsoft.com/downloads/desktopurl?env=production&plat=windows&arch=x64&managedInstaller=true&download=true"
        MsgFce "...downloading the '$($SwSet.$Action.Name)' installation package (link: $($swDownloadUrl))" -Output verbose
        $destFile = $InstallSrcDir +"\"+ $SwSet.$Action.SrcFile
        Invoke-WebRequest $swDownloadUrl -OutFile $destFile
        #(New-Object System.Net.WebClient).DownloadFile($swDownloadUrl, $destFile)
        $swSrc = @{"FullName" = $destFile}
    }
    if($swSrc){
        $msiData = Get-MsiInformation $swSrc.FullName
        $sw = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $msiData.ProductName } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString
        if($sw.VersionBuild -lt $msiData.ProductVersion){
            MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))"
            $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "yes" }
            if($continue -like "yes"){
                # Uninstall
                if($sw.UninstallString -notlike ""){
                    # UnInstallation
                    MsgFce "INFO: Uninstalling the old version of '$($SwSet.$Action.Name)'..."
                    $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date), "un"
                    $msiProductCode = (($sw.UninstallString -split " ")[1] -replace "/I","")
                    $params = '/X "{0}" /l* "{1}" /qb /norestart' -f $msiProductCode, $log
                    Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ")
                }
                # Installation
                MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..."
                $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date), ""
                $options = if($VAR.MsTeamsDisableAutoStart){ ' OPTIONS="noAutoStart=true"' } else {''}
                $params = '/i "{0}" /l* "{1}" ALLUSER=1 ALLUSERS=1{2}' -f $msiData.FileFullName, $log, $options
                Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ")
            } else{
                MsgFce "INFO: The installation has been cancelled"
            }
        } else{
            MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))"
        }
    }
}

# --- Microsoft OneDrive (All Users) ---
elseif($Action -ilike "onedrive"){
    MsgFce "INFO: --- Install/Update Microsoft OneDrive (All Users) ---"
    $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1
    if(!$swSrc){
        MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn
    } else{
        MsgFce "Installation package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($swSrc.Name); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose
    }

    # Download MS OneDrive?
    $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the '$($SwSet.$Action.Name)' installer from the web?"
    EmptyLines
    if($sel -like "yes"){
        $swDownloadUrl = "https://go.microsoft.com/fwlink/?linkid=844652"
        MsgFce "...downloading the '$($SwSet.$Action.Name)' installation package (link: $($swDownloadUrl))" -Output verbose
        $destFile = $InstallSrcDir +"\"+ $SwSet.$Action.SrcFile
        Invoke-WebRequest $swDownloadUrl -OutFile $destFile
        #(New-Object System.Net.WebClient).DownloadFile($swDownloadUrl, $destFile)
        # Renew swSrc from downloaded file
        $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1
    }

    if($swSrc){
        $sw = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString
        if($sw.VersionBuild -lt $swSrc.VersionBuild){
            MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))"
            $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "yes" }
            if($continue -like "yes"){
                # Installation
                MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..."
                $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date)
                $params = "/allusers" #-f $log
                Start-Process -FilePath $swSrc.FullName -PassThru -ArgumentList $params.Split(" ") | Out-Null
                # Disable OneDrive Per-Machine Standalone Update Task
                Do{ Start-Sleep 5 } While (Get-Process | Where-Object ProcessName -eq $swSrc.Name)
                $schtaskName = "OneDrive Per-Machine Standalone Update Task"
                MsgFce "INFO: Disabling scheduled task '$($schtaskName)'"
                Start-Sleep 10
                schtasks /change /tn $schtaskName  /disable | Out-Null
            } else{
                MsgFce "INFO: The installation has been cancelled"
            }
        } else{
            MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))"
        }
    }
}

# --- finish ---
elseif($Action -ilike "exit"){
    MsgFce "No action" -Output verbose
} else{
    MsgFce "ERROR => No action parameter was specified" -Output warn
}

#----------------[ Logging maintenance ]----------------
$LogFiles = Get-ChildItem $LogDir | Where-Object {-not $_.PSIsContainer}
if($LogFiles.Count -gt $VAR.LogArchiveFiles){
    EmptyLines
    MsgFce "There is currently $($LogFiles.Count) files In log archive folder '$($LogDir)' - it's more than set archive limit $($VAR.LogArchiveFiles). The oldest files will be deleted."
    $LogFiles | Sort-Object LastWriteTime | Select-Object -First ($LogFiles.Count-$VAR.LogArchiveFiles) | Remove-Item
}

# --- End of the script ---
EmptyLines
$msg = "$($VAR.ScriptName) --- End of the script --- [{0:yyyy-MM-dd HH:mm:ss}]" -f (Get-Date)
MsgFce $msg

# Transcript Log - Stop
if($Host.Name -match "ConsoleHost"){ Stop-Transcript | out-null }