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
}
}
Categories: