As the number of Windows Terminalservers … excuse me … Remote Desktop Session Hosts (RDSH) grew more and more, so did the time consumed for keeping them up to date every month.

Though SCCM brings some basic functionality to update servers in groups, I decided to use PowerShell only. This way, the whole process is performed in one script with almost no limitations in length and complexity which makes it pretty flexible and easy to extend. In our case I installed it as a scheduled task on a scripting server.

The script is provided as-is with no guarantee at all. If you’re not sure what you’re doing, just don’t do it. Or at least try it on some testing systems first. šŸ™‚

Detailed steps

  • Read servers to be serviced from a CSV file
  • Set systems into drain mode so that no new users can connect
  • Wait at least 10 hours until active users have disconnected
  • Activate maintenance mode in SCOM
  • Trigger the installation of new Windows Updates
  • Wait 2 hours for the update installation to complete
  • Make sure there are no active user sessions remaining
  • Reboot the servers
  • Check if all systems are back online
  • End the SCOM maintenance window
  • End drain mode so that new users can connect again
  • Set Update Cycle +1 to make sure the next group will be services tomorrow
  • Send an e-mail with a detailed status report

Requirements to use the script

  • RD session hosts based on Windows Server 2016 and above
  • A comma-separated CSV containing the hostnames of the RDSH grouped into up to 5 update cycles (less is always possible, more requires some decent editings to the script) in the below format:
    Cycle1,Cycle2,Cycle3,Cycle4,Cycle5
    RDSH01.contoso.com,RDSH03.contoso.com,RDSH05.contoso.com,RDSH06.contoso.com,
    RDSH02.contoso.com,RDSH04.contoso.com,,,
  • An active RD deployment with at least one RD connection broker (though this script is designed to work with high availabiliy deployments)
  • A domain user with the required privileges (configure “New connections allowed” in the RD deployment, reboot servers, add maintenance windows to SCOM, send e-Mails)
  • A relaying mailserver (this is not really required but the whole thing is much more fun when you get to work and see the work that has been done for you)

Adjustments

  • The maintenance window used to install updates and reboot servers starts at 2:00 am and should last for about 3 hours. You can change it in the main script (Sleep-until "02:00:00 am").
  • If you want to see what this script does, just provide a CSV with testing servers and add the “-testing” switch when running the script. It will go into a fast forward where all waiting phases are cut short to 15 minutes (instead of waiting for the night for example). The switch changes only the timing, it is NOT comparable to a WhatIf-Switch. The servers WILL for example be rebooted!

The script

<#
.SYNOPSIS
    Set systems in drain mode, install software updates an reboot
.DESCRIPTION
    Use this script to set specified systems into drain mode, wait for user sessions to be closed, install software updates, reboot computers and set back active.
.PARAMETER Connectionbroker
    FQDN of a remote desktop connection broker. ANY connection broker in the correct deployment is OK, the active one in a HA deployment gets detected automatically.
.PARAMETER UpdateCycle
    Number of the update cycle (wave) to be performed. Allowed to be between 1 and 5. If empty, the script tries to determine the number from a script, if that fails it uses 1.
.PARAMETER MailFrom
    Sender e-mail addresss for the status report e-mail.
.PARAMETER MailTo
    Recipient e-mail addresss for the status report e-mail.
.PARAMETER MailServer
   FQDN of the SMTP server to relay the status report e-mail.
.PARAMETER SCOMServer
    FQDN of the SCOM server which will be connected to set maintenance windows.
.PARAMETER CSVPath
    Path to the CSV file which contains the FQDNs of all servers to be updates grouped into update cycles.
.PARAMETER sLogPath
    Path to the logfile into which output is to be written.
.PARAMETER ActiveCyclePath
    Path to the file containing the number of the Update Cycle to be performed (reads the cycle to perform, writes an increment when completed).
