How to Query and Log Off Remote Desktop Sessions with Powershell

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 :

Query Session / Qwinsta

Windows offers from the cmd-prompt the ability to query all sessions from the local or remote machine with this command:

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…

With this new function after importing it with the dot-sourcing we can get a list of all computers with an Active RDP Session :

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:

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.

22 Replies to “How to Query and Log Off Remote Desktop Sessions with Powershell”

  1. 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!

    1. 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

      1. 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*”

        1. 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

    1. 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 use invoke-command against the target endpoint to query that information remotely.

      Thanks,
      Regards

  2. 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

    1. 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

  3. 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"))
    }

    ##################################################################################

    1. 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

  4. 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.

    1. 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

  5. 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 PRIPHRIQUE
    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

    1. 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

    1. 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

  6. 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 )

    1. Hi WG,
      I’m glad you’re finding it useful, thanks for your comment and for adding this oneliner. Regards

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.