Для устойчивой работы терминального сервера необходимо проводить на нем регулярные чистки профилей пользователей, очереди печати, портов терминальных принтеров и прочих временных ресурсов.
При длительной работе 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 серверов.
Архив скрипта и вспомогательных файлов можно скачать тут.
