Скрипт обслуживания сервера RDS

Для устойчивой работы терминального сервера необходимо проводить на нем регулярные чистки профилей пользователей, очереди печати, портов терминальных принтеров и прочих временных ресурсов.

При длительной работе 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 серверов.

Архив скрипта и вспомогательных файлов
Компактные и недорогие кэшбоксы в Новосибирске.

Вверх