Query Terminal Server Sessions

Query Terminal Server Sessions

2018, Apr 27    

I wanted a quick way to get a list of user sessions on our VDI servers. My exact goal was to get a list of users that were a day away from being forced off the system and sending them an email.

TLDR: I ended up making this into a module: QuserObject.

Walkthrough

Anyway, down to business. We can use Quser to get the information. Keep in mind that you can easily target a different computer: quser /SERVER:servername.

USERNAME         SESSIONNAME  ID  STATE   IDLE TIME  LOGON TIME
 admin.xxxxxxxx                2  Disc     15+15:12  7/20/2017 1:19 PM
 admin.xxxxx      rdp-tcp#54   3  Active       1:39  7/21/2017 5:35 AM
 xxxxxxxx                      4  Disc      6+04:10  7/21/2017 9:25 AM
>vertigoray          console   5  Active          .  8/9/2017 4:40 PM

This is good information, but it would be so much better if it was an object. Since it’s already a nice grid, let’s treat it as a CSV:

PS > (quser) -replace '\s{2,}', ',' | ConvertFrom-Csv

USERNAME    : admin.xxxxxxxx
SESSIONNAME : 2
ID          : Disc
STATE       : 15+15:12
IDLE TIME   : 7/20/2017 1:19 PM
LOGON TIME  :

USERNAME    : admin.xxxxx
SESSIONNAME : rdp-tcp#54
ID          : 3
STATE       : Active
IDLE TIME   : 1:39
LOGON TIME  : 7/21/2017 5:35 AM

USERNAME    : xxxxxxxx
SESSIONNAME : 4
ID          : Disc
STATE       : 6+04:10
IDLE TIME   : 7/21/2017 9:25 AM
LOGON TIME  :

USERNAME    : >vertigoray
SESSIONNAME : console
ID          : 5
STATE       : Active
IDLE TIME   : .
LOGON TIME  : 8/9/2017 4:40 PM

Beautifully simple, right?! I originally posted this on stackoverflow because I just wanted a list of users.

Now, let’s get rid of that > on my entry, and fix those disconnected session columns. This part gets a little more complicated.

(((quser) -replace '^>', '') -replace '\s{2,}', ',').Trim() | ForEach-Object {
    if ($_.Split(',').Count -eq 5) {
        Write-Output ($_ -replace '(^[^,]+)', '$1,')
    } else {
        Write-Output $_
    }
} | ConvertFrom-Csv

Output:

USERNAME    : admin.xxxxxxxx
SESSIONNAME :
ID          : 2
STATE       : Disc
IDLE TIME   : 15+15:12
LOGON TIME  : 7/20/2017 1:19 PM

USERNAME    : admin.xxxxx
SESSIONNAME : rdp-tcp#54
ID          : 3
STATE       : Active
IDLE TIME   : 1:39
LOGON TIME  : 7/21/2017 5:35 AM

USERNAME    : xxxxxxxx
SESSIONNAME :
ID          : 4
STATE       : Disc
IDLE TIME   : 6+04:10
LOGON TIME  : 7/21/2017 9:25 AM

USERNAME    : vertigoray
SESSIONNAME : console
ID          : 5
STATE       : Active
IDLE TIME   : .
LOGON TIME  : 8/9/2017 4:40 PM

Last things to do:

  • Change the idle . to a $null.
  • Type cast everything appropriately.
  • Change the property names to PascalCase.

Here’s the code for that:

$script:headerRowProcessed = $false
(((quser) -replace '^>', '') -replace '\s{2,}', ',').Trim() | ForEach-Object {
    if ($_.Split(',').Count -eq 5) {
        Write-Output ($_ -replace '(^[^,]+)', '$1,')
    } else {
        Write-Output $_
    }
} | ForEach-Object {
    $rowParts = $_.Split(',')
    if (-not $script:headerRowProcessed) {
        [System.Collections.ArrayList] $parts = @()
        foreach ($part in $rowParts) {
            $parts.Add((Get-Culture).TextInfo.ToTitleCase($part.ToLower()).Replace(' ', '')) | Out-Null
        }
        Write-Output ($parts -join ',')
        $script:headerRowProcessed = $true
    } else {
        [int] $rowParts[2] = $rowParts[2]
        
        if ($rowParts[3] -eq 'Disc') {
            $rowParts[3] = 'Disconnected'
        }
        
        if ($rowParts[4] -eq '.') {
            $rowParts[4] = $null
        } else {
            $parts = $rowParts[4].Split('+:')
            $now = Get-Date
            if ($parts.Count -eq 3) {
                $rowParts[4] = $now.AddDays(-1 * $parts[0]).AddMinutes(-1 * $parts[1]).AddSeconds(-1 * $parts[2])
            } else {
                $rowParts[4] = $now.AddMinutes(-1 * $parts[0]).AddSeconds(-1 * $parts[1])
            }
        }
        
        $rowParts[5] = Get-Date $rowParts[5]

        Write-Output ($rowParts -join ',')
    }
} | ConvertFrom-Csv

Output:

Username    : admin.xxxxxxxx
Sessionname :
Id          : 2
State       : Disconnected
IdleTime    : 07/25/2017 16:24:48
LogonTime   : 07/20/2017 13:19:00

Username    : admin.xxxxx
Sessionname : rdp-tcp#54
Id          : 3
State       : Active
IdleTime    : 08/09/2017 16:38:21
LogonTime   : 07/21/2017 05:35:00

Username    : xxxxxxxx
Sessionname :
Id          : 4
State       : Disconnected
IdleTime    : 08/03/2017 16:35:50
LogonTime   : 07/21/2017 09:25:00

Username    : vertigoray
Sessionname : console
Id          : 5
State       : Active
IdleTime    :
LogonTime   : 08/09/2017 16:40:00

Conclusion

I bit more complex, but still beautiful, right? I’ll have to turn that into a module for ease of use. I ended up making this into a module: QuserObject.