There’s this module called ‘PSTerminalServices’ that has a method to collect active sessions on all RDS nodes in the domain. I find that such method is lacking one important metric: session idle time. Therefore, I create a function to gather such information. If a tool doesn’t exist, we make it.

function includePsTerminalServices{
    $ErrorActionPreference='stop'
    $psTerminalServicesZip='https://github.com/imseandavis/PSTerminalServices/archive/master.zip'

    $null=Import-Module PSTerminalServices -ea SilentlyContinue
    if(!(Get-Module -Name PSTerminalServices -ea SilentlyContinue)){
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        try{
            #if(!('NuGet' -in (get-packageprovider).Name)){
            #    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
            #    Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
            #    }
            # Download the module from github            
            $tempDir='c:\temp'
            $extractFolderName='psTerminalServices'
            $zipDownload=$tempDir+"\$extractFolderName.zip"
            if(!(test-path $tempDir)){mkdir $tempDir -Force}
            Import-Module BitsTransfer    
            try{
                Start-BitsTransfer -Source $psTerminalServicesZip -Destination $zipDownload
            }catch{
                Write-Error $_
                write-warning "Cannot continue without PSTerminalServices Module.`r`nPlease install that manually."
                }
            # unzip
            $unzipFolder="$tempDir\$extractFolderName"
            Expand-Archive -LiteralPath $zipDownload -DestinationPath $unzipFolder
            $msiFile=(gci $unzipFolder -Recurse|?{$_.FullName -match 'msi$'}).FullName
            # install msi silently
            $timeStamp = get-date -Format yyyyMMddTHHmmss
            $logFile = '{0}-{1}.log' -f "$tempDir\$extractFolderName",$timeStamp
            $MSIArguments = @(
                "/i"
                ('"{0}"' -f $msiFile)
                "/qn"
                "/norestart"
                "/L*v"
                $logFile
            )
            Start-Process "msiexec.exe" -ArgumentList $MSIArguments -Wait -NoNewWindow
            Import-Module PSTerminalServices
            return $true
        }catch{
            Write-Error $_
            return $false
            }
    }
}

function getSessionsInfo([string[]]$computername=$env:computername){
    $results=@()
    function getDisconnectedSessionInfo($line,$computer){
             # convert multiple spaces into single space and split pieces into array
            $thisLine = $line.Trim() -Replace '\s+',' ' -Split '\s'
            $properties = @{
                UserName = $thisLine[0]
                ComputerName = $computer
                }
            $properties.SessionName = $null
            $properties.Id = $thisLine[1]
            $properties.State = $thisLine[2]                        
            $properties.IdleMinutes=.{[string]$x=$thisLine[3]
                            switch -regex ($x){
                                '\.' {0;break}                   
                                '\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                        $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                        $dayMinutes+$hourMinutes
                                        break;
                                     }               
                                '\:' {try{
                                        ([TimeSpan]::Parse($x)).totalMinutes
                                        }catch{
                                            "Invalid value: $x"
                                            }
                                        break
                                        }
                                default {$x}
                            }                                  
                        }
            $properties.LogonTime = $thisLine[4..6] -join ' '
            $result=New-Object -TypeName PSCustomObject -Property $properties
            return $result
            }
        
    function getConnectedSessionInfo($line,$computer){
            $thisLine = $line.Trim() -Replace '\s+',' ' -Split '\s'
            $properties = @{
                UserName = $thisLine[0]
                ComputerName = $computer
                }
            $properties.SessionName = $thisLine[1]
            $properties.Id = $thisLine[2]
            $properties.State = $thisLine[3]
            $properties.IdleMinutes=.{$x=$thisLine[4]
                            switch -regex ($x){
                                '\.' {0;break}                   
                                '\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                        $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                        $dayMinutes+$hourMinutes
                                        break;
                                     }               
                                '\:' {try{
                                        ([TimeSpan]::Parse($x)).totalMinutes
                                        }catch{
                                            "Invalid value: $x"
                                            }
                                        break
                                        }
                                default {$x}
                            }                                 
                        }
            $properties.LogonTime = $thisLine[5..($thisLine.GetUpperBound(0))] -join ' '
            $result=New-Object -TypeName PSCustomObject -Property $properties
            return $result
        }

    foreach ($computer in $computername){
        try {
            # Perusing legacy commandlets as there are no PowerShell equivalents at this time
            $sessions=quser /server:$computer 2>&1 | Select-Object -Skip 1
            ForEach ($session in $sessions) {               
                $result=$null
                $thatLine = $session.Trim() -Replace '\s+',' ' -Split '\s'
                if ($thatLine[2] -eq 'Disc'){
                    $result=getDisconnectedSessionInfo $thatLine $computer                }elseif(!$onlyDisconnected){
                    $result=getConnectedSessionInfo $thatLine $computer
                }
                if($result){$results+=$result}
            }
        }catch{
            $results+=New-Object -TypeName PSCustomObject -Property @{
                ComputerName=$computer
                Error=$_.Exception.Message
            } | Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleMinutes,LogonTime,Error|sort -Property UserName
        }
    }
    return $results
}

if(!(includePsTerminalServices)){
    Write-Warning "Cannot continue without module PsTerminalServices"
    break;
    }

$allRdsNodes=(Get-TSServers).ServerName
#$allSessions=$allRdsNodes|%{Get-TSSession -computername $_} # This does not provide idle duration; hence, I've written a custom function for that
$allSessions=getSessionsInfo $allRdsNodes
$duplicateSessions=.{$uniques=$allSessions.UserName | select –unique
                    $duplicates=(Compare-object –referenceobject $uniques –differenceobject $allSessions.UserName).InputObject
                    $allSessions|?{$_.Username -in $duplicates}
                    }

# Advisory: although it's recommended to logoff duplicate & stale sessions, test these lines prior to execution
foreach($session in $duplicateSessions){
    if($session.IdleMinutes -ge $idleMinutesThreshold){
        Stop-TSSession -ComputerName $session.ComputerName -Id $session.Id -Force
        }
    }