.PARAMETER Testing
    If testing is true, the wait-times are minimized. This is NOT a WhatiIf, all actions will be performed!
     
.NOTES
    Name:        Update-RDSessionHosts.ps1
    Author:      Peter Stork
    Created:     2020-12-10
    Version:     1.0
    History:     2020-12-21: Added ActiveCyclePath to simplify scheduled task management and testing-switch.
                 2021-01-18: Added connectivity test for PowerShell remote ports.

.EXAMPLE
    Update-RDSessionHosts.ps1 -ConnectionBroker = "connectionbroker01.contoso.com" -UpdateCycle = 1 -MailFrom "TSmaintenance@contoso.com" -MailTo "masterchief@contoso.com" -MailServer "exchange.contoso.com" -ScomServer = "scom01.contoso.com" -CsvPath = "C:\Scripts\UpdateCycles.csv" -SLogPath = "C:\Scripts\Update-RDSessionhosts.log"
#>


####################### Variables #########################

param(
    [string]$connectionbroker = "connectionbroker01.contoso.com", 
    [int]$updateCycle,
    [string]$mailFrom = "TSmaintenance@contoso.com",
    [string]$mailTo = "masterchief@contoso.com",
    [string]$mailServer = "exchange.contoso.com",
    [string]$scomServer = "scom01.contoso.com",
    [string]$csvPath = "C:\ScheduledTasks\ServerMaintenance\UpdateCycles.csv",
    [string]$slogPath = "C:\ScheduledTasks\ServerMaintenance\Update-RDSessionhosts.log",
    [string]$activeCyclePath = "C:\ScheduledTasks\ServerMaintenance\ActiveCycle.txt",
    [switch]$testing
)

#Get the active connection broker (in case of working in a HA environment)
$activeCb = (Get-RDConnectionBrokerHighAvailability -ConnectionBroker $connectionbroker).ActiveManagementServer

# Create arrays
$rdServers = @()
$Script:ActiveSessions = @()

# Declare script-wide variables
$Script:LogOutput = ''

# Define update cycle. If parameter has not been set at script execution, the ActiveCycle is taken from the file (incremented on each execution).
if ($updateCycle -eq 0) {
    $updateCycle = Get-Content $activeCyclePath
}

####################### Functions #########################

function Log-Output {
    <#
    .SYNOPSIS
        Redirect output to console, logfile and e-mail
    .DESCRIPTION
        Use this script to redirect output to 3 targets (console, logfile, email) at once.
    .PARAMETER message
        The text to be postet in all 3 outputs.
    .PARAMETER el
        If set, creates an empty line before the log entry.
    .PARAMETER logPath
        Path to the target logfile.
 
    .NOTES
        Name:        Log-Output
        Author:      Peter Stork
        Created:     2020-12-03
        Version:     0.1
        History:     2020-12-04: Added -el switch for better overview

    .EXAMPLE
        Log-Output -message "Status: SNAFU" -el -logPath C:\Temp\mylogfile.txt
    #>

    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position = 1)][string]$message,
        [Parameter(Mandatory=$false, Position = 2)][switch]$el,
        [Parameter(Mandatory=$false, Position = 3)][string]$logPath = $slogPath
    )

    # Create an empty line upfront if -el is set.
    if ($el) {
        Out-File -FilePath $logPath -InputObject (".`r`n" + (Get-Date).ToString() + " " + $message) -Append
        Write-Host ("`r`n" + (Get-Date).ToString() + " " + $message)
        $Script:logOutput += ("</br>" + (Get-Date).ToString() + " " + $message + "</br>")
    }
    # Standard logging when -el not set
    else {
        Out-File -FilePath $logPath -InputObject ((Get-Date).ToString() + " " + $message) -Append
        Write-Host ((Get-Date).ToString() + " " + $message)
        $Script:logOutput += ((Get-Date).ToString() + " " + $message + "</br>")
    }
}


