Use PowerShell to Detect if a Workstation Is in Use

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 24×7 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