# manageRdsSessions.ps1
# Sequence of workflow:
# a. Gather active and disconnected sessions on all Remote Desktop Servers detected on the domain
# b. Determine whether there are duplicate sessions. That is defined as sessions with the same userId on 2+ RDS nodes
# c. Send out probes (PowerShell jobs) to follow certain executables identified by ProcessId, ComputerName, and SessionId
# d. Automatically logoff idle sessions that are duplicates with certain executables with inactive statuses
#
# Require: module PsTerminalServices with these nice methods
# Disconnect-TSSession - Disconnects any connected user from the session.
# Get-TSCurrentSession - Provides information about the session in which the current process is running.
# Get-TSProcess - Gets a list of processes running in a specific session or in all sessions.
# Get-TSServers - Enumerates all the terminal servers in a given domain.
# Get-TSSession - Lists the sessions on a given terminal server.
# Send-TSMessage - Displays a message box in the specified session Id.
# Stop-TSProcess - Terminates the process running in a specific session or in all sessions.
# Stop-TSSession - Logs the session off, disconnecting any user that might be connected.
#
# Usage:
# There are two options: tell program to logoff duplicate sessions if certain program is no longer consuming CPU, or just disconnect duplicate sessions without checking any programs
# a. manageRdsSessions -sessionIdleMinutesThreshold 60
# b. manageRdsSessions -sessionIdleMinutesThreshold 60 -exeName word.exe -minutesToDetermineProgramInactive 5
# User defined variables
$sessionIdleMinutesThreshold=60
$exeName='dynamics.exe'
$minutesToDetermineProgramInactive=5
function manageRdsSessions{
param(
[int]$sessionIdleMinutesThreshold=15,
[string]$exeName='chrome.exe',
[int]$minutesToDetermineProgramInactive=5
)
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
}
}else{return $true}
}
function checkInactiveProcess{
param(
[parameter(position=0)][string]$targetMachine=$env:computername,
[parameter(position=1)][int]$processId,
[parameter(position=2)][int]$minutesToDetermineInactive=5,
[parameter(position=3)][int]$maxMinutesToDetermineInactive=30
)
$checkCPU="(get-process -computerName $targetMachine -Id $processId -EA SilentlyContinue).CPU"
write-host "Checking CPU Usage of process ID $processId..."
$previousCpuConsumption=invoke-expression $checkCPU -ea SilentlyContinue
$inactivityMarkerReached=$false
$inactiveTimer=[System.Diagnostics.Stopwatch]::StartNew()
Do {
$currentCpuConsumption=invoke-expression $checkCPU -ea SilentlyContinue
write-host "Minute $([math]::round($inactiveTimer.Elapsed.totalminutes,2))`t: CPU cycles consumed = $currentCpuConsumption"
$cpuConsumptionChanged=$currentCpuConsumption -gt $previousCpuConsumption
if(!$currentCpuConsumption){
write-host "Process ID $processId is currently NOT running." -ForegroundColor Green
return $true
}elseif($cpuConsumptionChanged){
if($totalMinutes -ge $maxMinutesToDetermineInactive){return $true}
write-host "Activites are being detected. Countdown is restarting from $minutesToDetermineInactive" -ForegroundColor Yellow
$totalMinutes+=$inactiveTimer.Elapsed.totalminutes
$inactiveTimer.restart()
}elseif($inactiveTimer.Elapsed.totalminutes -ge $minutesToDetermineInactive){
write-host "$minutesToDetermineInactive minutes have passed without activities." -ForegroundColor Green
return $true
}
sleep -Seconds 6
$previousCpuConsumption=$currentCpuConsumption
} while (!$inactivityMarkerReached)
}
function getSessionsInfo([string[]]$computername=$env:computername){
$results=@()
function getDisconnectedSessionInfo($thisLine,$computer){
# convert multiple spaces into single space and split pieces into array
$sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
$properties = @{
UserName = $sessionArray[0]
ComputerName = $computer
}
$properties.SessionName = $null
$properties.SessionId = $sessionArray[1]
$properties.State = $sessionArray[2]
$properties.IdleMinutes=.{
[string]$x=$sessionArray[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 = $sessionArray[4..6] -join ' '
$result=New-Object -TypeName PSCustomObject -Property $properties
return $result
}
function getActiveSessionInfo($thisLine,$computer){
$sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
$properties = @{
UserName = $sessionArray[0]
ComputerName = $computer
}
$properties.SessionName = $sessionArray[1]
$properties.SessionId = $sessionArray[2]
$properties.State = $sessionArray[3]
$properties.IdleMinutes=.{
$x=$sessionArray[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;
}
'\:' { $timeFragments=[regex]::match($x,'(\d+)\:(\d+)')
[Int16]$hours=$timeFragments.Groups[1].Value
[Int16]$minutes=$timeFragments.Groups[2].Value
$minutes + ($hours * 60)
break
}
default {$x}
}
}
$properties.LogonTime = $sessionArray[5..($sessionArray.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 ($line in $sessions) {
$fragments = $line.Trim() -Replace '\s+',' ' -Split '\s'
$disconnectedSession=$fragments[2] -eq 'Disc' -or $fragments[3] -eq 'Disc'
if ($disconnectedSession){
$result=getDisconnectedSessionInfo $line $computer
}else{
$result=getActiveSessionInfo $line $computer
}
if($result){$results+=$result}
}
}catch{
$results+=New-Object -TypeName PSCustomObject -Property @{
ComputerName=$computer
Error=$_.Exception.Message
} | Select-Object -Property UserName,ComputerName,SessionName,SessionId,State,IdleMinutes,LogonTime,Error|sort -Property UserName
}
}
return $results
}
if(!(includePsTerminalServices)){
Write-Warning "Cannot continue without module PsTerminalServices"
return $false;
}
$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}
}
$targetSessions=$duplicateSessions|?{[int]$_.IdleMinutes -ge $sessionIdleMinutesThreshold}
if(!$exeName){
foreach($session in $targetSessions){
Stop-TSSession -ComputerName $session.ComputerName -Id $session.SessionId -Force
}
}else{
$targetNodes=$targetSessions.ComputerName|select -Unique
$targetUsers=$targetSessions.UserName|select -Unique
if(!$targetUsers){
write-host "These are NO duplicate sessions over idling minutes threshold of $sessionIdleMinutesThreshold" -ForegroundColor Green
return $true
}else{
write-host "User(s) $($targetUsers -join ', ') is/are having duplicate sessions over idling minutes threshold of $sessionIdleMinutesThreshold minutes" -ForegroundColor Yellow
}
$runningProcesses=Get-WmiObject -ComputerName $targetNodes -Class Win32_Process -Filter "Name = '$exeName'"
$targetProcesses=$runningProcesses|?{$_.GetOwner().User -in $targetUsers}
if($targetProcesses){
get-job|stop-job|remove-job
foreach ($process in $targetProcesses){
$processId=$process.ProcessId
$computer=$process.PSComputerName
$sessionId=$process.SessionId
return invoke-command -AsJob -ComputerName $computer -scriptblock {
param($checkInactiveProcess,$processId,$minutes,$sessionId)
[bool]$processIsInactive=[ScriptBlock]::Create($checkInactiveProcess).Invoke($env:computername,$processId,$minutes)|select -First 1
[PSCustomObject][ordered]@{
processId=$processId
sessionId=$sessionId
processIsInactive=$processIsInactive
}
} -Args ${function:checkInactiveProcess},$processId,$minutesToDetermineProgramInactive,$sessionId
}
$results=@()
$allJobsCompleted=$false
do{
$completedJobs=get-job|?{$_.State -eq 'Completed'}
if($completedJobs){
foreach ($job in $completedJobs){
$jobResult=Receive-Job -id $job.id
write-host "$jobResult"
$results+=$jobResult
remove-job -id $job.id
}
}else{
write-host '.' -NoNewline
sleep 6
}
$allJobsCompleted=(get-job).count -eq 0
}until($allJobsCompleted)
if($results){
$results|ForEach-Object{
if($_.ProcessIsInactive){
try{
Stop-TSProcess -ComputerName $($_.PSComputerName) -Id $($_.processId) -Force -ea -verbose Stop
Stop-TSSession -ComputerName $($_.PSComputerName) -Id $($_.sessionId) -Force -verbose
}catch{
write-warning $_
Continue
}
}else{
write-host "ComputerName $($_.PSComputerName) ProcessId $($_.processId) is currently Active! Thus, session $($_.sessionId)) may not be stopped." -ForegroundColor Yellow
}
}
}
}else{
write-host "User(s) $($targetUsers -join ', ') do(es) not have $exeName running in duplicate sessions. Thus, the program takes no actions at this time.`r`n$(($targetSessions|out-string).trim())" -ForegroundColor Yellow
}
}
}
manageRdsSessions $sessionIdleMinutesThreshold $exeName $minutesToDetermineProgramInactive
August 20, 2020August 20, 2020
0 Comments