Hey, Scripting Guy! Yes, Another One...

The Microsoft Scripting Guys

Download the code for this article: ScriptingGuy.exe (115KB)

Greetings, everyone. In one of their classic Halloween episodes, the Simpsons became rich and famous. Their faces were everywhere: on billboards, on T-shirts, you name it. Not too surprisingly the people of Springfield quickly tired of seeing the Simpsons everywhere they looked. Finally, while browsing through a record store, Otto the bus driver picked up a copy of The Simpsons Sing Calypso and spoke for the entire town when he growled, "Man, this thing is really getting out of hand."

So why did we mention that? Well, let’s see. There’s the "Hey, Scripting Guy!" Blog. And now there’s another Hey, Scripting Guy! column in this magazine.

Wait a second: is this getting out of hand? No, of course not. Sure, people might get tired of the Simpsons, but tired of Hey, Scripting Guy!? Couldn’t happen.

At any rate, welcome to the latest Hey, Scripting Guy! incarnation. No doubt you’re wondering what makes this column different from the column you read each day in the Script Center. Well, the big difference lies in the subject matter. For the most part the daily column deals with scripting questions that can be answered relatively quickly and easily; solutions posted there tend to be short and easy-to-digest.

We think there’s plenty of value in addressing these simpler questions. On the other hand, we also get asked plenty of questions that can’t be dismissed in three or four paragraphs. And that’s the purpose of this column: in each issue we’ll try to address a question that just seemed too complicated and too weighty to handle in the daily column. For example, take a look at the question for this issue:

"How can I determine the last time a user logged on to Active Directory®?"

Yes, we know, it doesn’t sound very hard. After all, Active Directory user accounts have a lastLogon attribute that keeps track of the last time a user logged on to the domain. Can’t we just retrieve and echo the value of the lastLogon attribute for any given user?

We wish. Instead we have to deal with a number of problems.

The lastLogon Attribute is Not Replicated Between Domain Controllers That’s a huge problem. Suppose you logged on to domain controller A one year ago. Ever since then, for whatever reason, you’ve always logged on to domain controller B. If you contact domain controller A and ask for the last logon time you’ll be told that you haven’t logged on to the domain for a year; that’s because domain controller A only tracks the last time you logged on to domain controller A. (Yes, it does seem a little selfish and self-centered, doesn’t it?) The only way to deal with this problem is to contact each domain controller in the domain, request the last logon time, and then compare the various dates and times.

The lastLogon Attribute is Stored in Large Integer Format Believe it or not, the lastLogon attribute is stored as the number of 100-nanosecond intervals that have elapsed since the 0 hour on January 1, 1601, which, by coincidence, is the same day that Scripting Guy Peter Costantini was born. This would pose a challenge even if Visual Basic® Scripting Edition (VBScript) could handle the large integer format, but unfortunately VBScript can’t handle the large integer format. The workaround? We can’t just echo back a date and time but instead have to perform some crazy-looking math and convert lastLogon to a value VBScript can deal with.

The lastLogon Attribute Date and Time is Based On Greenwich Mean Time (GMT) In other words, it doesn’t necessarily reflect local time. Thus we have to convert GMT values, also known as Coordinated Universal Time (UTC), to local time. This isn’t too terribly hard, but it is another hoop you have to jump through. But, hey, other than that it’s all pretty easy.

Actually, things are a little easier in Windows Server™ 2003; that’s because the Windows Server 2003 schema includes a new attribute—lastLogonTimestamp—that is replicated between domain controllers. For more information, see the Script Center article "Dandelions, VCR Clocks, and Last Logon Times: These are a Few of Our Least Favorite Things".

You can probably see why we haven’t dealt with this in our daily column—just the list of problems we have to overcome is longer than most of our columns. But with a little extra room in the magazine and the expectation of a more experienced audience than we sometimes get with the daily column, we thought we’d take a shot at it.

