The Remote Desktop Procotol (RDP) is still strong and it’s not going away anytime soon, indeed there are companies like CITRIX that have built part of their success creating robust management for it.
As you can imagine, there a lot of ways to manage RDP according to the type of implementations or tools involved, so some corner cases will not be cover in this article. There are also tools like gateways that will provide a central management point for all sessions, but let’s assume that these tools are not available to you or not part of your infrastructure design.
Today, I will show you some of the available tools that we can use to manage a common issue like listing all “Disconnected RDP sessions” on your network from the CLI, every tool that has a GUI is out of scope in this case.
You can easily realise why, if you want to create a report of all the disconnected sessions on hundreds of servers I don’t think that you want to login to each server to find out what are the connections and their state (active or disconnected), right?
Let’s start with the obvious statement that scripting can’t resolve all your issues, but for sure will help you to understand it sooner than later if you’ve got one!
If you want to get rid of all disconnected sessions from some of your servers or similar issues could be mitigated by automation, but scripting alone will not help you to solve it at scale. Sometimes the best way to solve it can involve GPOs, Configuration Management, or sometimes third-party solutions.
But let’s start with gathering some information from all our network querying AD for all computer part of the TEST project :
1 |
$computerlist = Get-ADComputer -filter * | where-object {$_ -match "test"}|select-object -exp name |
Query Session / Qwinsta
Windows offers from the cmd-prompt the ability to query all sessions from the local or remote machine with this command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
C:\Users\Paolo>query session /? Display information about Remote Desktop Services sessions. QUERY SESSION [sessionname | username | sessionid] [/SERVER:servername] [/MODE] [/FLOW] [/CONNECT] [/COUNTER] [/VM] sessionname Identifies the session named sessionname. username Identifies the session with user username. sessionid Identifies the session with ID sessionid. /SERVER:servername The server to be queried (default is current). /MODE Display current line settings. /FLOW Display current flow control settings. /CONNECT Display current connect settings. /COUNTER Display current Remote Desktop Services counters information. /VM Display information about sessions within virtual machines. |
Or qwinsta (which is exactly the same).
Get-RemoteRdpSession
The script that I wrote is basically a wrapper that accepts a list of computer names returns a DataTable object that could be filtered, exported, and so on…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
#requires -runasadministrator #Paolo Frigo, https://www.scriptinglibrary.com # ##RdpSessionTable will contain all your results function New-RdpSessionTable() { $RDPSessionTable = New-Object System.Data.DataTable("RDPSessions") "COMPUTERNAME", "USERNAME", "ID", "STATE" | ForEach-Object { $Col = New-Object System.Data.DataColumn $_ $RDPSessionTable.Columns.Add($Col) } return , $RDPSessionTable } ## function Get-RemoteRdpSession { <# .SYNOPSIS This function is a simple wrapper of query session / qwinsta and returs a DataTable Objects .DESCRIPTION This function is a simple wrapper of query session / qwinsta and returs a DataTable Objects .PARAMETER ComputerName ComputerName parameter is required to specify a list of computers to query .PARAMETER State State parameter is optional and can be set to "ACTIVE" or "DISC". If not used both ACTIVE and DISC connections will be returned. .EXAMPLE Get-RemoteRdpSession -computername $(Get-AdComputer -filter * | select-object -exp name ) .EXAMPLE Get-RemoteRdpSession -computername ("server1", "server2") -state DISC .NOTES Author: Paolo Frigo, https://www.scriptinglibrary.com #> [CmdletBinding()] [OutputType([int])] Param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [string[]] $computername, [Parameter(Mandatory = $false, Position = 1 )] [ValidateSet("Active", "Disc")] [string] $state ) Begin { $tab = New-RdpSessionTable $counter = 1 $total = $computername.Length } Process { foreach ($hostname in $computername) { Write-Progress -Activity "Get-RemoteRdpSession" -Status "Querying RDP Session on $hostname" -PercentComplete (($counter / $total) * 100) if (Test-Connection -ComputerName $hostname -Quiet -Count 1){ $result = query session /server:$hostname $rows = $result -split "`n" foreach ($row in $rows) { if ($state) { $regex = $state } else { $regex = "Disc|Active" } if ($row -NotMatch "services|console" -and $row -match $regex) { $session = $($row -Replace ' {2,}', ',').split(',') $newRow = $tab.NewRow() $newRow["COMPUTERNAME"] = $hostname $newRow["USERNAME"] = $session[1] $newRow["ID"] = $session[2] $newRow["STATE"] = $session[3] $tab.Rows.Add($newRow) } } } $counter += 1 } } End { return $tab } } |
With this new function after importing it with the dot-sourcing we can get a list of all computers with an Active RDP Session :
1 2 3 4 5 6 |
PS D:\>Get-RemoteRdpSession -computername "server2","server1" -state Active COMPUTERNAME USERNAME ID STATE ------------ -------- -- ----- server2 paolo 2 Active server1 paolo 18 Active |
The main goal of having this DataTable object is that now the result can be filtered.
I haven’t found anything similar on PowerShell gallery or any other module, so I was considering doing it myself. If you would like to have it on PowerShell gallery or part of a module please write a comment and I will spend some time doing it.
How to log off all disconnected sessions
Or log off all the disconnected RDP session found:
1 2 3 4 5 6 7 |
#Get a list of all RDP disconnected session $RDPDiscSessions = Get-RemoteRdpSession -computername $computerlist -state Disc #and then disconnect each of them one by one foreach ($row in $RDPDiscSessions){ Write-Progress -Activity "Logging Off all RDP Disc Sessions" -Status "Logging OFF $($row.Item("USERNAME")) from $($row.Item("COMPUTERNAME"))" logoff $($row.Item("ID")) /server:$( $row.Item("COMPUTERNAME")) } |
The benefit of gathering this type of data is that you can soon realise that you need to create a GPO to auto-logoff after a reasonable amount of hours, for instance, 3 hours, all disconnected sessions from a group, or all servers.
As query session has qwinsta, logoff has rwinsta.
For this and other commands regarding the Remote Desktop Session, I strongly recommend having a look at the official documentation.
I hope that you find this script interesting, as always this source code is available on my GitHub repository.
Good
Excellent Article Paolo, thank you
Works great, merci!
When I execute this I get no output, it just returns back to the command prompt. I’m executing this:
.\Get-RemoteRdpSession -computername (“servername_goes_here”, “servername_goes_here”) -state DISC
Thanks for any assistance!
Hi Pat,
Are you using the dot-sourcing method to import the function first?
The function Get-RemoteRdpSession needs to defined before invoking it if you define you Get-RemoteRdpSession on your Get-RemoteRdpSession.ps1 script you need to “import it” first
. .\Get-RemoteRdpSession.ps1
Then you can re-run your command. Have a look at this article https://www.scriptinglibrary.com/languages/powershell/powershell-dot-sourcing/
If I haven’t properly understood your problem or context, sorry for that.
Regards
Thanks Paolo, that cleared it up and I was able to execute the script. My new question would be that no matter what I seem to enter in for -computername the only response I ever get back “No session exists for*”
Hi Patrick,
Get-RemoteRdpSession is a wrapper of query session / qwinsta so that message is not coming from the powershell script, but from query session command instead. I guess that you get that message because your user doesn’t have enough rights on the remote server.
If your user is a member of the local admin group on the remote server it should not have any problem to retrieve this information. To troubleshoot this you can run query session /server:SERVERNAME and I guess you’ll have the message you’ve mentioned. If you add your user to the local administrator group on the target server or run this script with a user that is already in that group… then everything should work as expected.
I hope this is going to help you to solve your problem.
Regards
Got it…thanks again!
Paolo,
Thanks for the nice code.
Is there any way to tie this information to the user’s IP?
Hi David,
Thanks for your comment. Interesting question. I don’t know off top of my head.. I doubt that information is available via
query session
. The closest information to the IP is the ClientName that you can also get it from task manager/users and adding the client name field.To get it via the CLI a way to get that info maybe is to extract the logon (6424) event from the security log.. but again it will be not easy to extract the entry where you have the IP.
If there is just one connection a simple
netstat -at | findstr 3389
will show the ip and you can useinvoke-command
against the target endpoint to query that information remotely.Thanks,
Regards
Thank you. The get-remoteRdpSession script works great but I’d like to speed up the query using start-job or as-job. We have 5+ host servers so it takes a bit to query and return all of the results. I’ve tried a few things but seem to be missing a way to piece all of the job data back together and return a complete result. Thoughts?
Thank you
Hi Aaron,
Thanks for your comment. I’m glad you have found this script useful. I understand what you are trying to do, it makes sense, but without seeing the code it not obvious where the issue could be.
I will take the opportunity to write an article this week to talk about parallel/serial processing/execution using jobs and powershell 7.
This is a quick preview, that I hope can help you.
# THIS SCRIPT TEST PARALLEL EXECUTION USING JOBS
# Paolo Frigo, https://www.scriptinglibrary.com
$ServerList = "www.google.com", "www.bing.com", "www.yahoo.com"
#CLEAR THE JOB LIST
Get-Job | Remove-Job
#START A LIST OF JOBS
$ServerList | Foreach-object {Start-job -name "$_" -scriptblock {param ($Target) Test-connection -computername $Target -count 1} -argumentlist $_}
# Note that Job States are: RUNNING, COMPLETED, FAILED
# WAITS FOR ALL JOBS TO COMPLETE UP TO THE TIMEOUT LIMIT
# PREVENTING THE SCRIPT TO RUN FOREVER
$Timeout = 60 #seconds
$Counter = 0
do{Start-sleep -seconds 1; $Counter+=1} while( (Get-Job).state -contains "Running" -and $Timeout -gt $counter)
#GET ALL THE RESULTS WITH KEEP (WITHOUT DELETING THEM)
$ServerList | Foreach-object {Receive-Job -name $_ -keep}
#CLEAR THE JOB LIST
Get-Job | Remove-Job #this step is not required if KEEP flag is removed.
You can add a link to your code repository so other users and I can have a look at your code and maybe help you.
Regards
Hi Paolo,
Excellent article and nice explanation of each functions of script. It helped me a lot. I have added write-log function as below, just posting here to help others. You can save below as another script and call it to force log of both active and disconnected sessions.
##################################################################################
function Ensure-LogFilePath([string]$LogFilePath)
{
if (!(Test-Path -Path $LogFilePath)) {New-Item $LogFilePath -ItemType directory >> $null}
}
function Write-Log([string]$message)
{
Out-File -InputObject $message -FilePath $LogFile -Append
}
Ensure-LogFilePath($ENV:LOCALAPPDATA + "\ForcedLogoutSessions")
$LogFile = $ENV:LOCALAPPDATA + "\ForcedLogoutSessions\" + "sessions_" + $([DateTime]::Now.ToString('yyyyMMdd')) + ".log"
cd "C:\Scripts"
. .\Get-RemoteRdpSession.ps1
#Get a list of all RDP disconnected session
$RDPDiscSessions = Get-RemoteRdpSession -computername "WriteServerNameHere"
#and then disconnect each of them one by one
foreach ($row in $RDPDiscSessions){
#Write-Progress -Activity "Logging Off all RDP Sessions" -Status "Logging OFF $($row.Item("USERNAME")) from $($row.Item("COMPUTERNAME"))"
Write-Log -Message "Logging OFF $($row.Item("USERNAME")) from $($row.Item("COMPUTERNAME"))"
logoff $($row.Item("ID")) /server:$( $row.Item("COMPUTERNAME"))
}
##################################################################################
Hi Arpit,
Thanks for your comment and I’m glad you have found this article useful.
I appreciate that you’ve shared your code and I think that logging is always overlooked, so great that you have added this option.
It can be very useful to have a local file with the history of all disconnected sessions without needing to search on event logs on remote machines.
Regards,
Paolo
Why not use “query user /server:ServerNameHere”. It gives you all users and tells you if they are active or not and how long they have been idle.
Hi Roux,
thanks for your comment.
Your question is a valid one. It depends on what you need. I use the “query user /server:ServerNameHere” as well, but if a need to write a script for instance I would rather prefer to use my function instead.
My wrapper function offers the output in a table format so you can manipulate or filter the objects in the output. The example I’ve shown at the end of the article can be used to log off all disconnected sessions.
There are definitely benefits in using my function, but if you just need the output of one server and you don’t need to take any action or filter the output I agree that just “query session” can be enough for your use case.
Regards.
Paolo
Very interesting Post. Exactly what I look for to close disconnected session.
My difficulty is the servers are in fr-fr Get-UICulture
LCID Name DisplayName
—- —- ———–
1036 fr-FR Français (France)
And the value “Disc” = “D,co” because “é” is not recognize
SESSION UTILISATEUR ID TAT TYPE PRIPHRIQUE
services 0 D‚co
before I install a new “admin server” machine in US is there any tips or way to bypass this inconvenient by changing the culture during script execution ?
Kind regards
Hi Fred,
Thanks for your comment. I’m glad you’ve found this article useful, to answer your question there are probably different ways to look at the problem.
What I would suggest if you’re happy with your configuration is changing the script. If your output is different you can simply edit the $regex value that will make sure your results are returned as expected.
You can also use another regex to replace the text output with a more friendly version of it.
I hope that answers your question.
Regards
What would be the best way to add a column and insert Get-Date?
Hi Zack,
Thanks for your comment. There should be only a couple of lines to modify if I understood what you need.
Under the New-RdpSessionTable function where there is this list COMPUTERNAME”, “USERNAME”, “ID”, “STATE” can add
, "DATE"
and in the Get-RemoteRdpSession function after this line$newRow["STATE"] = $session[3]
add this one:
$newRow["DATE"] = Get-Date -format "yyyy-MM-dd"
.I hope that answers your question. Regards
Just wanted to say thanks for this!
In case anyone is curious, one can add an OU filter:
Get-RemoteRdpSession -computername $(Get-AdComputer -filter * -SearchBase "OU=Servers,DC=mydc,DC=somedomain" | select-object -exp name )
Hi WG,
I’m glad you’re finding it useful, thanks for your comment and for adding this oneliner. Regards