Отключение и завершение простаивающих сеансов на серверах Remote Desktop Session Host в зависимости от дня месяца и членства в доменной группе

Manage RDS Remote Desktop Session Host Sessions with PowershellКак правило, для отключения неактивных и завершения отключенных сессий на серверах сеансов служб удалённых рабочих столов Remote Desktop Session Host в Windows Server 2012 R2 администраторы используют возможности групповых политик домена Active Directory. Однако иногда может возникать потребность в управлении неактивными сеансами по хитрым правилам, которые невозможно уложить в рамки стандартных механизмов GPO или даже GPP. В таких случаях для управления сеансами можно прибегнуть к возможностям PowerShell.

В рассматриваем примере стоит задача организовать управление неактивными сеансами в ферме RD Connection Broker (RDCB) с несколькими серверами RD Session Host (RDSH) исходя из того условия, что все пользователи в ферме RDS делятся на две категории:

    1. Стандартные пользовали, для которых используются одинаковые правила сессионных таймаутов вне зависимости от каких-либо факторов. Правила :
      • Отключение простаивающих сеансов - через 1 час
      • Завершение отключённых сеансов - через 2 часа
    2. Пользователи со специальным режимом сессионных таймаутов, который действует только в определённые дни месяца (с 25 числа каждого месяца по 5 число каждого последующего месяца). В эти дни данная группа пользователей выполняет круглосуточные расчёты, в том числе и в отключенных сеансах, поэтому сессии не должны отключаться. В остальные дни месяца сессионные таймауты применяются по аналогии со стандартными пользователями из п.1.

Отделение пользователей со специальным режимом выполняется с помощью членства в доменной группе безопасности Active Directory.

Пример реализации в виде PS-скрипта RDS-Logoff-Inactive.ps1:

# Требования к модулям PS:  ActiveDirectory, RemoteDesktop
#
# Члены специальной группы $SpecialGroup не затрагиваются при отключении 
# простаивающих и завершении отключенных сессий в дни месяца из $SpecialDays

# Блок переменных
# $SpecialDays - Дни месяца, в которые члены группы $SpecialGroup не отключаются
# $MaxConnectedInactiveTime - Время простоя в подключенном состоянии, затем отключение сессии (мс)
# $MaxDisconnectedTime - Время простоя с момента отключения сессии (мс)
#
$ConnectionBroker = ""
$SessionHostCollection = "RDCollection1"
$SpecialGroup = "RDS-Extended-Session-Users"
$SpecialDays = @(01,02,03,04,05,25,26,27,28,29,30,31)  
$MaxConnectedInactiveTime = 3600000   # 1 час                 
$MaxDisconnectedTime = 7200000        # 2 часа
$LogFilePath = $($script:MyInvocation.MyCommand.Path).Replace('.ps1','.log')

# Функция загрузки модуля PowerShell
#
Function Load-Module ($MName)
{
    $retVal = $true
    If (!(Get-Module -Name $MName))
    {
        $retVal = Get-Module -ListAvailable | Where { $_.Name -eq $MName }
        If ($retVal)
        {
          Try { Import-Module $MName -ErrorAction SilentlyContinue }
          Catch { $retVal = $false }
        } Else {
          Write-Host $MName "Module does not exist. Please check that the module is installed."      
        }
    }
    Return $retVal
}

# Функция записи в лог-файл
#
Function WriteLog ($Text) 
{  
    $TimeStamp = (Get-Date).ToString("dd.MM.yyyy HH:mm:ss")
    Write-Host $Text    
    Add-Content $LogFilePath "$($TimeStamp)`t $Text"
}


# Загрузка модулей PowerShell
#
If (!(Load-Module "ActiveDirectory")) {return}
If (!(Load-Module "RemoteDesktop")) {return}

$GroupMembers = Get-ADGroupMember -Identity $SpecialGroup -Recursive
$ToDayIsSpecial = $SpecialDays -contains $(Get-Date -Format dd)

If ($ConnectionBroker -eq "") {
 $HAFarm = Get-RDConnectionBrokerHighAvailability
 $ConnectionBroker = $HAFarm.ActiveManagementServer
}
$Sessions = Get-RDUserSession -ConnectionBroker $ConnectionBroker -CollectionName $SessionHostCollection