function Specify-Servers {
    <#
    .SYNOPSIS
        Load servers from a CSV into specified update cycles
    .DESCRIPTION
        Use this script transform a CSV into arrays for further use in the update-script.
    .PARAMETER csvPath
        Path to the csv file containing servers per cycle.
    .PARAMETER updateCycle
        Number of the update cycle to activate. Must be between 1 and 5
 
    .NOTES
        Name:        Specify-Servers
        Author:      Peter Stork
        Created:     2020-12-03
        Version:     0.1
        History:     -

    .EXAMPLE
        Specify-Servers -csvPath "C:\Admin\Scripts\UpdateCycles.csv" -updateCycle 1
    #>

    param (
        [parameter(Mandatory=$true)][string]$csvPath,
        [parameter(Mandatory=$true)][int]$updateCycle
    )

    #Initialize lokal variables as arrays
    $rds1 = @()
    $rds2 = @()
    $rds3 = @()
    $rds4 = @()
    $rds5 = @()

    # Import Update Cycles from CSV file, leave out blank entries
    Import-Csv $csvPath | ForEach-Object {
        if($_.Cycle1 -ne '') {$rds1 += $_.Cycle1}
        if($_.Cycle2 -ne '') {$rds2 += $_.Cycle2}
        if($_.Cycle3 -ne '') {$rds3 += $_.Cycle3}
        if($_.Cycle4 -ne '') {$rds4 += $_.Cycle4}
        if($_.Cycle5 -ne '') {$rds5 += $_.Cycle5}
    }

    
    # Activate servers from the chosen cycle.
    switch($updateCycle) {
        1 {$rdServers = $rds1; Log-Output ("Active Update Cycle: " + $updateCycle)}
        2 {$rdServers = $rds2; Log-Output ("Active Update Cycle: " + $updateCycle)}
        3 {$rdServers = $rds3; Log-Output ("Active Update Cycle: " + $updateCycle)}
        4 {$rdServers = $rds4; Log-Output ("Active Update Cycle: " + $updateCycle)}
        5 {$rdServers = $rds5; Log-Output ("Active Update Cycle: " + $updateCycle)}
        default {Clear-Variable RDServers; Log-Output "No valid cycle chosen. Please enter a value for Updatecycle between 1 and 5."}
    }
    Log-Output ("Servers to be updated: " + $rdServers)
    Return($rdServers)
}



