В заметках "Скрипт обслуживания сервера RDS" и "PowerShell - Удаление профилей с сервера" я уже рассказывал о скрипте для очистки профилей и очереди печати на терминальных серверах. В этой публикации я размещаю обновленный текст кода Powershell, который я оптимизировал для обслуживания терминальных серверов Windows Server 2012.
# --- Description ---
# This script is for maintainance of Windows 2012 RDS server in roaming profile mode.
# - remove users from access to RDP
# - force users logoff from the server
# - delete broken profile folders (folders must be removed after user logoff)
# - delete broken profile registry section (registry sections must be removed after user logoff)
# - delete profile oprimization scheduled tasks
# - stop print spooler, delete all print jobs, run DeleteInactivePortSilently.exe, start print spooler
# - restore users access to RDP
# - reboot the server on Monday
# --- Declare variables ---
$ExcludedProfilePaths = New-Object System.Collections.ArrayList
$ExcludedRegistrySections = New-Object System.Collections.ArrayList
# --- Input data ---
# !!!!! DON'T FORGET TO EXCLUDE SERVICE ACCOUNTS. LOCAL ACCOUNTS WILL BE EXCLUDED AUTOMATICALLY !!!!!
$ProfileFolder = "C:\Users"
$ProfileRegistrySection = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
$DomainRDSGroup = "RDS_Application_Users"
$LocalRDSGroup = "Remote Desktop Users"
$ZipArhiverPath = "C:\Scripts\7za.exe"
[void] $ExcludedProfilePaths.AddRange( ("C:\Users\All Users", "C:\Users\Default", "C:\Users\Default User", `
"C:\Users\Public") )
[void] $ExcludedRegistrySections.AddRange( ("S-1-5-18", "S-1-5-19", "S-1-5-20") )
# --- Functions ---
Function Write-ScriptLog {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Message,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$LogFile
)
Process {
$LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
$LogMessage += "`t"
$LogMessage += $Message
$LogMessage | Out-File -FilePath $LogFile -Append
}
}#End Function
Function Get-ComputerSessions {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Computer
)
Process {
$Report = quser /server:$Computer | Select-Object -Skip 1 | ForEach-Object {
$CurrentLine = $_.Trim() -Replace '\s+',' ' -Split '\s'
$HashProps = @{
UserName = $CurrentLine[0]
ComputerName = $Computer
}
# If session is disconnected, different fields will be selected
If ($CurrentLine[2] -eq 'Disc') {
$HashProps.SessionName = $null
$HashProps.Id = $CurrentLine[1]
$HashProps.State = $CurrentLine[2]
$HashProps.IdleTime = $CurrentLine[3]
$HashProps.LogonTime = $CurrentLine[4..6] -join ' '
} Else {
$HashProps.SessionName = $CurrentLine[1]
$HashProps.Id = $CurrentLine[2]
$HashProps.State = $CurrentLine[3]
$HashProps.IdleTime = $CurrentLine[4]
$HashProps.LogonTime = $CurrentLine[5..7] -join ' '
}
New-Object -TypeName PSCustomObject -Property $HashProps |
Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleTime,LogonTime
}
Return $Report
}
}#End Function
Function Process-DeleteProfileFolders {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
[String]$ProfileFolder,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
[String[]]$ExcludedProfiles,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
[String]$LogFile = $Nothing
)
Process {
$Report = $Nothing
# Получение объектов из папки профилей
$SubDirs= Get-ChildItem $ProfileFolder -Force
# Обработка объектов из папки профилей
ForEach ($Dir in $SubDirs) {
$LogMessage = $Nothing
# Проверка, что объект существуюет и это папка
If (Test-Path $Dir.FullName -PathType Container) {
# Проверка, что профиль - это не служебная папка
$NotDeleteFlag = $False
ForEach ($ExcludedProfile in $ExcludedProfiles) {
If ($Dir.FullName -eq $ExcludedProfile) {
$NotDeleteFlag = $True
}
}
# Removing the profile
If ($NotDeleteFlag -eq $False) {
$LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
$LogMessage += "`t"
$LogMessage += "Delete " + $Dir.FullName + "."
# Executing removing process
$CmdLine = "CMD /c RD /S /Q """ + $Dir.FullName + """"
Invoke-Expression -command $CmdLine
} Else {
$LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
$LogMessage += "`t"
$LogMessage += "Skip " + $Dir.FullName + " - service folder."
}
} Else {
$LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
$LogMessage += "`t"
$LogMessage += "Skip " + $Dir.FullName + " - file."
}
If ($LogFile.Length -gt 0) {
$LogMessage | Out-File -FilePath $LogFile -Append
} else {
Write-Output $LogMessage
}
}
}
}#End Function
Function Process-DeleteProfileRegistry {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
[String]$ProfileRegistrySection,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
[String[]]$ExcludedRegistrySections,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
[String]$LogFile = $Nothing
)
Process {
$Report = $Nothing
# Convering the registry path to Powershell path
$ProfileRegistrySection = $ProfileRegistrySection.Replace("HKEY_LOCAL_MACHINE","HKLM:")
# Requesting profile sections from the registry
$Sections= Get-ChildItem $ProfileRegistrySection
ForEach ($Section in $Sections) {
$LogMessage = $Nothing
# Filtering exclusions
$NotDeleteFlag = $False
ForEach ($ExcludedRegistrySection in $ExcludedRegistrySections) {
If ($Section.Name.Contains($ExcludedRegistrySection)) {
$NotDeleteFlag = $True
}
}
# Removing profile sections
If ($NotDeleteFlag -eq $False) {
$LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
$LogMessage += "`t"
$LogMessage += "Delete " + $Section.Name + "."
# Processing deleting the profile section
$SectionPath = $Section.Name.Replace("HKEY_LOCAL_MACHINE","HKLM:")
Remove-Item -Path $SectionPath -Force -Recurse
} Else {
$LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
$LogMessage += "`t"
$LogMessage += "Skip " + $Section.Name + " - service profile."
}
If ($LogFile.Length -gt 0) {
$LogMessage | Out-File -FilePath $LogFile -Append
} Else {
# Write-Output $LogMessage
}
}
}
}#End Function
Function Process-ResetPrintSpooler {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
[String]$LogFile = $Nothing
)
Process {
$LogMessage = $Nothing
$Error.Clear()
# Остановка сервиса
Try {
Stop-Service -Name "Spooler" -Force
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" +
"Print Spooler service is stopped." + "`r`n"
} Catch {
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error + "`r`n"
}
# Очистка заданий
Try {
Remove-Item -Path "C:\Windows\System32\spool\PRINTERS\*.*" -Force
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + "Print queue is purged." + "`r`n"
}
Catch {
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error + "`r`n"
}
# Очистка неактивных портов принтеров терминального сервера
$CmdLine = $ScriptTools + "DeleteInactivePortSilently.exe"
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" +
"Delete Inactive TS Ports by DeleteInactivePortSilently.exe command." + "`r`n"
$CommandOutput = Invoke-Expression -command $CmdLine
$CommandOutput | ForEach-Object {
If ($_.Length -gt 0) {
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $_ + "`r`n"
}
}
# Запуск сервиса
Try {
Start-Service -Name "Spooler"
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + "Print Spooler service is started."
} Catch {
$LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error
}
# Запись события в лог-файл
If ($LogFile.Length -gt 0) {
$LogMessage | Out-File -FilePath $LogFile -Append
} Else {
Write-Output $LogMessage
}
}
}#End Function
Function Get-Monday {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[DateTime]$DateValue
)
Process {
For ($i=0; $i -le 6; $i++) {
If (($DateValue).adddays(-$i).DayOfWeek -eq "Monday") {
Return ($DateValue).adddays(-$i)
}
}
}
}#End Function
# --- Start ---
$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length - `
$MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" + `
$MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"
$OSName = (Get-WmiObject -class Win32_OperatingSystem).Caption
$MondayDate = Get-Monday (Get-Date)
$CurrentDate = Get-Date
$DomainRDSGroupObject = [ADSI]"WinNT://glaverbel.com/$DomainRDSGroup,group"
$LocalRDSGroupObject = [ADSI]"WinNT://./$LocalRDSGroup,group"
# Adding local accounts SIDs to exclusions
$LocalAccounts = Get-WmiObject Win32_UserAccount -Filter "Domain='$env:computername'"
ForEach ($LocalAccount in $LocalAccounts) {
$CurrentSID = $LocalAccount.SID
If ($CurrentSID -ne $Nothing) {
[void] $ExcludedRegistrySections.Add( $CurrentSID )
}
}
# Adding local accounts Profiles paths to exclusions
ForEach ($CurrentSID in $ExcludedRegistrySections) {
$CurrentPath = (Get-WmiObject Win32_UserProfile -Filter "SID='$CurrentSID'").LocalPath
If ($CurrentPath -ne $Nothing) {
[void] $ExcludedProfilePaths.Add( $CurrentPath )
}
}
# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile
# Check OS version
If (-Not $OSName.Contains("2012")) {
Write-ScriptLog -LogFile $LogFile -Message ("The operating system is not match for this script.")
Exit
}
# Extracting additional files
$ScriptTools = $ScriptFolder + `
$MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + "\"
$ScriptToolsArchive = $ScriptFolder + `
$MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".zip"
If (Test-Path $ScriptToolsArchive -PathType Leaf) {
Write-ScriptLog -LogFile $LogFile -Message ("Script tools are found in " + `
$ScriptToolsArchive + " archive.")
Write-ScriptLog -LogFile $LogFile -Message ("Extracting the archive into " + $ScriptTools +".")
New-Item -Path $ScriptTools -ItemType directory -Force
$Shell = New-Object -com shell.application
$ArchiveItem = $shell.NameSpace($ScriptToolsArchive)
ForEach($Item in $ArchiveItem.items()) {
$Shell.Namespace($ScriptTools).copyhere($Item, 0x14)
}
If (Test-Path $ZipArhiverPath -PathType Leaf) {
[String]$CmdLine = $ZipArhiverPath
[Array]$CmdLineArg = 'x', """$ScriptToolsArchive""", "-o""$ScriptTools""", "-y"
# Оператор & (ampersand) указывает, то необходимо выполнить внешнюю команду, указанную после него
# подробнее тут https://technet.microsoft.com/en-us/library/ee176880.aspx
Write-ScriptLog -LogFile $LogFile -Message (&$CmdLine $CmdLineArg)
} Else {
Write-ScriptLog -LogFile $LogFile -Message ("C:\Scripts\7za.exe is not found. " + `
"Script execution can't be continue.")
Exit
}
}
# Forbiging users for loging - Removing users from Remote Desktop group
Write-ScriptLog -LogFile $LogFile -Message ("--- Closing the server for users ---")
$LocalRDSGroupObject.Remove($DomainRDSGroupObject.Path)
Write-ScriptLog -LogFile $LogFile -Message ($DomainRDSGroupObject.Name.ToString() + `
" is removed from " + $LocalRDSGroupObject.Name.ToString() + ".")
Write-ScriptLog -LogFile $LogFile -Message ("The server is closed for users.")
# Logging off active users
$Sessions = Get-ComputerSessions -Computer localhost
ForEach ($Session in $Sessions) {
logoff $Session.Id /server:localhost
Write-ScriptLog -LogFile $LogFile -Message ($Session.UserName + " (" + $Session.Id + `
") forced to logoff from the server.")
}
# Waiting for 30 seconds
Write-ScriptLog -LogFile $LogFile -Message ("Waiting 30 seconds for users logoff.")
Start-Sleep -s 30
# Check if there are no users sessions
$Sessions = Get-ComputerSessions -Computer localhost
If ($Sessions.Count -gt 0) {
Write-ScriptLog -LogFile $LogFile -Message ("Currently logged on users number is " + `
$Sessions.Count + ".")
$ActiveSessions = "`r`n"
$ActiveSessions += $Sessions | FT ID, UserName, State, IdleTime, LogonTime
Write-ScriptLog -LogFile $LogFile -Message ($ActiveSessions)
Write-ScriptLog -LogFile $LogFile -Message ("The script processing is terminated, " + `
"please reboot the server to release sessions.")
} Else {
# Executing cleanup
Write-ScriptLog -LogFile $LogFile -Message ("Currently logged on users number is 0.")
# Profiles cleanup
Write-ScriptLog -LogFile $LogFile -Message ("--- Profiles cleanup ---")
Process-DeleteProfileFolders -ProfileFolder $ProfileFolder `
-ExcludedProfiles $ExcludedProfilePaths -LogFile $LogFile
# Registry cleanup
Write-ScriptLog -LogFile $LogFile -Message ("--- Registry cleanup ---")
Process-DeleteProfileRegistry -ProfileRegistrySection $ProfileRegistrySection `
-ExcludedRegistrySections $ExcludedRegistrySections -LogFile $LogFile
# Task scheduler cleanup
Write-ScriptLog -LogFile $LogFile -Message ("--- Task scheduler cleanup ---")
Write-ScriptLog -LogFile $LogFile -Message ("Delete ""Optimize Start Menu Cache Files"" scheduled tasks.")
Get-ScheduledTask | Where {$_.taskname -like "Optimize Start Menu Cache Files*"} | `
Unregister-ScheduledTask -Confirm:$false
# Print prooler cleanup
Write-ScriptLog -LogFile $LogFile -Message ("--- Print prooler cleanup ---")
Process-ResetPrintSpooler -LogFile $LogFile
}
# Allowing users for loging - Adding users to Remote Desktop group
Write-ScriptLog -LogFile $LogFile -Message ("--- Openning the server for users ---")
$LocalRDSGroupObject.Add($DomainRDSGroupObject.Path)
Write-ScriptLog -LogFile $LogFile -Message ($DomainRDSGroupObject.Name.ToString() + " is added to " +
$LocalRDSGroupObject.Name.ToString() + ".")
Write-ScriptLog -LogFile $LogFile -Message ("The server is opened for users.")
# Remove auxiliary files
$CmdLine = "CMD /c RD /S /Q """ + $ScriptTools + """"
Invoke-Expression -command $CmdLine
Write-ScriptLog -LogFile $LogFile -Message ("$ScriptTools auxiliary folder is deleted.")
#Reboot server
If ($CurrentDate.Date -eq $MondayDate.Date) {
Write-ScriptLog -LogFile $LogFile -Message ("Today is Monday - rebooting the server.")
Restart-Computer -ComputerName "localhost"
}
Указанный скрипт рекомендуется поставить в планировщик Windows на ежедневное выполнение в ночные часы, когда на серверах нет пользователей. Запуск задания нужно выполнять от имени системы (от имени пользователя SYSTEM).