ForEach ($Session in $Sessions) {
    
    # Пропускаем пользователей из специальной группы в специальные дни
    #
    If ($ToDayIsSpecial -eq $true -and $GroupMembers.SamAccountName -contains $Session.UserName){Continue}
    
    # Пропускаем активные сессии 
    #
    If ($Session.SessionState -eq "STATE_ACTIVE"){Continue}
    
    # Отключаем простаивающие сессии
    #
    If ($Session.SessionState -eq "STATE_CONNECTED" -and $Session.IdleTime -ge $MaxConnectedInactiveTime) {
        Try {           
            WriteLog "Disconnect RD User: $($Session.UserName) `t Server: $($Session.HostServer) `t Session disconnect time: $($Session.DisconnectTime.ToString("dd.MM.yyyy HH:mm:ss")) `t Idle time: $([TimeSpan]::FromMilliseconds($Session.IdleTime).ToString())"      
            Disconnect-RDUser -HostServer $Session.HostServer -UnifiedSessionID $Session.UnifiedSessionId -Force -ErrorAction Stop
              } Catch {
            WriteLog "ERROR! Can't disconnect RD User: $($Session.UserName) `t Server: $($Session.HostServer) `n $($_)" 
            }
        Continue
    }
    
    # Завершаем отключенные сессии
    #
    If ($Session.SessionState -eq "STATE_DISCONNECTED" -and $Session.IdleTime -ge $MaxDisconnectedTime) { 
        
        Try { 
            WriteLog "Logoff RD User: $($Session.UserName) `t Server: $($Session.HostServer) `t Session disconnect time: $($Session.DisconnectTime.ToString("dd.MM.yyyy HH:mm:ss")) `t Idle time: $([TimeSpan]::FromMilliseconds($Session.IdleTime).ToString())"
            Invoke-RDUserLogoff -HostServer $Session.HostServer -UnifiedSessionID $Session.UnifiedSessionId -Force -ErrorAction Stop
        } Catch {
            WriteLog "ERROR! Can't logoff RD User: $($Session.UserName) `t Server: $($Session.HostServer) `n $($_)"       
        } 
    }
}

Скрипт не имеет обработки входных параметров, поэтому все исходные данные мы указываем в начале скрипта в блоке переменных. В ходе выполнения скрипт создаёт лог-файл о результатах отключения и завершения сессий в том же каталоге, где размещён сам скрипт.

Скрипт размещаем на каким-нибудь сервере, отличном от серверов RDSH, на которых будет выполняться скриптовая обработка сессий. Например, можно разместить этот скрипт на сервере с ролью RD Connection Broker (RDCB), если эта роль работает на выделенном сервере. Для выполнения скрипта на выбранном сервере потребуется установка PowerShell-модулей RemoteDesktop и ActiveDirectory. Если первый модель уже присутствует на сервере RDCB, то второй можно доустановить PS-командой:

Install-WindowsFeature -Name "RSAT-AD-PowerShell"

Автоматический периодический запуск скрипта можно настроить в Task Scheduler от имени специально созданной сервисной учётной записи Group Managed Service Account (gMSA). Пример того, как создать и установить учётную запись gMSA можно найти в статьях Вики:

В нашем примере в домене AD создана учётная запись gMSA с именем KOM\s-S06$ и установлена на сервере RDCB. Эта учётная запись должна быть включена в локальную группу Administrators на всех серверах RDSH, сессиями которых мы планируем управлять из скрипта. Также учётной записи gMSA на сервере RDCB потребуется дать права на чтение каталога со скриптом и права на запись в файл лога (для этого потребуется предварительно создать пустой лог-файл).

Прежде, чем создавать задание планировщика по запуску скрипта, выполним его проверочный запуск от имени учётной записи gMSA с помощью утилиты PsExec:

PsExec64.exe -i -u KOM\s-S06$ -p ~ cmd.exe

Запустив в контексте пользователя gMSA интерпретатор командной строки cmd.exe, попробуем выполнить скрипт:

powershell.exe -NoProfile -command "C:\Scripts\RDS-Logoff-Inactive.ps1"

Если скрипт отработал как надо и создал записи в лог-файл об отключенных и завершённых сессиях в ферме RD Connection Broker, выполняем его добавление в планировщик заданий Task Scheduler:

