Use PowerShell to Detect if a Workstation Is in Use

Use PowerShell to Detect if a Workstation Is in Use

  • Comments 9
  • Likes

Summary: Learn how to use Windows PowerShell to detect if a workstation is in use prior to performing a reboot

Hey, Scripting Guy! Question

  Hey, Scripting Guy! I have a problem in that I need to detect when a user is logged on to a system so that I can patch it and reboot the machine without disrupting the individual. Can this be done very easily?

—SG

Hey, Scripting Guy! AnswerHello SG,

Microsoft Scripting Guy, Ed Wilson, here. Today we have a guest in the house! That is right—guest blogger, Glenn Sizemore, who will answer your question. Glenn has held just about every position one could hold in IT, everything from cable dog to enterprise architect. He started scripting early in his IT career, and has made a living off it ever since. Along the way, he started a blog, wrote a book, and was the winner of the coveted 2010 Scripting Games. Take it away Glenn…

If you have worked in the IT world for a while, you have run into a scenario where you had to reboot everything. Usually this comes about as part of a patch, or the need to upgrade an application. Too easy, right? Add in the requirement that you cannot disrupt the user base, and it gets a bit more difficult. However, this scenario is still doable because all that is needed are defined maintenance windows where we can disrupt services. Alas, it is not that easy because we have the final requirement of a 24x7 workforce.

Now you have the doomsday scenario for maintaining a patched environment. You cannot perform any blind pushes, and you have to programmatically check to see if any users are using their computers prior to doing anything. Unfortunately, we will not be showing you a one, end-all solution today. Instead, we will cover several techniques for detecting if a user is logged on to a system. You can then select the techniques that will work for you.

We will start with Windows Management Instrumentation (WMI) because it is the most familiar. If you are targeting mainly workstations and you are not concerned with remote users, you can reliably detect if a user is logged on via the WIN32_Computersystem class. This technique is shown here.

$Computers = @(
,   "PC001"
,   "PC002"
,   "PC003"
,   "PC004"
)

Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computers |
    Where-Object {-Not $_.Username} |
    Select-Object -ExpandProperty Name

Perfect! We are done, right? Not yet. There is a known bug in Windows Vista and Windows Server 2008 whereby this field will be misreported, and it only reports who is logged on to the console. So how do we account for users who are logged on to computers running Windows Vista and using a remote desktop? It has been a standard practice for years to check for the explorer.exe process as shown here.

Get-WmiObject -Class win32_process `
    -computer 'PC001'
    -Filter "name='explorer.exe'" |
    Foreach-Object {
        $_.GetOwner()
    }

Unfortunately, that approach is flawed on many levels. Service accounts will trigger a false positive using this method. Not to mention that you are assuming explorer.exe is the default shell for all users. Although it is possible to work around these shortcomings, it is best that we do not use this technique at all.

By far one of the most successful techniques is to lean on Terminal Services or Remote Desktop Services, because anyone who has ever used Terminal Services Manager or Remote Desktop Services Manager will tell you that they unequivocally know who is using what and how. Not only can we determine who is using a given system, we also know if they are actively using it, and if not, how long since they stopped! With that in mind, you should be able to determine without any doubt if it is safe to reboot a computer.

Unfortunately, this information is buried deep in the unmanaged code of Windows. Fortunately, MVP, Shay Levy, wrote a Windows PowerShell module that ferries that critical information from the depths of Windows for us! The module leverages the Cassia project to obfuscate the differences in Terminal Services and Remote Desktop Services between versions of Windows. This means that the functions within the Terminal Services PowerShell Module should work on any computer beginning with Windows 2000. For more information about the module, see the Terminal Services PowerShell Module site.

After the Terminal Services PowerShell Module is loaded, you can use the following code. The code should return green for any host that isn’t being used, and red for any host currently in use. (For more information about modules, refer to these Hey, Scripting Guy! blogs.)

$Computers = @(
,   "PC001"
,   "PC002"
,   "PC003"
,   "PC004"
)

Foreach ($ComputerName in $Computers)
{
    $sessions = Get-TSSession -ComputerName $ComputerName `
        -State 'Active'
    If ($sessions) 
    {  
        Write-Host $ComputerName -ForegroundColor 'Red'
    }
    Else
    {
        Write-Host $ComputerName -ForegroundColor 'Green'
    }
}

Unfortunately, there are complications with the Terminal Services and Remote Desktop Services providers on client versions of Windows. Mainly, if you try to connect with Terminal Services Manager or Remote Desktop Services Manager to a non-server edition of Windows, you will discover compatibility issues. If you are only scanning server versions of Windows, such as a XenApp farm, the Terminal Services PowerShell Module is your one stop shop—but if you are scanning desktops, you will need to keep looking.

So far we have tried WMI, process discovery, and using the built-in Terminal Services and Remote Desktop providers, and all of them have fallen short of an all-in-one solution. So how can you safely reboot a server or workstation?

Answer: Mark Russinovich! The PsLoggedon utility within the Windows Sysinternals Suite is the only tool I have come across that will work the first time every time! We can wrap PsLoggedon within a simple Windows PowerShell script to enable a best-of-both-worlds solution. The script to do this is shown here.

$Computers = @(
,   "PC001"
,   "PC002"
,   "PC003"
,   "PC004"
)

Foreach ($Computer in $Computers)
{
    [object[]]$sessions = Invoke-Expression ".\PsLoggedon.exe -x -l \\$Computer" |
        Where-Object {$_ -match '^\s{2,}((?<domain>\w+)\\(?<user>\S+))|(?<user>\S+)'} |
        Select-Object @{
            Name='Computer'
            Expression={$Computer}
        },
        @{
            Name='Domain'
            Expression={$matches.Domain}
        },
        @{
            Name='User'
            Expression={$Matches.User}
        }
    IF ($Sessions.count -ge 1)
    {
        Write-Host ("{0} Users Logged into {1}" –f $Sessions.count,    
            $Computer) -ForegroundColor 'Red'
    }
    Else
    {
        Write-Host ("{0} can be rebooted!" -f $Computer) `
            -ForegroundColor 'Green'
    }
 }

There is actually a lot going on there, so let’s break it down a little. First, we use the Invoke-Expression cmdlet to execute the PsLoggedon.exe. Note that in our example we are assuming that the executable is in the current directory. Then we pipe the output of that cmdlet to Where-Object. Here we are using a little regular expression magic to extract any user name or domain information that is returned. The following image explains each part of this regular expression.

Image of regular expression

The following table shows the information from the previous image in a table view.

Char & meaning

Char & meaning

Char & meaning

Char & meaning

Char & meaning

Char & meaning

 

Match all of case 1

OR

Match all of case 2

^\s{2,}

((?<domain>\w+)

\\

(?<user>\S+))

|

(?<user>\S+)

Match any line that starts with at least two spaces.

Save any letters to the domain property

Match exactly one \

Save any remaining none whitespace to the user variable

OR

Save any none whitespace to the user variable

This will do two things for us: it will filter out any of the output that we are not interested in, and it formats the data in a format that we can easily ingest, which we take advantage of by piping that output to the Select-Object cmdlet. We then create a custom PSObject with the User, Domain, and Computer properties. All of this is then cast into an [object[]] collection. We do this to ensure that we will have the count property even if we return a single object.

Now that we have our collection of objects, it is a simple if statement to determine if anyone is logged on to the target computer. Sadly, as cool as this final solution was, it too has a flaw. PSLoggedon is so effective at detecting if anyone is logged on, that a service account will be returned.

Where do we go from here?

Honestly, this is not a simple, cut-and-dried case—you will have to take a blend of the techniques that we covered today and develop your own solution. We gave you the ingredients; it’s up to you to cook the meal. Happy scripting!

SG, that is all there is to detecting if a workstation is in use prior to a reboot. Thank you, Glenn, for sharing with us today. Join me tomorrow when I will post the Scripting Games Frequently Asked Questions document.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • First of all - great article, very professional, thanks Glenn! :)

    One tip regarding array definition I've recently (and accidentally) discovered: you do not need comma if you use <newline>:

    @(  1

       2

       3

       4 )[1]

    2

    And the result can get quite unexpected if you use comma at the line start, as Glenn did:

    $a = 1..20

    @( $a, $a, $a

      ,$a,$a, $a) | foreach { $_.length }

    '-' * 80

    @( $a, $a, $a,

      $a,$a, $a) | foreach { $_.length }

    '-' * 80

    $b = @( $a, $a

       ,$a, $a)

    'No such index'

    $b[1][0][1]

    'Unary comma, so array got nested'

    $b[2][0][9]

    It won't bite you if array element is alone on the line, but well... you may get something unpredictable at times... ;)

  • Bartek,

    Thank you so much for the tip, and of course you are 100% correct. Honestly I picked up the habit from reading Jakul’s code, It’s handy because reordering the items in an array won’t break anything.  If you use the trailing comma you constantly have to shuffle commas as you modify your code.  Plus it's easy to spot a typo using a preceding comma.  Most people hate them, but I've grown so accustomed to it, that it just looks wrong without it.  The fix of coarse would be to remove the first comma, or use no commas at all, but again it just looks wrong to me.

    Thanks again for your kind words and highlighting the potential pitfall of defining an array as I have.

    ~Glenn

  • Thanks for the article, well written. I am new to Powershell scripting and Windows scripting, as a result my current knowledge does not provide me the ability to "cook the meal".  I have a Client/Server sandbox running XP 64bit and VPC DC & 3 clients, I would really appreciate it if you could point me to a doc or reference which would provide the necessary cooking directions, as any good recipe does. :)

    Thank you for your time!

  • Thanks Glenn.  That very neatly solves an issue I was just starting to try to figure out.

    Regarding the use of psloggedon.exe, I added a couple of lines to run psloggedon.exe from webdav if it is not found locally and also added the "-accepteula" switch (becaue I didn't feel like testing the registry for the "EulaAccepted" key) so that I don't have to click "agree" when running from different machines or accounts.  

    if (Test-path Y:\PSTools\psloggedon.exe) {

    #run locally

    $PsloggedonPath = "Y:\PSTools\psloggedon.exe"

    }

    Else {

    #run via webdav:

    $PsloggedonPath = "\\live.sysinternals.com\tools\psloggedon.exe"

    }

    With those changes the first line of the ForEach loop becomes:

    [object[]]$sessions = Invoke-Expression "$PsloggedonPath -accepteula -x -l \\$Computer" |

    This seems a little clunky to me but it work adequately for my needs. YMMV.

    Thanks again for the excellent article. :-)

  • I am seeing an issue with PSLoggedOn where it is reporting users as being logged on when they definitely aren't. I am not sure, but I assume that PSLoggedOn works by querying which user registries hives have been loaded. Looking at these machines, sure enough multiple registry hives for the various users have been loaded (Windows 7), but they're not actually logged on and have not been logged into these machines for months. A restart resolves the issue, but anyone know what could cause this behaviour? I'm thinking maybe it is Symantec AV.

  • Thank you very much for the article. It really helped me. However, I had to make a few alterations to it so that it won't report incorrect data.

    The changes I made are...

    * Removed (?<user>\S+) from regular expression so that it won't report "Connecting" in user account. This is happening because when psloggedon is run, it outputs the data saying  connecting to \\computer.

    * Suppressed psloggedon logo information by redirecting the error output to null

    Thanks again. Modified script is available at techibee.com/.../1082

  • I've been browsing through those blogs since 1 month and this tutorial is by far something of the best you are able to find!

    useful, informative and really well graphical aranged. most people who try to explain regex should be refered to your tutorial, even though its just a part of it.

    congratulations again.

    regards

    alex

  • I could not get these scripts to work
    I did get this one to work

    # .csv is the logfile, - path it where you want the logfile
    # .txt is your list of computers, path where you wish
    # psloggedon.exe needs to point to your copy

    [string]$Logfile = "c:\logfiles\loggedOnUsers.csv"

    Foreach ($Computer in Get-Content C:\logfiles\loggedOnUsers.txt)
    {
    $cmd = & 'C:\pstools\PsLoggedon.exe' -x -l \\$Computer >> $Logfile

  • # .csv is the logfile, just path it where you want the logfile
    # .txt is your list of computers, path where you wish
    # psloggedon.exe needs to point to your copy

    [string]$Logfile = "c:\logfiles\loggedOnUsers.csv"

    Foreach ($Computer in Get-Content C:\logfiles\loggedOnUsers.txt)
    {
    $cmd = & 'C:\pstools\PsLoggedon.exe' -x -l \\$Computer >> $Logfile}

    I just noticed the closing bracket got missed on that last post - sorry- its here now