function Set-Drainmode {
<#
.SYNOPSIS
    Sets Remote Desktop Session Hosts (RDSH) in drain mode.
.DESCRIPTION
    Use this script to set specified systems into drain mode, which means that no new RD sessions are accepted and the session count will slowly decrease to 0.
    It is recommended to have a GPO in place that automatically disconnects sessions based on time limits.
.PARAMETER RDServers
    String-Array containing a list of servers on which the drain mode is to be altered.
.PARAMETER Drainmode
    Choose to activate ($true) or disable ($false) drain mode on the defined servers.
 
.NOTES
    Name:        Set-Drainmode
    Author:      Peter Stork
    Created:     2020-12-02
    Version:     0.1
    History:     -

.EXAMPLE
    Activate Drain Mode on two servers
    Set-Drainmode -RDServers @("sessionhost01.contoso.com,sessionhost02.contoso.com" -Drainmode $true

#>
    param(
        [Parameter(Mandatory=$true, Position = 1)][string[]]$rdServers, 
        [Parameter(Mandatory=$true, Position = 2)][bool]$drainMode
    )

    foreach ($rds in $rdServers)
    {
            if($drainMode -eq $true)
            {
                Log-Output ("Turning ON drain mode for RDSH " + $rds)
                Set-RDSessionHost -SessionHost $rds -ConnectionBroker $activeCB -NewConnectionAllowed No
            }
            else
            {
                Log-Output ("Turning OFF drain mode for RDSH " + $rds)
                Set-RDSessionHost -SessionHost $rds -ConnectionBroker $activeCB -NewConnectionAllowed Yes
            }
    }
}


function Sleep-Until($future_time)
# Sleeps until a specified time. Only works within one day.
# Original Source: https://gallery.technet.microsoft.com/scriptcenter/Sleeppause-until-a-given-5c6bc7fa 
{ 
    if ([String]$future_time -as [DateTime]) { 
        if ($(get-date $future_time) -gt $(get-date)) { 
            $sec = [system.math]::ceiling($($(get-date $future_time) - $(get-date)).totalseconds) 
            start-sleep -seconds $sec 
        } 
        else { 
            Log-Output "You must specify a date/time in the future" 
            return 
        } 
    } 
    else { 
        Log-Output "Incorrect date/time format" 
    } 
}


function Trigger-AvailableSupInstall
# Triggers target computers to run "Install All (Updates)" in the SCCM Software Center.
# Original Source: https://timmyit.com/2016/08/01/sccm-and-powershell-force-install-of-software-updates-thats-available-on-client-through-wmi/
{
    Param(
    [String][Parameter(Mandatory=$true, Position=1)] $computername,
    [String][Parameter(Mandatory=$true, Position=2)] $supName)
    
    Begin {
        $appEvalState0 = "0"
        $appEvalState1 = "1"
        $applicationClass = [WmiClass]"root\ccm\clientSDK:CCM_SoftwareUpdatesManager"
    }
 
    Process {
        If ($supName -Like "All" -or $supName -like "all") {
            Foreach ($computer in $computername) {
            $application = (Get-WmiObject -Namespace "root\ccm\clientSDK" -Class CCM_SoftwareUpdate -ComputerName $computer | Where-Object { $_.EvaluationState -like "*$($appEvalState0)*" -or $_.EvaluationState -like "*$($AppEvalState1)*"})
            Invoke-WmiMethod -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (,$application) -Namespace root\ccm\clientsdk -ComputerName $computer
            } 
        }
        Else {
            Foreach ($computer in $computername) {
                $application = (Get-WmiObject -Namespace "root\ccm\clientSDK" -Class CCM_SoftwareUpdate -ComputerName $computer | Where-Object { $_.EvaluationState -like "*$($appEvalState)*" -and $_.Name -like "*$($SupName)*"})
                Invoke-WmiMethod -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (,$application) -Namespace root\ccm\clientsdk -ComputerName $computer
            }
        }
    }
End {}
}


function Get-ActiveSessions {
<#
.SYNOPSIS
    Gets the active sessions on specified RD Session Hosts
.DESCRIPTION
    Use this script to get both number of and details on active sessions on specified RD Session Hosts and display them 
.PARAMETER ActiveCB
    String containing the active management server (active[!] RD connection broker).
.PARAMETER RDServers
    String-Array containing the Remote Desktop Session Hosts to be queried for sessions.
 
.NOTES
    Name:        Get-ActiveSessions
    Author:      Peter Stork
    Created:     2020-12-02
    Version:     0.1
    History:     -

.EXAMPLE
    Get active sessions for two session hosts:
    Get-ActiveSessions -ActiveCB RDCB01.contoso.com -RDServers @("RDSH01.contoso.com", "RDSH02.contoso.com") 

#>
    param(
    [Parameter(Mandatory=$true, Position = 1)][string]$activeCb,
    [Parameter(Mandatory=$true, Position = 2)][string[]]$rdServers
    )

    # Get the active session details from the active connection broker
    $Script:ActiveSessions = @(Get-RDUserSession -ConnectionBroker $activeCB | select CollectionName,HostServer,Username,CreateTime,DisconnectTime | where {$rdServers -like $_.HostServer})
    
    # Show the results (count + details)   
    Log-Output ("Active Session Count: " + $Script:ActiveSessions.Count)
    Log-Output "Active Session Details can be found below in the log and at the end of the email report."
    Write-Host ($Script:ActiveSessions | ft | Out-String)

    # Prepare information to be sent via e-mail (count + details)
    $Script:Mail_ActiveSessions = `
        "************************************************************</br>" `
        + "There are " + $Script:ActiveSessions.Count + " active sessions. Details are shown below: </br>" `
        + "************************************************************</br>.</br>.</br>." `
        + ($Script:ActiveSessions |  ConvertTo-Html -Fragment -Property CollectionName,HostServer,Username,CreateTime,DisconnectTime | Out-String)

}


function Set-MaintenanceMode {
    <#
    .SYNOPSIS
        Configures the SCOM maintenance mode.
    .DESCRIPTION
        Use this script to configure (activate with specified duration or deactivate) the SCOM maintenance mode for a specified group of servers.
    .PARAMETER servers
        String-Array containing the servers to be put into or out of maintenance mode.
    .PARAMETER scomServer
        The FQDN of the SCOM server which will be used to configure the maintenance mode.
    .PARAMETER maintenanceMode
        $true if you want to activate MM, $false if you want to deactivate it.
    .PARAMETER minutes
        Duration of the maintenance window to be set in minutes. Must be 5 or greater.
 
    .NOTES
        Name:        Set-MaintenanceMode
        Author:      Peter Stork
        Created:     2020-12-03
        Version:     0.1
        History:     2020-12-04: Added handling for pre-existing MM entriesChanged logging to use Log-Output function

    .EXAMPLE
        Activate maintenance mode
        Set-MaintenanceMode -servers @("RDSH01.contoso.com", "RDSH02.contoso.com") -scomServer "scom01.contoso.com" -maintenanceMode $true -minutes 30
        Deactivate maintenance mode
        Set-MaintenanceMode -servers @("RDSH01.contoso.com", "RDSH02.contoso.com") -scomServer "scom01.contoso.com" -maintenanceMode $false

    #>
    param(
    [Parameter(Mandatory=$true)][string[]]$servers,
    [Parameter(Mandatory=$true)][string]$scomServer,
    [Parameter(Mandatory=$true)][bool]$maintenanceMode,
    [int]$minutes
    )
    # Make sure all required information exists
    if ($maintenanceMode -eq $true -and $minutes -LT 5) {
    Log-Output "To create a new maintance window, use the -minutes switch to enter a duration of at least 5 minutes."
    } 
    else {
    # Perform the below scriptblock on the SCOM Server which holds and alters the information about maintenance windows
        $result = Invoke-Command -ComputerName $scomServer -ScriptBlock {
            Import-Module OperationsManager
            $return = @()
            $instanceclass= Get-SCOMClass -Name Microsoft.Windows.Computer
        
         # "$Using:" forwards local variables into the remotely invoked session 
            foreach($server in $Using:servers) {
                # $instance contains the computer object from SCOM that will be altered
                $instance = Get-SCOMClassInstance -Class $instanceClass | Where-Object {$_.DisplayName -match $server}

                    # If Maintenance Mode (MM) needs to be activated, get the server object ($instance) and create a new MM for it with the actual time plus duration ($minutes)
                    if ($Using:maintenanceMode -eq $true) {
                        # Check if there already is an MM entry. If so, remove it.
                        if ($instance.InMaintenanceMode -eq $true) {
                            $mmEntry = Get-SCOMMaintenanceMode -Instance $instance
                            $endTime = (Get-Date)
                            $return += ("Removing old maintenance mode entry for " + $instance + " - Comment: " + $mmEntry)
                            Set-SCOMMaintenanceMode -MaintenanceModeEntry $MMEntry -EndTime $endTime -Comment "Preparing new maintenance windows for update deployment."
                            Start-Sleep 3
                        }
                        # Create a new maintenance mode entry
                        $endTime = ((Get-Date).AddMinutes($Using:minutes))
                        $return += ("Activating maintenance mode for " + $instance + " - Endtime: " + $endTime)
                        Start-SCOMMaintenanceMode -Instance $instance -EndTime $endTime -Reason "PlannedOther" -Comment "Windows Update Deployment via automation script."
                    } 
                
                    # If MM needs to be deactivated, get existing MM entries for the selected server object ($instance) and set its endTime to "Now" to let it end instantly 
                    else {
                        $mmEntry = Get-SCOMMaintenanceMode -Instance $instance
                        $endTime = (Get-Date)
                        $return += ("Deactivating maintenance mode for " + $instance + " - MMEntry: " + $mmEntry)
                        Set-SCOMMaintenanceMode -MaintenanceModeEntry $MMEntry -EndTime $endTime -Comment "Windows Update Deployment ended."
                    }
            }
        $return
        }
    
    }
    foreach ($r in $result) {
        Log-Output $r
    }
}


############################### Main script ##############################

# Start a new chapter in the logfile
Log-Output "****************************** Starting new script execution ******************************"

# Mode-Info
if ($testing) {
    Log-Output "***** Running in TESTING mode. Waiting times will be shortened to about 15 minutes each. Stop the script this is not what you want." -el
    Start-Sleep 10
} else {
    Log-Output "***** Running in PRODUCTION mode. Original timing will be used." -el
}

# Import information on servers which shall be updated
Log-Output "*** Main script: Importing server information." -el
$rdServers = Specify-Servers -csvPath $csvPath -updateCycle $updateCycle

# Test connectivity
Log-Output "*** Main script: Checking remote connectivity." -el
$offlineCount = 0
$offlineServers =""
foreach($rds in $rdServers) {
    if (Test-NetConnection -ComputerName $rds -Port 5985 -InformationLevel Quiet) {
        Log-Output ("Successfully connected to " + $rds + " on Port 5985")
    } 
    else {
        Log-Output ("FAILED to connect to " + $rds + " on Port 5985")
        $offlineCount = $offlineCount + 1
        $offlineServers = $offlineServers + $rds + "`r`n"
    }
}
if($offlineCount -ge 1) {
    Send-MailMessage -From $mailFrom -To $mailTo -Bodyashtml ("Hosts offline: </br>" + $offlineServers) -Subject "Update-RDSessionhosts - WARNING: Server(s) offline - Stopping" -SmtpServer $MailServer
    exit
} else {
    Log-Output "All servers could be connected from remote. Continuing."
}
        
# Turn on drain mode on servers
Log-Output "*** Main script: Turning on drain mode." -el
Set-Drainmode -RDServers $rdServers -DrainMode $true

# Wait until the end of the day to continue. Not yet possible to wait for the next day.
Log-Output "*** Main script: Sleeping until the maintenance window begins." -el
if ($testing) {
    # Shorter waiting times in test  mode.
    start-sleep 900
} else {
    Sleep-Until "11:55:00 pm"
    Start-Sleep 900
    Sleep-until "02:00:00 am"
}


# Set SCOM maintenance mode
Log-Output "*** Main script: Activating SCOM maintenance mode." -el
Set-MaintenanceMode -servers $rdServers -scomServer $scomServer -maintenanceMode $true -minutes 240

# Install updates
Log-Output "*** Main script: Triggering update installation." -el

foreach ($RDS in $rdServers) {
    Trigger-AvailableSupInstall -Computername $rds -SupName all
}


# Wait for 2 hours for the updates to complete
Log-Output "*** Main script: Waiting for updates to complete." -el
if ($testing) {
    # Shorter waiting times in test  mode.
    start-sleep 900
} else {
    Start-Sleep 7200
}

#Check number of active sessions on servers to make sure no active users will be disconnected
Log-Output "*** Main script: Checking for active sessions." -el
Get-ActiveSessions -ActiveCB $activeCB -RDS $rdServers
if ($ActiveSessions.Count -ge 3) {
    Log-Output ($ActiveSessions.Count + " active sessions found on update servers. Stopping script execution.</br>.</br>.</br>.")
    Send-MailMessage -From $MailFrom -To $MailTo -Bodyashtml ("ATTENTION - ACTIVE SESSIONS FOUND - STOPPING</br>.</br>.</br>." + $logOutput + "</br>.</br>"  + $Mail_ActiveSessions) -Subject "Update-RDSessionhosts - WARNING: Active sessions found, stopping" -SmtpServer $MailServer
    exit
}
else {
    Log-Output "Less than 4 active sessions found. Continuing."
}

# Reboot the servers
Log-Output "*** Main script: Rebooting Servers." -el
    foreach ($rds in $rdServers)
    {
        Log-Output ("Sending forced reboot request to server " + $rds)
        Restart-Computer -ComputerName $rds -Force
    }

# Wait 15 minutes to give servers enough time to reboot
Log-Output "*** Main script: Waiting for servers to reboot." -el
Start-Sleep 900

# Check if all servers are back online
Log-Output "*** Main script: Checking if servers are back online." -el
$offlineCount = 0
$offlineServers = ''
foreach($rds in $rdServers) {
    if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $rds -Quiet) {
        Log-Output ("The remote computer " + $rds + " is Online")
    } 
    else {
        Log-Output ("The remote computer " + $rds + " is Offline")
        $offlineCount = $offlineCount + 1
        $offlineServers = $offlineServers + $rds + "`r`n"
    }
}
if($offlineCount -ge 1) {
    Send-MailMessage -From $mailFrom -To $mailTo -Bodyashtml ("Hosts offline: </br>" + $offlineServers) -Subject "Update-RDSessionhosts - WARNING: Server(s) offline - Stopping" -SmtpServer $MailServer
    exit
} else {
    Log-Output "All servers are online. Continuing."
}

