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:
- Christoph S.
- wingwaa (Technet Gallery)
- Timmy Andersson (TimmyIT.com)
- Patrick Squire (resdevops.com)