Для устойчивой работы терминального сервера необходимо проводить на нем регулярные чистки профилей пользователей, очереди печати, портов терминальных принтеров и прочих временных ресурсов.
При длительной работе RDS сервера на нем могут накапливаться:
- профили пользователей в папке C:\Users, которые должны удаляться при выходе пользователя из системы, если включен механизм перемещаемых профилей (roaming profiles).
- порты принтеров из терминальных сессий, которые не удаляются из системы при выходе пользователя из системы, но для нормальной работы RDS они не нужны.
- задания очереди печати, которые не смогли достигнуть принтера назначения.
- ветки реестра пользователей, которые указывают на расположение локального профиля, и, которые должны удаляться при выходе пользователя из системы, если включен механизм перемещаемых профилей.
- задания на оптимизацию профилей пользователей, которые, впринципе, не нужны на терминальном сервере.
Для чистки терминального сервера предлагается Powershell-скрипт, который вычищает вышеперечисленные объекты из системы.
# ------------------------------------ DESCRIPTION ------------------------------------ # Скрипт для обслуживания терминального сервера Windows 2008/2012 RDS в режиме перемещаемых # профилей пользователей. # - удаление пользователей из доступа к RDP # - принудительное завершение сеанса пользователей # - удаление испорченных папок профилей (папки должны быть удалены при завершении сеанса # пользователя) # - удаление испорченных веток реестра профилей (ветки реестра должны быть удалены при завершении # сеанса пользователя) # - удаление задач по оптимизации профилей # - остановка сервиса печати, удаление всех заданий печати, запуск DeleteInactivePortSilently.exe, запуск # сервиса печати # - добавление пользователей к доступу к RDP # ------------------------------------- VARIABLES ------------------------------------- # Указание папки профилей $ProfileFolder = "C:\Users" # Указание служебных папок $ExcludedProfiles = "Default", "Default User", "Public", "All Users", "Administrator" # Указание ветки реестра профилей $ProfileRegistrySection = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" # Указание служебных веток реестра $ExcludedRegistrySections = "S-1-5-18", "S-1-5-19", "S-1-5-20" # Указание доменной группы пользователей терминала $DomainGroup = [ADSI]"WinNT://domain.com/RDS_Application_Users,group" # Указание локальной группы удаленных пользователей $LocalGroup = [ADSI]"WinNT://./Remote Desktop Users,group" # ------------------------------------------------------------------------------------- # Получение пути к скрипту $ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length – ` $MyInvocation.MyCommand.Name.Length)) # Формирование пути к лог-файлу $LogFile = $ScriptFolder + ` $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length – 4)) + ".log" # Создание лог-файла Out-File -FilePath $LogFile # Имя операционной системы $OSName = (Get-WmiObject -class Win32_OperatingSystem).Caption # Проверка наличия вспомогательных файлов и их распаковка $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) { $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "Script tools are found in " + $ScriptToolsArchive + " archive.`r`n" $LogMessage += get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "Extracting the archive into " + $ScriptTools +"." $LogMessage | Out-File -FilePath $LogFile -Append 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 "C:\Kits\7-Zip\7za.exe" -PathType Leaf) { [String]$CmdLine = "C:\Kits\7-Zip\7za.exe" [Array]$CmdLineArg = 'x', """$ScriptToolsArchive""", "-o""$ScriptTools""", "-y" # Оператор & (ampersand) указывает, то необходимо выполнить внешнюю команду, указанную после него # подробнее тут https://technet.microsoft.com/en-us/library/ee176880.aspx $LogMessage = &$CmdLine $CmdLineArg $LogMessage += "`r`n" $LogMessage | Out-File -FilePath $LogFile -Append } else { $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "C:\Kits\7-Zip\7za.exe is not found. Script execution can't be continue." $LogMessage | Out-File -FilePath $LogFile -Append Exit } } 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 } } 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 ) 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.Name -eq $ExcludedProfile) { $NotDeleteFlag = $True } } # Удаление профиля If ($NotDeleteFlag -eq $False) { # Формирование события для лога $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += $Dir.FullName + " is deleted." # Удаление папки $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 += $Dir.FullName + " is skipped as a service folder." } } Else { # Формирование события для лога $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += $Dir.FullName + " is skipped as a file." } If ($LogFile -ne $Nothing) { $LogMessage | Out-File -FilePath $LogFile -Append } else { Write-Output $LogMessage } } } } 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 ) Process { $Report = $Nothing # Преобразование пути реестра в Powershell путь $ProfileRegistrySection = $ProfileRegistrySection.Replace("HKEY_LOCAL_MACHINE","HKLM:") # Получение объектов из секции профилей реестра $Sections= Get-ChildItem $ProfileRegistrySection | Select Name ForEach ($Section in $Sections) { $LogMessage = $Nothing # Проверка, что профиль - это не служебная папка $NotDeleteFlag = $False ForEach ($ExcludedRegistrySection in $ExcludedRegistrySections) { If ($Section.Name.Contains($ExcludedRegistrySection)) { $NotDeleteFlag = $True } } # Удаление профиля из реестра If ($NotDeleteFlag -eq $False) { # Формирование события для лога $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += $Section.Name + " is deleted." # Удаление профиля Remove-Item -Path $Section -Force -Recurse } Else { # Формирование события для лога $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += $Section.Name + " is skipped as a service profile." } If ($LogFile -ne $Nothing) { $LogMessage | Out-File -FilePath $LogFile -Append } Else { Write-Output $LogMessage } } } } Function Process-ResetPrintSpooler { Param( [CmdletBinding()] [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)] [String]$LogFile ) 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" + "`r`n" $LogMessage += Invoke-Expression -command $CmdLine $LogMessage += "`r`n" + "`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 -ne $Nothing) { $LogMessage | Out-File -FilePath $LogFile -Append } Else { Write-Output $LogMessage } } } # Запрет на вход пользователей - удаление пользовательской доменной группы из локальной группы # удаленного доступа $LocalGroup.Remove($DomainGroup.Path) $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "The server is closed for users. " + $DomainGroup.name + " is removed from " + ` $LocalGroup.Name + "." $LogMessage | Out-File -FilePath $LogFile -Append # Закрытие сессий текущих пользователей $Sessions = Get-ComputerSessions -Computer localhost ForEach ($Session in $Sessions) { logoff $Session.Id /server:localhost $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += $Session.UserName + " (" + $Session.Id + ") forced to logoff from the server." $LogMessage | Out-File -FilePath $LogFile -Append } # Ожидание закрытия сессий - 30 секунд $LocalGroup.Add($DomainGroup.Path) $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "Waiting 30 seconds for users logoff." $LogMessage | Out-File -FilePath $LogFile -Append Start-Sleep -s 30 # Проверка текущих сессий на сервере $Sessions = Get-ComputerSessions -Computer localhost If ($Sessions.Count -gt 0) { $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "Currently logged on users number is " + $Sessions.Count + "." $LogMessage | Out-File -FilePath $LogFile -Append $Sessions | FT ID, UserName, State, IdleTime, LogonTime | Out-File -FilePath $LogFile -Append $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "The script processing is terminated, please reboot the server to release sessions." $LogMessage | Out-File -FilePath $LogFile -Append } Else { $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "Currently logged on users number is 0." $LogMessage | Out-File -FilePath $LogFile -Append # Удаление папок испорченных профилей (только для Windows 2008) If ($OSName.Contains("2008")) { Process-DeleteProfileFolders -ProfileFolder $ProfileFolder -ExcludedProfiles $ExcludedProfiles ` -LogFile $LogFile } Else { $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "The server operating system is not match for cleaning profile folders." $LogMessage | Out-File -FilePath $LogFile -Append } # Удаление ключей реестра испорченных профилей Process-DeleteProfileRegistry -ProfileRegistrySection $ProfileRegistrySection -ExcludedRegistrySections ` $ExcludedRegistrySections -LogFile $LogFile # Удаление из планировщика задач заданий на оптимизацию кеша профилей пользователей $LocalGroup.Add($DomainGroup.Path) $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "Delete ""Optimize Start Menu Cache Files"" scheduled tasks." $LogMessage | Out-File -FilePath $LogFile -Append Get-ScheduledTask | where {$_.taskname -like "Optimize Start Menu Cache Files*"} | ` Unregister-ScheduledTask -Confirm:$false # Очистка очереди печати Process-ResetPrintSpooler -LogFile $LogFile } # Разрешение на вход пользователей - добавление пользовательской доменной группы в локальную # группу удаленного доступа $LocalGroup.Add($DomainGroup.Path) $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "The server is opened for users. " + $DomainGroup.name + " is added to " + ` $LocalGroup.Name + "." $LogMessage | Out-File -FilePath $LogFile -Append # Удаление вспомогательных файлов $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S" $LogMessage += "`t" $LogMessage += "$ScriptTools auxiliary folder is deteted." $CmdLine = "CMD /c RD /S /Q """ + $ScriptTools + """" Invoke-Expression -command $CmdLine
Для установки этого скрипта на RDS сервер необходимо:
0. Убедиться, что сервер - это RDS-сервер с перемещаемыми пользовательскими профилями.
1. Создать папку C:\Scripts на сервере.
2. Скопировать в созданную папку файлы из архива, приложенного к этой статье.
3. В PowerShell-скрипте отредактировать раздел VARIABLES в соответствии со своими нуждами.
4. Разрешить на сервере запуск неподписанных скриптов с помощью команды
Set-ExecutionPolicy -ExecutionPolicy "RemoteSigned"
5. Установить в расписание сервера CMD-скрипт запуска Powershell-скрипта, который находится в папке C:\Scripts, от имени системы с повышением прав
powershell.exe "%~DP0Process-RDSServerMaintainance.ps1"
6. Выполнить скрипт вне расписания и проверить результат работы и Log-файл.
При небольших модификациях этот скрипт можно использовать для чистки Citrix XenApp серверов.
Архив скрипта и вспомогательных файлов можно скачать тут.