Отключение и завершение простаивающих сеансов на серверах Remote Desktop Session Host в зависимости от дня месяца и членства в доменной группе. Вариант 2: Избавляемся от некорректных значений IdleTime в Get-RDUserSession

Incorrect IdleTime in Get-RDUserSessionРанее мы рассматривали пример скриптовой реализации контроля простаивающих пользовательских сеансов на серверах Remote Desktop Session Host (RDSH). В комментариях к этой заметке один из наших постоянных читателей - Солодовников Матвей (aka equinox) отметил то обстоятельство, что при использовании представленного скрипта возникают проблемы с правильностью определения времени простоя сеансов. И связано это с тем, что командлет Get-RDUserSession может возвращать некорректное значение времени простоя IdleTime для активных сессий со статусом STATE_ACTIVE. Наша практика использования скрипта подтвердила наличие этой проблемы, поэтому в данной заметке мы рассмотрим альтернативный вариант скрипта, решающий данный вопрос.

В переписке по электронной почте Матвей любезно предложил нам откорректированный вариант скрипта, решающий данную проблему путём замены механизма получения времени простоя пользовательских сеансов. Альтернативный вариант скрипта для получения времени простоя использует данные, полученные от "олдскульной" утилиты query (query user / quser), у которой нет таких проблем, как у командлета Get-RDUserSession. Однако предложенный вариант был адаптирован под использование скрипта на выделенном сервере RDSH, не являющемся участником фермы RD Connection Broker (RDCB). Поэтому мы подправили его для использования с фермой RDCB и возможности вызова на сервере без роли RDSH, например, на сервере с ролью RDCB.

# Требования к модулям 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 = 60 # 60 минут                 
$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-Object { $_.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"
}

# Функция возвращает значение простоя сессии в минутах
#
Function Get-TotalMinutes ($duration) {
    $TotalMinutes = 0
    
    # активный сеанс (quser возвращает точку ".", поэтому простой сессии равен нулю)
    if ($duration -match "^\.$") { [int]$TotalMinutes = 0 }
    
    # только минуты ("5")
    if ($duration -match "(^\d{1,2}$)") { [int]$TotalMinutes = $matches[1] }
    
    # часы и минуты ("1:25")
    if ($duration -match "^(\d{1,2}):(\d{1,2})$") { [int]$TotalMinutes = (New-TimeSpan -Hours $matches[1] -Minutes $matches[2]).Totalminutes }
    
    # дни, часы и минуты ("1+2:34")
    if ($duration -match "^(\d{1,2})\+(\d{1,2}):(\d{1,2})$") { [int]$TotalMinutes = (New-TimeSpan -Days $matches[1] -Hours $matches[2] -Minutes $matches[3]).Totalminutes }
    
    # $matches
    $TotalMinutes
} 
    
# Загрузка модулей 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) {
    
    $RDHost = $Session.HostServer
    
    # Получаем значение простоя каждой сессии
    #
    quser $Session.UserName /Server:$RDHost | Select-Object -Skip 1 | ForEach-Object {
        $IdleTime2 = ""
        $IdleTime2 = ($_.Trim() -Replace '\s+', ' ' -Split '\s')
    }
    
    $IdleTime = ""
    
    # Если сессия активна (STATE_ACTIVE), то значение простоя будет в четвертом столбце (0-4)
    #
    if ($IdleTime2.Count -eq "7") { $IdleTime = $IdleTime2[4] }
    
    # Если сессия в статусе STATE_CONNECTED, то значение простоя будет в третьем столбце (0-3)
    #
    if ($IdleTime2.count -eq "6") { $IdleTime = $IdleTime2[3] }
            
    $IdleTimeMinutes = Get-TotalMinutes $IdleTime

    # Пропускаем пользователей из специальной группы в специальные дни
    #
    If ($ToDayIsSpecial -eq $true -and $GroupMembers.SamAccountName -contains $Session.UserName) { Continue }
   
    # Отключаем простаивающие сессии 
    #
    If (($Session.SessionState -eq "STATE_ACTIVE" -or $Session.SessionState -eq "STATE_CONNECTED") -and ($IdleTimeMinutes -ge $MaxConnectedInactiveTime)) {
        Try {           
            switch ($Session.SessionState) {
                STATE_ACTIVE { WriteLog "Disconnect RD User: $($Session.UserName) `t SessionState: $($Session.SessionState) `t Server: $($Session.HostServer) `t Idle time: $IdleTimeMinutes minutes" }
                STATE_CONNECTED { WriteLog "Disconnect RD User: $($Session.UserName) `t SessionState: $($Session.SessionState) `t Server: $($Session.HostServer) `t Session disconnect time: $($Session.DisconnectTime.ToString("dd.MM.yyyy HH:mm:ss")) `t Idle time: $IdleTimeMinutes minutes" }
            }
            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 $($_)"       
        } 
    }
}

Представленный вариант скрипта по нашим наблюдениям работает уже несколько недель так, как это и задумано.

Мы намеренно не стали обновлять опубликованную ранее заметку с первичным вариантом скрипта, а создали отдельную заметку. Ибо, не смотря на то, что проблема с некорректно возвращаемым значением простоя из командлета Get-RDUserSession имеет давнюю историю, всё-таки остаётся надежда на то, что когда-нибудь проблема будет исправлена со стороны Microsoft и первичный вариант скрипта можно будет использовать без оговорок.

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

  1. Sasha Odarchuk /

    Спасибо!
    Алексей, у вас ферма на Win 2012r2 ??
    Для Win 2016 и Win 2019 проблема с Get-RDUserSession так же актуальна ?

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

      У нас Windows Server 2012 R2. Про более "модные" версии сам объективно сказать ничего не могу, так как не имею таковых под руками. Но судя по тому, что пишут на форуме (последняя ссылка в заметке), проблема наблюдается и в Windows Server 2016.

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