# Set SCOM maintenance mode
Log-Output "*** Main script: Deactivating SCOM maintenance mode." -el
Set-MaintenanceMode -servers $rdServers -scomServer $scomServer -maintenanceMode $false

# Switch drain modes on servers (turn DM off for servers which just installed updates, turn DM off for servers which will be working normal again)
Log-Output "*** Main script: Turning off drain mode." -el
Set-Drainmode -RDServers $rdServers -DrainMode $false

# Increment ActiveCycle to prepare the next script execution.
if ($updateCycle -le 4) {
    # If cycle less or equal 4, increment by one to get to the next cycle.
    Out-File -FilePath $activeCyclePath -InputObject ($updateCycle + 1) -Encoding ascii -Force
    Log-Output ("*** Main script: Writing ActiveCycle. Next cycle will be: " + ($updateCycle + 1)) -el
} else {
    # If updateCycle is 5, set ActiveCycle to 1 to start a new cycle.
    Out-File -FilePath $activeCyclePath -InputObject 1 -Encoding ascii -Force
    Log-Output ("*** Main script: Writing ActiveCycle. Next cycle will be: 1") -el
}

# Send status report
Log-Output "*** Main script: Sending status report. Goodbye." -el
Send-MailMessage -From $mailFrom -To $mailTo -Bodyashtml ($LogOutput) -Subject "Update-RDSessionhosts - Info: Script execution successfully ended." -SmtpServer $MailServer

In case you have assigned the parameters at the beginning of the script to your needs, a scheduled task may look like this:

  • Program/script: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
  • Add arguments (optional): -ExecutionPolicy Unrestricted -WindowStyle Hidden -File "C:\ScheduledTasks\ServerMaintenance\Update-RDSessionHosts.ps1" -csvPath "C:\ScheduledTasks\ServerMaintenance\UpdateCycles_Prod.csv" -slogPath "C:\ScheduledTasks\ServerMaintenance\Update-RDSessionHosts_Prod.log"

Credits and sources: