Finding RDP sessions on servers using PowerShell

PowerShell Have you ever needed to use RDP to get to a server console for some local admin work and then been bounced out because there are already active sessions? Or have you had your Active Directory account locked out because of an open RDP session with the old password sending locks to the domain?  Wouldn’t it be so useful to be able to query sessions with a tool like PowerShell?  The truth is PowerShell needs a little help to do this, from a tool called QWINSTA which we are about to take a closer look at.

Rather than having to run the Remote Desktop Manager and search between servers, I decided to build a little PowerShell and QWINSTA script which will scan Active Directory for computers running server OS and list out the active and disconnected sessions.

The Magic of QWINSTA

One of the challenges with this activity is that PowerShell does not have a native CmdLet to extract RDP information from servers. Luckily, we can use a hybrid approach here to solve that problem.

WIndows ships with two tools named QWINSTA.exe and RWINSTA.exe for querying and resetting Remote Desktop Services sessions. For our purposes we will use QWINSTA (apparently stands for Query WINdows STAtion).

You can find the options in the command line by typing QWINSTA /? as shown here:

QWINSTA PowerShell help

The Process to Get RDP Sessions With PowerShell and QWINSTA

There is a simple flow to the script which is:

  1. Query Active Directory for Servers
  2. Run QWINSTA to extract the session information
  3. If a session exists, read the username and session type
  4. Log the username and session type to a variable
  5. Email the results

There are some setup options for email parameters that are done and most importantly the Import-Module CmdLet is used to load the Active Directory module so that we can use Get-ADComputer. This will require that you have the AD PowerShell environment on the workstation or server that you are running this process from.

I’ve added some sections in the script which are commented out but you can uncomment them to have them output to the console so that you can see what is happening while the script is running. This is handy for troubleshooting in the case that there are issues.

Adding the QWINSTA Command

Because we have to use an external command in our script we will simply use the command inline with other script actions. The interesting thing about this is that we can run the command easily and pass the computer name parameter which we have assigned to $ServerName:

qwinsta /server:$ServerName

To be able to leverage the PowerShell goodness we have to assign the output to a variable so that we can reuse it as needed:

$queryResults = qwinsta /server:$ServerName

Luckily PowerShell makes it just that easy to capture the output of external commands as well as native CmdLets.

Managing the QWINSTA Output of Querying RDP Sessions

When you look at the script you can see that the QWINSTA command line is not quite as simple as I’ve shown above. This is because the output of the QWINSTA command isn’t in a neat and tidy one line format like we really would prefer:

query session rdp QWINSTA powershell output

This is why we have a much more complex command line in the script:

$queryResults = (qwinsta /server:$ServerName | foreach { (($_.trim() -replace “s+”,”,”))} | ConvertFrom-Csv)

We are taking each line of the output from the command, trimming the content and replacing the spaces with commas and then piping it to a ConvertFrom-CSV to put the content into a table array with the appropriate headers of USERNAME and SESSIONNAME which is the information we are looking for.

Logging and Emailing the Results of PowerShell and QWINSTA Querying RDP Sessions

The logging of the output is quite simple. We just want to collect each line as we loop through the sessions and the add the content into a variable ($SessionList).

Once we have completed querying each of the computers, the loop exits and then we put together our email message using the Send-MailMessage CmdLet:

Send-MailMessage -To $emailTo -Subject $subject -Body $SessionList -SmtpServer $smtpServer -From $emailFrom -Priority $priority

The parameters for the Send-MailMessage command come from those defined in the static variables at the beginning of the script and with the $SessionList which was created during the script run.

The Script – Query RDP Session: PowerShell and QWINSTA

Once we’ve put it all together it is a nifty and fairly lightweight script. I’ve put the script up on my TechNet Gallery and you can find it here: http://gallery.technet.microsoft.com/PowerShell-script-to-Find-d2ba4252

Here is the script in it’s full form to read through. I hope that you are able to make use of it. An ideal situation is to have it run as a batch process every night to scan the environment and email your Systems Admin team so that they know what RDP sessions are left overnight.

 

# Import the Active Directory module for the Get-ADComputer CmdLet

Import-Module ActiveDirectory

# Get today’s date for the report

$today = Get-Date

# Setup email parameters

$subject = “ACTIVE SERVER SESSIONS REPORT – ” + $today

$priority = “Normal”

$smtpServer = “YourMailServer”

$emailFrom = “email@yourdomain.com

$emailTo = “email@yourdomain.com

# Create a fresh variable to collect the results. You can use this to output as desired

$SessionList = “ACTIVE SERVER SESSIONS REPORT – ” + $today + “`n`n”

# Query Active Directory for computers running a Server operating system

$Servers = Get-ADComputer -Filter {OperatingSystem -like “*server*”}

# Loop through the list to query each server for login sessions

ForEach ($Server in $Servers) {

$ServerName = $Server.Name

# When running interactively, uncomment the Write-Host line below to show which server is being queried

# Write-Host “Querying $ServerName”

# Run the qwinsta.exe and parse the output

$queryResults = (qwinsta /server:$ServerName | foreach { (($_.trim() -replace “s+”,”,”))} | ConvertFrom-Csv)

# Pull the session information from each instance

ForEach ($queryResult in $queryResults) {

$RDPUser = $queryResult.USERNAME

$sessionType = $queryResult.SESSIONNAME

# We only want to display where a “person” is logged in. Otherwise unused sessions show up as USERNAME as a number

If (($RDPUser -match “[a-z]”) -and ($RDPUser -ne $NULL)) {

# When running interactively, uncomment the Write-Host line below to show the output to screen

# Write-Host $ServerName logged in by $RDPUser on $sessionType

$SessionList = $SessionList + “`n`n” + $ServerName + ” logged in by ” + $RDPUser + ” on ” + $sessionType   }

}

}

# Send the report email

Send-MailMessage -To $emailTo -Subject $subject -Body $SessionList -SmtpServer $smtpServer -From $emailFrom -Priority $priority

# When running interactively, uncomment the Write-Host line below to see the full list on screen

# $SessionList

 

 

17 thoughts on “Finding RDP sessions on servers using PowerShell”

  1. The parse line doesn’t work correctly for some servers. It returns 6 field headers for the CSV, then only adds the populated fields for the sessions, so the output ends up showing login sessions that don’t exist.

    SERVER4 logged in by Disc on 0
    SERVER4 logged in by Down on 1

    Running the qwinsta /server:SERVER4 command manually produces this:

    SESSIONNAME USERNAME ID STATE TYPE DEVICE
    0 Disc rdpwd
    rdp-tcp 65536 Listen rdpwd
    console 2 Conn wdcon
    1 Down

    Running the qwinsta command manually without the convertfrom-csv returns this:

    qwinsta /server:SERVER4 | foreach { (($_.trim() -replace “s+”,”,”))
    SESSIONNAME,USERNAME,ID,STATE,TYPE,DEVICE
    0,Disc,rdpwd
    rdp-tcp,65536,Listen,rdpwd
    console,2,Conn,wdcon
    1,Down

    So the csv query for the first line returns “0” as the sessionname, and “Disc” as the username. That produces erroneous output.

    I think that you would need to parse this as an array rather than a csv. I have been looking at it, but I was distracted by other duties.

    Reply
    • Hi Clay,

      Thanks for the note on this. I wasn’t as clear on the output for disconnected sessions. For my environment I actually have hung disconnected sessions sometimes so I didn’t do any additional management for that result.

      To get around this we can adjust the If statement which generates the results to return to the console/email:

      Currrent:

      If (($RDPUser -match “[a-z]“) -and ($RDPUser -ne $NULL)) {

      Remove results if the Username is Disc:

      If ($RDPUser -match “[a-z]“ -And $RDPUser -notmatch “Disc” -and $RDPUser -ne $NULL) {

      Now that I look at it, I had some unnecessary brackets in the statement 🙂

      Hopefully I’ll have some time to update the post to reflect that change. Thanks for the input!

      Eric

      Reply
  2. Here is what I did based on your suggestion.

    # Pull the session information from each instance
    ForEach ($queryResult in $queryResults) {
    $RDPUser = $queryResult.USERNAME
    $sessionType = $queryResult.SESSIONNAME
    If ($sessionType -ne $NULL ) {$ICA = $sessionType.startswith(“ica”)}

    # We only want to display where a “person” is logged in. Otherwise unused sessions show up as USERNAME as a number
    If ($RDPUser -match “[a-z]“ -And $RDPUser -notmatch “Disc” -And $RDPUser -notmatch “Down” -and $RDPUser -ne $NULL -and $ICA -match “False”) {
    # When running interactively, uncomment the Write-Host line below to show the output to screen
    Write-Host $ServerName logged in by $RDPUser on $sessionType
    $SessionList = $SessionList + “`n`n” + $ServerName + ” logged in by ” + $RDPUser + ” on ” + $sessionType
    }
    }
    }

    I also excluded any ICA sessions, as we have a bunch of citrix servers, and we just want to audit the admins who leave RDP sessions open.

    Reply
  3. Eric – Great Script. is there anyway for it to show me inactive or inactive/disconnected sessions. I believe it was built only to show active sessions?

    Thanks!

    Reply
    • Hi Brent,

      It does pull in disconnected sessions which display in the output as a user “disc”. If it isn’t, let me know what OS you are querying and I can give it a whirl. It seems to be working for 2008/2008 R2 instances for me.

      Thanks,

      Eric

      Reply
  4. We are trying to use QWINSTA to verify that our batch systems user-ids are logged in via the console. However, I am seeing a variety of output, in that some with session 0 show as “console” and others don’t. Which conclusively indicates that it is a console user – the “console” designation or being session “0”?

    -Thanks-

    Reply
    • Hi Tom,

      The QWINSTA output always shows the console user as SESSIONNAME=console. There can be an ID of 0 which could be another user like a service or something. It’s a little confusing sometimes.

      I think I need to do some different use cases and might have to tweak the script a little.

      Thanks,

      Eric

      Reply
    • Hi Travis,

      You sure can! You can change the line that populates the $servers variable to import a file from CSV.

      To remove the AD portion, comment out (add a # to the beginning of the line) the Import-Module Active Directory line and also replace the following line:
      $Servers = Get-ADComputer -Filter {OperatingSystem -like “*server*”}
      with something like this:
      $Servers = Import-CSV “YourImportFileHere.CSV”
      which will query the file for the input. Your file should have a header row which says Name, and the list of servers will be on each line below it. When the script assigns the $Server.Name as it loops through the array, it will use your CSV file. Make sure to use the full path to the CSV, or use no path and ensure the file is in the same folder as the script.

      As usual, test, test and re-test but this is a non-intrusive script, so even if the input doesn’t work on the first try, it is easy to experiment.

      Hope that helps!

      Eric

      Reply
  5. Great script!
    I changed two things, however:
    1) Output of qwinsta is language dependent and thus the properties created by ConvertFrom-Csv may get different names. I resorted to replacing strings but that doesn’t resolve the problem of not beeing able to force a given language in Windows / Powershell entirely, it’s just a mitigation for a known language.
    2) Output of qwinsta may have empty fields and thus break parsing. I parsed it using fixed number of characters
    ( -replace “^(………………)(……………..)(…………)(……..)(……..)(.*)$”,’$1,$2,$3,$4,$5,$6′ … )

    Otherwise great, thanks for posting it.

    Markus

    Reply
  6. If you’re looking for user details I would recommend trying the QUSER command which not only provides similar information but also whether the user is actively using the machine and how long they’ve been logged-on.

    Jim

    Reply
  7. Looks like an escape is missing in the -replace section. I had to change
    -replace “s+”,”,”
    to
    -replace “\s+”,”,”
    and it started working.

    Reply
  8. Hey Eric,
    Old post, but it was 90% of what I needed, THANKS!!! I fixed what might have been an error introduced in Powershell with the escape before the s+ in the replace command (see previous post), plus I sorted the list of computers before processing each one, checked if they are online before processing, and included output lines for that, or if nobody was found to be logged on. Personal preference, I also removed the double line spacing in the output. We needed these details because we now have a mix of on-site and WFH users who may either be at their desk or they RDP in (via RAS). Sometimes user change workstations, occasionally they need to know which ones are ‘available’ for remote access. Seeing as I got 90% done with your script, I thought it only fair to give back and include the additions I mentioned above…

    # Import the Active Directory module for the Get-ADComputer CmdLet
    Import-Module ActiveDirectory
    # Get today’s date for the report
    $today = Get-Date
    # Setup email parameters
    $subject = “Active login sessions”
    $priority = “Normal”
    $smtpServer = “smtp.yourdomain.com”
    $emailFrom = “from@yourdomain.com”
    $emailTo = “to@yourdomain.com”
    # Create a fresh variable to collect the results. You can use this to output as desired
    $SessionList = “Report for workstation login sessions as at ” + $today + “`n`n”
    # Query Active Directory for computers running a Server operating system
    $Servers = (Get-ADComputer -Filter {enabled -eq “true”} -SearchBase ‘OU=”Your OU”,DC=yourdomain,DC=local’) | Sort-Object
    # Loop through the list to query each server for login sessions
    ForEach ($Server in $Servers)
    {
    $ServerName = $Server.Name
    if ((Test-NetConnection $ServerName -WarningAction SilentlyContinue).PingSucceeded -eq $true)
    {
    # Assume nobody is logged on
    $UserCount = 0
    # When running interactively, uncomment the Write-Host line below to show which server is being queried
    # Write-Host “Querying $ServerName”
    # Run the qwinsta.exe and parse the output
    $queryResults = (qwinsta /server:$ServerName | foreach { (($_.trim() -replace “\s+”,”,”))} | ConvertFrom-Csv)
    # Pull the session information from each instance
    ForEach ($queryResult in $queryResults)
    {
    $RDPUser = $queryResult.USERNAME
    $sessionType = $queryResult.SESSIONNAME
    # We only want to display where a “person” is logged in. Otherwise unused sessions show up as USERNAME as a number
    If (($RDPUser -match “[a-z]”) -and ($RDPUser -ne $NULL))
    {
    # Somebody is logged on, increase user count
    $UserCount = $UserCount +1
    # When running interactively, uncomment the Write-Host line below to show the output to screen
    Write-Host $ServerName logged in by $RDPUser on $sessionType
    $SessionList = $SessionList + “`n” + $ServerName + ” logged in by ” + $RDPUser + ” on ” + $sessionType
    }
    }
    # If nobody has been found logged on, include a line to say so
    if ($UserCount -eq 0)
    {
    Write-Host “$ServerName is not logged on.”
    $SessionList = $SessionList + “`n” + $ServerName + ” is not logged on.”
    }
    }
    else
    {
    Write-Host “$ServerName is offline.”
    $SessionList = $SessionList + “`n” + $ServerName + ” is offline.”
    }
    }
    # Send the report email
    Send-MailMessage -To $emailTo -Subject $subject -Body $SessionList -SmtpServer $smtpServer -From $emailFrom -Priority $priority
    # When running interactively, uncomment the Write-Host line below to see the full list on screen
    # $SessionList

    Reply
  9. I appreciate the start – I ended up wrapping another loop around this to grab multiple servers, and filter out the header line from the command response. Works great! Thanks

    $ServerNames = @(“server1”, “server2”, “server3”, “etc”, “etc”, “…”)

    ForEach ($server IN $ServerNames){
    $users = quser /server:$server
    $Parsed_user += ($users | ForEach-Object { $server +”,” + (($_.trim() -replace “\s{2,}”,”,”))} | ConvertFrom-Csv -Header “Server”,”UserName”,”SessionName”,”ID”,”State”,”IdleTime”,”LogonTime”) | ? {$_.UserName -ne “USERNAME”}
    #Get UserName
    #Write-Host $Parsed_user
    }

    $Parsed_user | Sort -Property Server, SessionName, UserName | Format-Table

    Reply
  10. Using only data from “Quser” the following code was enough.

    Quser | foreach {$_ -replace ” +”,”;” -replace “^.”,””} | ConvertFrom-Csv -Delimiter “;”

    Reply

Leave a Reply to Eric Cancel reply

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