We’re still taking a few liberties here, though. For example, our script is really optimized for single-domain organizations: by default, it will retrieve information for all the domain controllers in the domain. If you have multiple domains, that means it will go out and grab information from all those domains, even if the user can’t log on there. There’s a way to work around that, but it’s a bit too complex to cover this time. It will have to wait for another column on another day.

These are the steps we’re going to perform to tackle the problem:

  • We’re going to start by using Windows® Management Instrumentation (WMI) to retrieve time zone information from the local computer. In particular, we need to know the difference (the bias or offset) between local time and GMT, as well as any adjustments that need to be made for daylight saving time.
  • Next we’ll do a search of Active Directory and return a list of all the domain controllers in the domain. We do that because, as we noted, we need to retrieve the value of the lastLogon attribute from each domain controller.
  • We’ll then connect to each domain controller and grab the value of the lastLogon attribute for a specific user. After we have that value we’ll use some fancy mathematical sleight-of-hand to convert the large integer value into a plain old date and time.
  • As we contact each domain controller we’ll use a variable to keep track of the last time the user logged on to the domain. At the end of the script, we’ll echo only the last logon time. Figure 1 shows the script.

Figure 1 Determine Last Logon Time

On Error Resume Next dtmLatestLogon = #1/1/1601# strComputer = "." Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") Set colTimeZones = objWMIService.ExecQuery("Select * From Win32_TimeZone") For Each objTimeZone in colTimeZones intTimeZoneBias = objTimeZone.Bias intDaylightBias = objTimeZone.DaylightBias Next Const ADS_SCOPE_SUBTREE = 2 Set objRootDSE = GetObject("LDAP://RootDSE") strConfigurationNC = objRootDSE.Get("configurationNamingContext") Set objConnection = CreateObject("ADODB.Connection") Set objCommand = CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection objCommand.Properties("Page Size") = 1000 objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE objCommand.CommandText = _ "SELECT ADsPath FROM ‘LDAP://" & strConfigurationNC & "’ WHERE objectClass=‘nTDSDSA’" Set objRecordSet = objCommand.Execute objRecordSet.MoveFirst Do Until objRecordSet.EOF Set objParent = GetObject(GetObject(objRecordset.Fields("ADsPath")).Parent) strDCName = objParent.dnsHostName Set objUser = GetObject _ ("LDAP://" & strDCName & "/CN=Ken Myer,OU=Finance,DC=fabrikam,DC=com") Set objLastLogon = objUser.Get("lastLogon") intLastLogonTime = objLastLogon.HighPart * (2^32) + objLastLogon.LowPart intLastLogonTime = intLastLogonTime / (60 * 10000000) intLastLogonTime = intLastLogonTime / 1440 dtmLastLogon = intLastLogonTime + #1/1/1601# dtmLastLogon = DateAdd("n", intTimeZoneBias, dtmLastLogon) dtmLastLogon = DateAdd("n", intDaylightBias, dtmLastLogon) If dtmLastLogon > dtmLatestLogon Then dtmLatestLogon = dtmLastLogon End If objRecordSet.MoveNext Loop Wscript.Echo "Last logon: " & dtmLatestLogon

So how does this all work? Well, it starts off easy enough: we begin by querying the WMI class Win32_TimeZone and grabbing the GMT and daylight saving time offsets, as shown in Figure 2.

Figure 2Retrieve Time Zone Offsets

strComputer = "." Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") Set colTimeZones = objWMIService.ExecQuery("Select * From Win32_TimeZone") For Each objTimeZone in colTimeZones intTimeZoneBias = objTimeZone.Bias intDaylightBias = objTimeZone.DaylightBias Next

Next we need to get a list of all the domain controllers in our domain. We can do this by querying the Configuration container in Active Directory and requesting a list of all the NTDSDSA objects. The configuration container contains configuration information such as sites, services, display specifiers, and domain controllers. The NTDSDSA object, in turn, represents the Active Directory service installed on a server. If a server has Active Directory installed, then it must be a domain controller.

We won’t talk much about searching Active Directory; for more information you might check out the April and May 2005 Tales from the Script columns (April 2005 and May 2005).

We do have to use one little trick when retrieving the name of each domain controller, however. Remember, technically we aren’t searching for domain controllers; we’re searching for instances of the Active Directory service installed on a computer. Thus we get back information similar to this:

LDAP://CN=NTDS Settings,CN=atl-dc-01, CN=Servers,CN=NA-GA-ATL,CN=Sites, CN=Configuration,DC=fabrikam,DC=com

Nice, but how does that help us connect to an individual domain controller?

In and of itself maybe it doesn’t. However, we can retrieve the value of the service’s Parent object. In this case that will be the computer where the service is installed. If we bind to the parent object we can then get hold of the parent object’s dnsHostName, which will be the name of the domain controller as registered in DNS. That’s what we do here:

Set objParent = GetObject(GetObject _ (objRecordset.Fields("ADsPath")).Parent) strDCName = objParent.dnsHostName

With the name of the first domain controller safely stashed away in the variable strDCName, we can then bind to the Ken Myer user account (for example) on that domain controller. That’s what this line of code does:

Set objUser = GetObject _ ("LDAP://" & strDCName & _ "/CN=Ken Myer,OU=Finance,DC=fabrikam,DC=com")

After connecting to this first DC, we retrieve the value of the lastLogon attribute and use some nifty mathematics to convert the large integer to a "real" date and time. We won’t detail that process here; you can read all about that in "Dandelions, VCR Clocks, and Last Logon Times." We do, however, need to adjust this value for both local time and daylight saving time. That can be done by adding both the GMT and daylight saving time offsets, like so:

dtmLastLogon = DateAdd("n", intTimeZoneBias, dtmLastLogon) dtmLastLogon = DateAdd("n", intDaylightBias, dtmLastLogon)

Whew. At this point we now have the last date and time that the user last logged on to this domain controller. Is this the last time the user logged on to the domain itself? Well, we don’t know, and the only way we can find out is to check the rest of the domain controllers. We’ll loop around and do that in a second. First, though, we execute this block of code:

If dtmLastLogon > dtmLatestLogon Then dtmLatestLogon = dtmLastLogon End If

So what’s going here? Well, way back at the very beginning of time (or the very beginning of the script, whichever came first) we created a variable named dtmLatestLogon and set the value to January 1, 1601. What we’re doing now is checking to see if the last logon time we just retrieved is later than January 1, 1601. If it is, then we’ll replace the value of dtmLatestLogon with the last logon value we just retrieved, so we can keep track of the latest logon time.

Suppose we get back a last logon value of June 1, 2005 (to keep this explanation a little cleaner we’ll just use dates here, ignoring times). We thus set the value of dtmLatestLogon to June 1, 2005. Now we loop around and retrieve the lastLogon value for the next domain controller. Suppose we get back May 15, 2005. We compare this value with the current value of dtmLatestLogon: June 1, 2005. Is May 15, 2005 greater than June 1, 2005? No. Therefore we just loop around and check the next domain controller. By the time we’re done dtmLatestLogon will hold the last logon time for the user.

We then echo the value of the last logon time and we’re finished. Just that, uh, easy and just that quick.

Incidentally, you can use this same process in order to determine the last time a user logged off; in that case you would simply work with the lastLogoff attribute rather than using the lastLogon attribute. And keep in mind that both of these attributes are found in the Computer class as well as the User class. In other words, you can also use this script to determine the last time a computer logged on to or logged off of the domain. Pretty cool, huh?

But that’s something you’ll just have to investigate on your own. We really don’t have time for that discussion right now. After all, we’ve got to get started on Hey, Scripting Guy Sings Calypso. Anyone know of a WMI term that rhymes with coconut?

TechNet Online Resources

This column offers just a brief look at a specific scripting scenario. For more in-depth information on scripting and Active Directory, visit these TechNet Online resources.

The Microsoft Scripting Guys spend most of their time at the beach...oh, um, no, we mean: The Scripting Guys are hard-working members of the Microsoft Windows Server User Assistance team. To find out more, go to About The Scripting Guys.

© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.