$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -command `"C:\Scripts\RDS-Logoff-Inactive.ps1`""
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15) -RepetitionDuration ([System.TimeSpan]::MaxValue)
$SvcUser = New-ScheduledTaskPrincipal -UserID KOM\s-S06$ -LogonType Password
Register-ScheduledTask -TaskName "RDS Logoff Inactive Users" -Action $Action -Trigger $Trigger -Principal $SvcUser

Таким образом, задание планировщика каждые 15 минут будет подключаться к ферме RDCB, получать из фермы информацию о всех сессиях пользователей на серверах RDSH и отключать простаивающие и завершать отключенные сессии пользователей.

Всего комментариев: 8 Комментировать

  1. equinoxnet /

    Спасибо, любопытная статья. И очень насущная (ведь в каждом предприятии есть бухгалтерский отдел, не терпящий прерываний в работе).
    Но есть вопросы:

    1) Нужно ли выставлять в настройках сессии значения таймаутов? https://imgur.com/yhUcKOB
    Или же эти параметры задаются лишь в скрипте?

    2) Можно ли сделать так, чтобы пользователей из группы $SpecialGroup не отключало от сеанса про простою? Сколько ни тестирую — всё равно возникает окно "Таймер входа в систему истек" — а после и отключение сеанса ("Удаленный компьютер не получил от вас никакого ввода").

    Как я понял, сессия получает время простоя (ненулевое значение $Session.IdleTime) только после отключения (и ее статус становится не STATE_ACTIVE, а STATE_CONNECTED) — следовательно, пользователя все равно отключит от сеанса, даже если этот пользователь состоит в группе $SpecialGroup?

    1. Алексей Максимов / Автор записи

      1) Так как фактически управление отключением простаивающих и завершением отключенных сессий берёт на себя скрипт, настройки в прочих местах (в свойствах коллекции сеансов или в локальных/доменных групповых политиках), как минимум, не требуются. Самое главное, что если такие настройки в других местах и есть, то они по своей логике не должны конфликтовать с настройками в скрипте. Иначе получите полную неразбериху.

      2) В дни, указанные в переменной $SpecialDays, пользователи из группы $SpecialGroup, согласно логике скрипта, не должны отключаться вовсе. Если в Вашем случае это происходит, то ищите проблему в другом месте (в свойствах коллекции сеансов или в групповых политиках). Сообщение "Таймер входа в систему истек" намекает на то, что где-то в другом месте настроено ограничение времени сеанса (в не зависимости от его состояния).

      1. equinoxnet /

        Спасибо, буду разбираться. Если полностью отключить в настройках ограничения сеансов, то тогда (в моем случае) сессии "висят" сколь угодно долго (оставлял на ночь). Групповые политики управления временем сеансов не настраивал.

  2. equinoxnet /

    Разобрался со скриптом, пришлось немного его видоизменить: значения простоя сессий получаю через встроенную утилиту quser (query user) — там таймауты активных сессий (STATE_ACTIVE) видны нормально, в отличие от командлета Get-RDUserSession. В остальном все так же. Проверка на тестовом сервере не выявила проблем, скрипт отключает «обычных» пользователей, не трогая привилегированных.
    Если интересно, могу приложить свой вариант скрипта.

    1. Алексей Максимов / Автор записи

      Можете выслать на почту (адрес внизу страницы), выложу скрипт в Вики.

  3. Обратная ссылка: Решение проблемы с некорректным IdleTime в Get-RDUserSession /

  4. Александр /

    А как быть с такой ситуацией: У удаленного сотрудника лагает интернет. Он отключен уже 10 минут или дольше. Но его сессия на сервере до сих пор active. И ее надо вручную делать disconnect. Иначе он заходит в новую сессию, и не видит своих открытых программ. Если ограничить юзера, одной сессией вообще, то он не может войти в старую Активную сессию, так как она не переходит в состояние disconnect.
    Вопрос, каким образом происходит проверка, подключен пользователь или нет, и почему в ней возникают такие сбои.

    1. Whols /

      Если сессия активна, то пользователь должен подключиться " в неё", даже если заходит с другого устройства. Попробуйте очистить профиль пользователя.

Добавить комментарий