PSH Oneliner: Find Last Logged On User(s) / Owner Print E-mail
Written by Darwin Sanoy   
Saturday, August 9, 2014 8:01am

Desktop management systems frequently add or update software using a user profile other than the actual user of the system.  This creates a classic problem in discerning which user profile(s) represent the active user(s) of the machine.  Several variants of this oneliner show a couple methods for identifying one or more "active", "recent" or "owning" users of the machine locally or remotely.  This article layer's up useful code that also demonstrates: selecting files by age, sorting one array with another, showing hidden files, selecting the most recent file and other techniques.

In this article we are going to layer up the command from a basic command you may already be familiar with.  For brevity the output of these commands is not in the article - but feel free to execute the commands as you read to cement the concepts and see real output.

We are going to use a "rough and ready" criteria.  In my world, "rough and ready" means that you may trade off a small amount of selection precision for flexibility.  For instance, since this script looks only at the dates of ntuser.dat user registry files, it is super easy to make it work on remote machines.  Occasionally it may needlessly update a profile that should have been out of scope, but as long as your assumptions about "active users" on your target systems is accurate - it is not likely to leave one out that needs updating. 

We do not want to use the date of the user profile folder because it is not updated with every user logon.  Thanks to powershell's "powerful" path wildcarding capabilities - it is still a very simple process to list out all the ntuser.dat files like this:

gci c:\users\*\ntuser.dat

Nothing!  Ahhh, these are hidden files, so the force switch is necessary:

gci c:\users\*\ntuser.dat -force

This should result in a list of every ntuser.dat file on your system.  Let's add a date sort:

gci c:\users\*\ntuser.dat -force | sort LastWriteTime

We are really after the directory name which contains the user name, so let's select that out.  Using -expandproperty will also give us that name as a string:

gci c:\users\*\ntuser.dat -force | sort LastWriteTime | select -ExpandProperty DirectoryName

This should result in a list of just the folder names.  Now let's select only the last one using "-last 1" on the Select CMDLet - which would be the most recently modified due to our sort order:

gci c:\users\*\ntuser.dat -force | sort LastWriteTime | select -ExpandProperty DirectoryName -last 1

Now we have the user profile folder of the user whose ntuser.dat was most recently changed.  Of course this is correct much of the time, but could be incorrect for infrequently logged on machines or where a non-owner was the most recent logon.

If you need to update something in the profile folder - you have your data, just set a variable equal to the above command.  However, let's say you want just the user id - this can be accomplished using an enclosing split command:

split-path -leaf (gci c:\users\*\ntuser.dat -force | sort LastWriteTime | select -ExpandProperty DirectoryName -last 1)

Perhaps you are thinking to yourself, I would like to play it safer than that, I would like to update the 3 most recently modified profiles just in case the owner end user wasn't the last one to use it.  That is easily accomplished by changing our -last parameter to 3, like this:

split-path -leaf (gci c:\users\*\ntuser.dat -force | sort LastWriteTime | select -ExpandProperty DirectoryName -last 3)

But some of you might say, I would like it to be more accurate than arbitrarily picking the last three - how about anything newer than 1 month old is considered an "active" logon.  One word of caution here - if you are updating individual user registries and you either push this criteria too far into the past or frequently run scripts that update these user registries, your script itself may create the illusion that certain users are *active* by constantly causing the date on ntuser.dat to update.

Notice that if we truly want *only* profiles modified in the last 30 days, we no longer need the "Sort" CMDLet nor the "-last 3".

split-path -leaf (gci c:\users\*\ntuser.dat -force | where-object {$_.lastwritetime -ge (Get-Date).AddDays(-30)} | select -ExpandProperty DirectoryName) 

But let's say you run this on a system and get too many results.  You could re-instate the sort and last parameters to limit it to the most recent 3 users modified within the time frame choosen:

split-path -leaf (gci c:\users\*\ntuser.dat -force | where-object {$_.lastwritetime -ge (Get-Date).AddDays(-30)} | sort LastWriteTime | select -ExpandProperty DirectoryName -last 3)

What if you also spot the name of a user that is used strictly for background maintenance that should not be updated?  If there is just one of these profiles, the following modification to the where clause can exclude it (User id is "MaintenanceID").  Notice that when our selection occurs before "Select -ExpandProperty" we must use the where-object clause, the appropriate object property, a wildcard comparison operator and a wildcarded string to match the rest of the folder name:

split-path -leaf (gci c:\users\*\ntuser.dat -force | where-object {$_.lastwritetime -ge (Get-Date).AddDays(-30) -and $_.Directory -inotlike "*MaintentanceID"} | sort LastWriteTime | select -ExpandProperty DirectoryName -last 3)

However, what if you saw *multiple* user ids in the list that you did not want to include?  Filtering one list (array) via another list can be quite complex.  We can keep it simple by moving the selection logic from a "where-object" clause (where we still dealing with file objects) until after "Select -ExpandProperty"  At this point in the pipeline we are dealing with just a string array of the actual folder names so we can use select-string which will greatly simplify the task of filtering one list with another. ("Filter one list with another" tip was previously published here)

split-path -leaf (gci c:\users\*\ntuser.dat -force | where-object {$_.lastwritetime -ge (Get-Date).AddDays(-30)} | sort LastWriteTime | select -ExpandProperty DirectoryName -last 3 | select-string -pattern @("MaintentanceID","AnotherID") -simplematch -notmatch)

If you're writing a script, you would probably like to store this data in a variable.  Keep in mind that if only one value is returned, PowerShell would normally make your variable into a scalar (single value) and into an array if multiple values were returned.  This can make further manipulation of the values respond inconsistently to the rest of your script.  So the following forces the return value to be an array - even if there is only one (allowing you to consistently deal with it as an array in subsequent code):

$ActiveUserIDs = @(split-path -leaf (gci c:\users\*\ntuser.dat -force | where-object {$_.lastwritetime -ge (Get-Date).AddDays(-30)} | sort LastWriteTime | select -ExpandProperty DirectoryName -last 3 | select-string -pattern @("MaintentanceID","AnotherID") -simplematch -notmatch))

Of course, if you are looking only for the last one changed, using "-last 1" would ensure that the return variable was *consistently* scalar (single value) - avoiding the need to treat the variable as an array of one if you know you only want one return result.

It is a quick an easy change to use this code against a remote machine *where you have admin rights* by changing "C:\" to "\\machinename\c$" like this:

$ActiveUserIDs = @(split-path -leaf (gci \\machinename\c$\users\*\ntuser.dat -force | where-object {$_.lastwritetime -ge (Get-Date).AddDays(-30)} | sort LastWriteTime | select -ExpandProperty DirectoryName -last 3 | select-string -pattern @("MaintentanceID","AnotherID") -simplematch -notmatch))