{"id":51463,"date":"2010-01-27T12:30:00","date_gmt":"2010-01-27T12:30:00","guid":{"rendered":"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2010\/01\/27\/dandelions-vcr-clocks-and-last-logon-times-these-are-a-few-of-our-least-favorite-things\/"},"modified":"2010-01-27T12:30:00","modified_gmt":"2010-01-27T12:30:00","slug":"dandelions-vcr-clocks-and-last-logon-times-these-are-a-few-of-our-least-favorite-things","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/scripting\/dandelions-vcr-clocks-and-last-logon-times-these-are-a-few-of-our-least-favorite-things\/","title":{"rendered":"Dandelions, VCR Clocks, and Last Logon Times: These Are a Few of Our Least Favorite Things"},"content":{"rendered":"<h2><font size=\"1\">&nbsp;<\/h2>\n<p><\/font><\/p>\n<h2><font size=\"1\">(Note: This blog post appeared originally as an article on the old Microsoft Script Center. It was originally published in December 2005. We are resurrecting it here by popular demand.)<\/font><\/h2>\n<p>Sometimes the things that should be so easy turn out to be incredibly difficult. For example, have you ever tried to get rid of dandelions? (One of the Scripting Guys once found a dandelion growing on the <i>roof of his house<\/i>.) How about getting your VCR clock to stop flashing <b>12:00<\/b> over and over again? (It\u2019s been estimated that as many as 25% of all the VCRs in use are even now flashing <b>12:00<\/b>.) And as one Scripting Guy discovered, it\u2019s actually easier to build a new Volkswagen Passat from scratch than it is to change the headlight.<\/p>\n<div class=\"overview\">\n<p>The same thing has always been true of a seemingly-innocuous Active Directory task: determining the last time a user logged on to the domain. Determining the last logon time ought to be pretty easy; after all, Active Directory includes an attribute \u2013 <b>lastLogon<\/b> \u2013 that tells you the last time a user or computer logged on. How hard could it be simply to retrieve and report the value of that one attribute?<\/p>\n<p>Well, as it turns out, surprisingly hard. For one thing, there\u2019s some difficulty regarding the way the last logon date and time are stored in Active Directory. But that can be solved with some clever coding and mathematics. A much bigger stumbling block is this: the lastLogon attribute is not replicated from one domain controller to another. Suppose a new user logs on to domain controller A. You now write a script that requests the last logon time for our new user, and the script happens to connect to domain controller B. Oddly enough, the script will tell you that the user has <i>never<\/i> logged on, even though you know for a fact that the user is logged on right now.<\/p>\n<p><a title=\"ESB\" name=\"ESB\"><\/a>&nbsp;<\/p>\n<\/div>\n<h2><font size=\"4\">Hey, What\u2019s Going on Here?<\/font><\/h2>\n<p>So why is Active Directory lying to you? Well, it\u2019s not really lying, it\u2019s just that domain controller B doesn\u2019t know that the user logged on to domain controller A. Because the lastLogon attribute is not replicated throughout the domain, if our new user has never logged on to domain controller B then domain controller B will have no knowledge of the user\u2019s last logon time. In fact, to determine the last logon time for a user you have to retrieve the lastLogon attribute from every domain controller in the domain and then compare all those values to determine the true last logon time.<\/p>\n<p>Yuck.<\/p>\n<p>The Scripting Guys, who are asked several times a day how to determine the last time a user logged on to a domain (Mom, please, quit asking: you <i>don\u2019t<\/i> want to know!), have just one thing to say: thank goodness for Windows Server 2003. The lastLogon attribute is still present in the Active Directory schema for Windows 2003 and this attribute still isn\u2019t replicated from one domain controller to another. But that\u2019s OK, because there\u2019s a brand-new attribute in the schema: <b>lastLogonTimestamp<\/b>. This attribute also keeps track of the last time a user logged on to the domain, but \u2013 wonder of wonders \u2013 this new attribute <i>is<\/i> replicated from one domain controller to another. Want to know the last time a user logged on? Then just write a script and connect to <i>any<\/i> domain controller; the value will be the same on each one.<\/p>\n<p>It <i>is<\/i> like a miracle, isn\u2019t it?<\/p>\n<p>In this article we\u2019ll show you a script that can return the last logon time for a user in a Windows Server 2003 domain. Before we do this, however, bear in mind that we still face a few complicating factors. For one thing, it\u2019s important to note that the last logon timestamp will typically <i>not<\/i> report the user\u2019s true last logon time. Why not? Well, imagine a group of users who log on and log off several times a day. Each time one of these users logs on that information would have to be replicated throughout the entire domain. That could generate a large amount of replication traffic, and for little purpose: typically you care about only the so-called \u201cstale\u201d accounts,\u201d users who haven\u2019t logged on in the last few weeks. For the most part, you don\u2019t need an up-to-the-minute report on each user\u2019s last logon status. Because of that, the lastLogonTimestamp is replicated only once every 14 days. This helps limit replication traffic, although it also means that the lastLogonTimestamp for any given user could be off by as much as 14 days.<\/p>\n<table id=\"EOC\" class=\"dataTable\" cellSpacing=\"0\" cellPadding=\"0\">\n<blockquote>\n<thead><\/thead>\n<\/blockquote>\n<tbody>\n<tr class=\"record\" vAlign=\"top\">\n<td>\n<blockquote>\n<p class=\"lastInCell\"><b>Note<\/b>. If that actually <i>is<\/i> a problem then you can simply connect to each domain controller and retrieve the value of the lastLogon attribute for the user. The lastLogon attribute isn\u2019t replicated throughout the domain, but it <i>is<\/i> updated on the authenticating domain controller each time a user logs on. But if you\u2019re trying to answer a question like \u201cDo we have any users who haven\u2019t logged on in the past two weeks?\u201d then the lastLogonTimestamp will more than suffice.<\/p>\n<\/blockquote>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<div class=\"dataTableBottomMargin\"><\/div>\n<p>Believe it or not, the fact that the lastLogonTimestamp isn\u2019t 100% accurate actually makes our script a little <i>easier<\/i> to write. As you\u2019ll see, we have to go through some mathematical gyrations in order to convert the lastLogonTimestamp to a date-time value we can make sense of. If we had to adjust for possible time zone differences between our computer and the domain controllers that would make our math even <i>more<\/i> complicated. But we don\u2019t really have to worry about that. After all, we already know \u2013 in advance \u2013 that our last logon time could be off by as much as 14 days. Based on that, there\u2019s no reason to worry about a few hours\u2019 worth of time zone differences.<\/p>\n<p>The other complicating factor, as we hinted at, is this: the lastLogonTimestamp is stored as a 64-bit integer. When you query the lastLogonTimestamp you don\u2019t get back a date-time like May 15, 2005 8:05 AM. Instead, you get back the number of 100-nanosecond intervals that passed between January 1, 1601 and the time the user last logged on. (Come on: we\u2019re not clever enough to make up something like <i>that<\/i>!) Consequently most of our code will be involved in taking that weird 64-bit integer value and converting it to a date and time.<\/p>\n<table id=\"EMD\" class=\"dataTable\" cellSpacing=\"0\" cellPadding=\"0\">\n<blockquote>\n<thead><\/thead>\n<\/blockquote>\n<tbody>\n<tr class=\"record\" vAlign=\"top\">\n<td>\n<blockquote>\n<p class=\"lastInCell\"><b>Note<\/b>. In case you\u2019re wondering, a number of years ago the American National Standards Institute (ANSI) adopted a system of counting days; this system began with December 31, 1600 as Day 0. In turn, that made January 1, 1601 the first \u201cofficial\u201d day in history, with all subsequent dates and times being based on the number of nanoseconds elapsed since the 0 hour on January 1, 1601. (That day was a Monday, by the way.) These so-called ANSI decimal dates were originally designed for use with the COBOL programming language and have continued to be used by Windows and other operating systems.<\/p>\n<\/blockquote>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<div class=\"dataTableBottomMargin\"><\/div>\n<p>Incidentally, did we mention the fact that VBScript can\u2019t actually handle the 64-bit integer returned by lastLogonTimestamp? Well, we should have: 64-bit integers are not supported in VBScript. But at least there is a workaround for this: ADSI\u2019s IADsLargeInterger interface can break this into a pair of 32-bit integers for us, and VBScript can handle those two integers just fine. That means we can still work with the lastLogonTimestamp attribute: we just need to use an additional step, one in which we add the two 32-bit integers to get a single value that VBScript is comfortable with.<\/p>\n<p><\/p>\n<div><font size=\"4\">Don\u2019t Worry: Everything\u2019s Going to Be Just Fine<\/font><\/div>\n<p>Still with us? (We were afraid we\u2019d scared everyone off.) Despite all those dire warnings the script that returns the last logon time for the user really isn\u2019t all that bad. Here, see for yourself:<\/p>\n<pre class=\"codeSample\"><p class=\"MsoNormal\"><span>Set objUser = GetObject(\"LDAP:\/\/cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com\")<\/span><\/p><p class=\"MsoNormal\"><span>Set objLastLogon = objUser.Get(\"lastLogonTimestamp\")<\/span><\/p><p class=\"MsoNormal\"><span>&nbsp;<\/span><\/p><p class=\"MsoNormal\"><span>intLastLogonTime = objLastLogon.HighPart * (2^32) + objLastLogon.LowPart <\/span><\/p><p class=\"MsoNormal\"><span>intLastLogonTime = intLastLogonTime \/ (60 * 10000000)<\/span><\/p><p class=\"MsoNormal\"><span>intLastLogonTime = intLastLogonTime \/ 1440<\/span><\/p><p class=\"MsoNormal\"><span>&nbsp;<\/span><\/p><p class=\"MsoNormal\"><span>Wscript.Echo \"Last logon time: \" &amp; intLastLogonTime + #1\/1\/1601#<\/span><\/p><\/pre>\n<p>The script starts off easy enough: we simply bind to the user account in Active Directory and then use the <b>Get<\/b> method to retrieve the lastLogonTimestamp, storing that value in an IADsLargeInteger object with the object reference objLastLogon.<\/p>\n<table id=\"EHE\" class=\"dataTable\" cellSpacing=\"0\" cellPadding=\"0\">\n<blockquote>\n<thead><\/thead>\n<\/blockquote>\n<tbody>\n<tr class=\"record\" vAlign=\"top\">\n<td>\n<blockquote>\n<p class=\"lastInCell\"><b>Note<\/b>. One of the nice things about ADSI is that, in general, we don\u2019t have to tell it which interface to use; you might notice that we never create an instance of the IADsLargeInteger object. Instead ADSI typically figures that sort of thing out for itself. You can see we also never explicitly tell it that we\u2019re working with a user object. ADSI is smart enough to determine that without any help.<\/p>\n<\/blockquote>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<div class=\"dataTableBottomMargin\"><\/div>\n<p>This is where things get a tad bit hairy. The IADsLargeInteger object has two properties: <b>HighPart<\/b>, which stores the upper 32 bits of our 64-bit integer; and <b>LowPart<\/b>, which stores the lower 32 bits of the integer. To combine those into a single value we use this line of code:<\/p>\n<pre class=\"codeSample\"><p class=\"MsoNormal\"><span>intLastLogonTime = objLastLogon.HighPart * (2^32) + objLastLogon.LowPart<\/span><\/p><\/pre>\n<p>Don\u2019t worry too much about the math; we\u2019re just taking the HighPart times two to the 32<sup>nd<\/sup> power, and then adding the LowPart. Unless you\u2019re a glutton for mathematical punishment just take it on faith that this formula is correct.<\/p>\n<p>Believe it or not, that one line of code actually gives us the last logon time for the user; the only problem is that the last logon time comes back as the number of 100-nanosecond intervals that expired between January 1, 1601 and the user\u2019s last logon. That\u2019s going to be a value similar to this:<\/p>\n<pre class=\"codeSample\"><p class=\"MsoNormal\"><span>1.27588712492538E+17<\/span><\/p><\/pre>\n<p>How\u2026nice\u2026.<\/p>\n<p>What we need to do, obviously, is convert that value to something a little easier to deal with. As everyone knows, there are 1,000,000,000 nanoseconds in a second; therefore, there are 10,000,000 100-nanosecond intervals in a single second (10,000,000 x 100 = 1,000,000,000). We need to know that because \u2013 as we noted earlier \u2013 the lastLogonTimestamp measures time in 100-nanonsecond intervals. If we carry out the math one step further that also means there are 600,000,000 of these 100-nanonsecond intervals in each minute. <\/p>\n<p>Don\u2019t worry about it; we\u2019ll wait until your head stops spinning. Better? OK, let\u2019s proceed. Where are we going with this? Well, armed with this knowledge we can now use <i>this<\/i> line of code to tell us how many <i>minutes<\/i> elapsed between January 1, 1601 and the time the user last logged on (note that we\u2019re taking 60 seconds times the 10,000,000 100-nanosecond intervals in each of those seconds):<\/p>\n<pre class=\"codeSample\"><p class=\"MsoNormal\"><span>intLastLogonTime = intLastLogonTime \/ (60 * 10000000)<\/span><\/p><\/pre>\n<p>And because there are 1,440 minutes in every 24-hour day, this line of code tells us how many days have elapsed:<\/p>\n<pre class=\"codeSample\"><p class=\"MsoNormal\"><span>intLastLogonTime = intLastLogonTime \/ 1440<\/span><\/p><\/pre>\n<p>And, yes, we could have used just one equation rather than two. We just thought that breaking it into two pieces would make it a little easier for you to follow.<\/p>\n<p>As soon as we know the number of days that passed we can add that number to the date January 1, 1601 and generate a date-time value that makes some sense. (For example, suppose we determined 3 days had passed. We\u2019d add 3 to January 1, 1601 and come up with a last logon time of January 4, 1601.) Here\u2019s the code that does this addition:<\/p>\n<pre class=\"codeSample\"><p class=\"MsoNormal\"><span>Wscript.Echo \"Last logon time: \" &amp; intLastLogonTime + #1\/1\/1601#<\/span><\/p><\/pre>\n<p>And here\u2019s an example of the output we get:<\/p>\n<p class=\"MsoNormal\"><span>Last logon time: 4\/25\/2005 2:54:09 PM<\/span><\/p>\n<p>Definitely crazy. But, on the bright side, you <i>can<\/i> connect to any domain controller in the domain and retrieve this information. Like we said, in Windows 2000 you still have to do all this high-falutin\u2019 mathematics; on top of that, though, you also have to connect to each and every domain controller, retrieve the value of the lastLogon attribute, and then compare all those values to determine the last time the user logged on. Compared to that, lastLogonTimestamp represents a huge leap forward.&nbsp;<\/p>\n<p>Now if Microsoft could just do something about those flashing <b>12:00s<\/b> we\u2019d be in business.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>&nbsp; (Note: This blog post appeared originally as an article on the old Microsoft Script Center. It was originally published in December 2005. We are resurrecting it here by popular demand.) Sometimes the things that should be so easy turn out to be incredibly difficult. For example, have you ever tried to get rid of [&hellip;]<\/p>\n","protected":false},"author":595,"featured_media":87096,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[13,3,4,5],"class_list":["post-51463","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-scripting","tag-dates-and-times","tag-scripting-guy","tag-scripting-techniques","tag-vbscript"],"acf":[],"blog_post_summary":"<p>&nbsp; (Note: This blog post appeared originally as an article on the old Microsoft Script Center. It was originally published in December 2005. We are resurrecting it here by popular demand.) Sometimes the things that should be so easy turn out to be incredibly difficult. For example, have you ever tried to get rid of [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/51463","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/users\/595"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/comments?post=51463"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/51463\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media\/87096"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media?parent=51463"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/categories?post=51463"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/tags?post=51463"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}