The Surly Admin

Father, husband, IT Pro, cancer survivor

Who’s Logged in Where?

Interesting script request through Spiceworks last week, where QQQ was asking to find out who was logged in on his computers at any given moment.  This seemed like a relatively easy script to throw together, here’s how I did it.


There were a few things to consider when approaching this script.  How do we go through the network for computers?  Once we have that, there are several states we have to consider:  powered on, powered off, is it a Windows PC (or having WMI problems)?  And last, how do we deliver the completed report?

How do we go through the computers?  One thought would be to have the script accept an array of computer names and work off of that, but what I decided to do was to use an IP range and loop through that.  First things first, let’s define our network and IP ranges we want to loop through.  This is a very simplistic approach and I’m not doing anything to verify proper IP ranges, or increasing the octets as you proceed through the network addresses–and if that doesn’t make any sense to you, that’s OK.  Suffice to say you need a pretty simple network for this to work properly and luckily most of us have that.

$Network = "192.168.0"
[int]$StartIP = 100
[int]$EndIP = 110

$Result = @()
$StartIP..$EndIP | ForEach {

Powershell has a cool little feature where you can use “..” to have it create a set of numbers between two numbers seperated by “..”.  That means 1..5 would produce “1 2 3 4 5”.  This is great for setting up a simple loop, and that’s just what I did above.  Get an integer of the starting IP address and one for the ending address and then create a range of all the  number in between and pipe that into the ForEach-Object cmdlet.  Now, when we go into the scriptblock for ForEach $_ will contain the current number in the range.

If (Test-Connection -ComputerName "$Network.$_" -Quiet -Count 2)
{ $WMI = Get-WmiObject Win32_ComputerSystem -ComputerName "$Network.$_"

First we use Test-Connection to test if something responds to the IP address, which is constructed by combining the $Network variable and the current pipeline number.  If something responds we’ll then use WMI to connect to that server to get the logon information.  Again, we’ll take advantage of the fact that if the WMI call fails, the $WMI variable will be $null and we can test that.  If it is $null we can report that the network node responds to ping (is powered on) but we can’t get the user logon information.  Otherwise we’ll get the logon information and save it.

If ($WMI -eq $null)
{ $User = "Unknown"
$ComputerName = "Unknown - either WMI error or non-Windows device"
{ If ($WMI.UserName)
{ $User = $WMI.UserName
{ $User = "No one logged in"
$ComputerName = $WMI.Name
$PowerState = "Powered On"
{ $User = "None"
$PowerState = "Powered Off"
$ComputerName = "None"

Here we check if $WMI is $null, and also check if the .UserName property has any value in it, if not we’ll assume the PC is powered on but no one is logged in.  Loading all of this into variables for later use.

$Result += New-Object PSObject -Property @{
'Computer Name' = $ComputerName
IP = "$Network.$_"
User = $User
'Power State' = $PowerState

Then we load everything into a PSObject so we can use it later.  That’s the end of the loop, then we create a little here-string with some HTML/CSS code in it for our report, and I admit I kept it really simple here.

$Header = @"
TABLE {border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}
TD {border-width: 1px;padding: 3px;border-style: solid;border-color: black;}
$Body = $Result | ConvertTo-Html -Head $Header | Out-String
Send-MailMessage @splat -Body $Body -BodyAsHTML

Interesting little problem, when you use the ConvertTo-HTML cmdlet it creates a object which the -Body parameter in Send-MailMessage can’t use!  So that’s why we pipe the results to Out-String.  I’m really enjoying the ConvertTo-HTML with the -Header parameter lately.  Might not be the best HTML you’ve ever seen in your life, but it makes for a really presentable report and is incredibly easy to do (you can re-use that here-string in a lot of scripts–in fact I think I’ve used it in 3 different strings so far).

Now, if you’ve read the full Spiceworks thread I linked to above, you’ll notice that another user (J500) asked for the ability to scan using Active Directory Organization Unit.  This turned out to be a little more challenging then I expected.

Scan by OU

Couple of extra goals here, one to scan by the partial name of an OU–the reason for this is knowing the full FQDN of an OU can be a pain (though I do have a script for that).  Second goal is I wanted to validate the OU was correct by using the Get-ADOrganizationUnit cmdlet.

But here’s where things started to go bad.  I was going to use Get-ADComputer to find the computers, and then use the -Filter parameter to just get the computers I wanted.  A quick check of the help file showed that -Filter supports -Like so finding the string we want should be no problem.

Get-ADComputer -Filter { DistinguishedName -like "*$OU*" }

Except I didn’t get anything.  This kicked off a lot of testing, changes, Google searches and no matter what I did I couldn’t make this work.  I even tried using the -LDAPFilter with *$OU* in there and it just didn’t work.  I finally came to the conclusion that while it says it supports -like, the cmdlet really doesn’t.  So as much as I hated to do this, it was time to use the brute force method.

Get-ADComputer -Filter * | Where { $_.DistinguishedName -like "*$OU*" }

Worked perfectly, and with my small network it’s actually very fast too.  If you have a network with tens of thousands of computers you will likely have some slowness.  Next test, was to validate the OU in Active Directory to make sure it’s there–technically we do this test first but I wrote the script out of order and this is where I ran into a problem).

$OU = Get-ADOrganizationalUnit -Filter { DistinguishedName -eq $ScanOU }

And this worked fine, until it finally hit me that I had some conflicting requirements.  The search on the computers was more generalized so you didn’t need the full FQDN of the OU, but the validation of the OU was using the exact FQDN.  The two just can’t live together.  You’re either using the full FQDN or you’re not and trusting something will come up when you do a partial name search.

I decided to go with the partial name as it’s a bit more user friendly, so removed the OU validation completely.  After that the script had to be modified some more.

$Test = Test-Connection $($_.Name) -Count 2 -ErrorAction SilentlyContinue
If ($Test)
{ $IP = $Test[0].IPV4Address.IPAddressToString

With the IP scanning version, we knew the IP address, but with the name version we don’t, but I still wanted to include the IP address in the report.  This required a small change to the Test-Connectivity section, instead of using the -Quiet parameter I used the default behavior which returns several pieces of information, including the .IPV4Address property.  Of course, that’s not in a string, so I also had to invoke the IPAddressToString method to get the data the way I wanted it.  You’ll also notice that I use the [0] array element reference, this was because in the Test-Connectivity command I use the -Count 2 parameter which does 2 pings to make sure the device is there.  That means the $Test variable will actually be an array with 2 elements in it (1 for each ping).

After that the script is pretty much the same, though I did add a little tracking ($Count) into the script to let me know the final results at the end beyond just the emailed report.

You can pull the scripts from the Spiceworks thread if you want to use them.


November 15, 2012 - Posted by | PowerShell | ,

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: