Friday, April 7, 2017

How to get the most accurate Windows Install Date (time zone adjusted)

There have been many attempts to get the correct Windows Install Date but 99% of the solutions I have seen are technically the wrong date. It was driving me bonkers.


Most Common Solutions for getting Windows Install Date are All Wrong


Here are most common way to find Windows Install Date;

1. Most popular way, using following command line

1
systeminfo.exe | find /i "Original Install Date" 

    filtering for Original Install Date outputs the wrong date.


2. The following Powershell is a common solution; checking WMI Win32_Registry class for InstallDate but outputs the wrong date.


1
2
(Get-WmiObject Win32_Registry).InstallDate
([WMI]'').ConvertToDateTime((Get-WmiObject Win32_Registry).InstallDate)

3. The following Powershell is a common solution, checking the WMI Win32_OperatingSystem class for InstallDate outputs the wrong date.


1
2
(Get-WmiObject Win32_OperatingSystem).InstallDate
([WMI]'').ConvertToDateTime((Get-WmiObject Win32_OperatingSystem).InstallDate)


4. The following Powershell is common solution checking the Windows Registry key
    HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\InstallDate



1
2
 [TimeZone]::CurrentTimeZone.ToLocalTime([DateTime]'1.1.1970').AddSeconds(
 (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').InstallDate )

or using the C# equivalent snippet


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var key = Microsoft.Win32.RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);

key = key.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", false);
if (key != null)
{
    DateTime startDate = new DateTime(1970, 1, 1, 0, 0, 0);
    object objValue = key.GetValue("InstallDate");
    string stringValue = objValue.ToString();
    Int64 regVal = Convert.ToInt64(stringValue);

    DateTime installDate = startDate.AddSeconds(regVal);
}

will always output the wrong date, even in the C# snippet. 



Reason why these will not work;


1  systeminfo.exe uses UNIX timestamp, but applies your current local time zone to the result when you run it.


2. ConvertToDateTime is not converting the CMI datetime datatype correctly period. See below

3. Ditto.

4. HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\InstallDate contains a UNIX timestamp is correct, it contains no times zone information.


Explanation


Now let me explain; 

The definition of a UNIX timestamp is time zone independent. The UNIX timestamp is defined as the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) the Western European Time (WET) time zone, Thursday, 1 January 1970 and not counting leap seconds.

It does not store the original time zone, in which it was created. Or you could say it always stores the 
Western European Time (WET) timezone. 

In other words, if you have installed you computer in Seattle, WA and moved to New York,NY the HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\InstallDate will give you the date in NY time zone, not in Seattle time zone where Windows was original installed. It's the wrong date, it doesn't store time zone info where the computer was initially installed.


SOLUTION

1) Change you computer time zone (right-click on you clock->Adjust date/time->Adjust time zone) to the time zone where windows was installed, or first turned on.

Then run systeminfo.exe  find /i "Original Install Date" 


2) Get the UNIX Timestamp found in HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\InstallDate for me is 16-Feb-10 6:09:20 AM  
This is the date with UTC+/-0:00, or "(UTC) Coordinated Universal Time" time zone. Add your target time zone to it. 

3) Poweshell is complicated, since (Get-WmiObject Win32_Registry).InstallDate does store the original time zone as the format CIM_DATETIME (yyyymmddHHMMSS.mmmmmmsUUU) would suggest. 

But when you interrogate (Get-WmiObject Win32_Registry).InstallDate in different time zones the value changes? by adding the current time zone to the value. Not what I would expect, this is a datatype and values should not change.

Runs show how ConvertToDateTime does not change over time zones?
This is a bug I found and described in full in my post about this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Computername                                                      : THUNDERBALL-W7U
Windows Install Current Time Zone                                 : (UTC) Coordinated Universal Time
Windows Install Date (ConvertToDateTime)                          : 16-Feb-10 1:09:20 AM
Windows Install Date (MmgmtDTC::ToDateTime)                       : 16-Feb-10 1:09:20 AM
Windows Install Date (ParseExact)                                 : 16-Feb-10 6:09:20 AM
Windows Install Date (ParseExact with TMZ offset 0)               : 16-Feb-10 6:09:20 AM
Windows Install Date (ParseExact BIAS removed, with TMZ offset 0) : 16-Feb-10 6:09:20 AM
Windows Install Date (above & DST adjusted? - False)              : 16-Feb-10 6:09:20 AM
Windows Install Date Raw Value                                    : 20100216060920.000000+000
Age                                                               : 7 years, 2 months, 3 days & 08 hours, 22 mins, 27 secs, 898  msecs

Computername                                                         : THUNDERBALL-W7U
Windows Install Current Time Zone                                    : (UTC-05:00) Eastern Time (US & Canada)
Windows Install Date (ConvertToDateTime)                             : 16-Feb-10 1:09:20 AM
Windows Install Date (MmgmtDTC::ToDateTime)                          : 16-Feb-10 1:09:20 AM
Windows Install Date (ParseExact)                                    : 16-Feb-10 1:09:20 AM
Windows Install Date (ParseExact with TMZ offset -300)               : 15-Feb-10 8:09:20 PM
Windows Install Date (ParseExact BIAS removed, with TMZ offset -300) : 16-Feb-10 1:09:20 AM
Windows Install Date (above & DST adjusted? - False)                 : 16-Feb-10 1:09:20 AM
Windows Install Date Raw Value                                       : 20100216010920.000000-300
Age                                                                  : 7 years, 2 months, 3 days & 13 hours, 21 mins, 55 secs, 180  msecs

Computername                                                         : THUNDERBALL-W7U
Windows Install Current Time Zone                                    : (UTC-08:00) Pacific Time (US & Canada)
Windows Install Date (ConvertToDateTime)                             : 16-Feb-10 1:09:20 AM
Windows Install Date (MmgmtDTC::ToDateTime)                          : 16-Feb-10 1:09:20 AM
Windows Install Date (ParseExact)                                    : 15-Feb-10 10:09:20 PM
Windows Install Date (ParseExact with TMZ offset -480)               : 15-Feb-10 2:09:20 PM
Windows Install Date (ParseExact BIAS removed, with TMZ offset -480) : 15-Feb-10 10:09:20 PM
Windows Install Date (above & DST adjusted? - False)                 : 15-Feb-10 10:09:20 PM
Windows Install Date Raw Value                                       : 20100215220920.000000-480
Age                                                                  : 7 years, 2 months, 3 days & 16 hours, 45 mins, 13 secs, 146  msecs


Poweshell Script for above, download here.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#requires -version 2.0
 
# -----------------------------------------------------------------------------
# Script: Get-WindowsInstallDateTMZAdjustedVersion3.ps1
# Version: 1.2017.07.08
# Author: Mark Pahulje
#    http://metadataconsulting.blogspot.com/
# Date: 08-Apr-2017
# Keywords: Registry, WMI, Windows Install Date
# Comments:
#
# "Those who forget to script are doomed to repeat their work."
#
#  ****************************************************************
#  * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
#  * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
#  * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
#  * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
#  ****************************************************************
# -----------------------------------------------------------------------------
 
Function Get-WindowsInstallDateTMZAdjusted {
 
<#
.SYNOPSIS
Get accurate Windows Install Date Time Zone (TMZ) Adjusted
.DESCRIPTION
This command uses WMI to retrieve install date of the Windows registry, which is equivallent to Windows Install Date that is time zone adjusted.
Win32_Registry InstallDate is stored as WMI Datetime a datatype which really stores a 
Microsoft UTC format (yyyymmddHHMMSS.xxxxxx±UUU) 
where crucially 
±UUU is number of minutes different local time zone (at the time Windows was installed) from Greenwich Mean Time and 
xxxxxx is milliseconds.
This scripts add the time zone offset (±UUU) to get the real correct Windows install date. 
This version has no provision for alternate credentials.
.MUNCHIES
99% of all scripts out there do not account for this! 
.OPTIONAL PARAMETER Computername
The name of a computer to query. The default is the local host.
.EXAMPLE
PS C:\> Get-WindowsInstallDateTMZAdjusted 

Computername                                                         : THUNDERBALL-W7U
Windows Install Current Time Zone                                    : (UTC-05:00) Eastern Time (US & Canada)
Windows Install Date (ConvertToDateTime)                             : 16-Feb-10 1:09:20 AM
Windows Install Date (MmgmtDTC::ToDateTime)                          : 16-Feb-10 1:09:20 AM
Windows Install Date (ParseExact)                                    : 16-Feb-10 1:09:20 AM
Windows Install Date (ParseExact with TMZ offset -300)               : 15-Feb-10 8:09:20 PM
Windows Install Date (ParseExact BIAS removed, with TMZ offset -300) : 16-Feb-10 1:09:20 AM
Windows Install Date (above & DST adjusted? - False)                 : 16-Feb-10 1:09:20 AM
Windows Install Date Raw Value                                       : 20100216010920.000000-300
Age                                                                  : 7 years, 2 months, 3 days & 13 hours, 21 mins, 55 secs, 180  msecs

 
Return registry usage information for the local host.
.EXAMPLE
PS C:\> Get-Content Computers.txt | Get-WindowsInstallDateTMZAdjusted | Export-CSV c:\work\ListofComputerswithInstallDates.csv
Retrieve registry install date (Windows install date) for all the computers in the text file, computers.txt. The results
are exported to a CSV file.
.NOTES
NAME        :  Get-WindowsInstallDateTMZAdjusted
VERSION     :  1.2017.04.08  
LAST UPDATED:  08-Apr-2017
AUTHOR      :  Martin Kohonen
.LINK
http://metadataconsulting.blogspot.ca/2017/04/How-to-get-the-most-accurate-Windows-Install-Date-time-zone-adjusted.html
.LINK
Get-WindowsInstallDateTMZAdjusted
.INPUTS
String
.OUTPUTS
A formatted table
#>
 
[cmdletbinding()]
 
Param (
[Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[ValidateNotNullorEmpty()]
[String[]]$Computername=$env:Computername
)
 
Begin {
    Write-Verbose "Starting $($myinvocation.mycommand)"
} #Begin
 
Process {
    Foreach ($computer in $computername) {
        Write-Verbose "Processing $computer"
        Try {
         #retrieve registry information via WMI
         $data=Get-WmiObject -Class Win32_Registry -ComputerName $computer -ErrorAction Stop
         
         
         $installdatestring = ($data).InstallDate
         
         $timeZone=Get-WmiObject -Class Win32_TimeZone -ComputerName $computer -ErrorAction Stop
         #UTC = local time - bias #https://msdn.microsoft.com/en-us/library/aa394498%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
         
         
         #$timeZone=[TimeZoneInfo]::Local.DisplayName - this does not work when you change timezones

         # converts yyyymmddHHMMSS to datetime by default
         # $installdatetime = ([WMI]'').ConvertToDateTime($installdatestring);  not trusting this function add +5:00 for some reason
         
         $yyyymmddHHMMSS = $installdatestring.Split('.')[0];
         
         #[int]$year =     [convert]::ToInt32($yyyymmddHHMMSS.Substring(0, 4),10);
         #[int]$month =    [convert]::ToInt32($yyyymmddHHMMSS.Substring(4, 2),10);
         #[int]$day =      [convert]::ToInt32($yyyymmddHHMMSS.Substring(4 + 2, 2),10);
         #[int]$hours =    [convert]::ToInt32($yyyymmddHHMMSS.Substring(4 + 2 + 2, 2),10);
         #[int]$mins =     [convert]::ToInt32($yyyymmddHHMMSS.Substring(4 + 2 + 2 + 2),10);
         #[int]$secounds = [convert]::ToInt32($yyyymmddHHMMSS.Substring(4 + 2 + 2 + 2 + 2, 4),10);

         #http://dusan.kuzmanovic.net/2012/05/07/powershell-parsing-date-and-time/         
         
         $template = 'yyyyMMddHHmmss'
         $installdatetime = [DateTime]::ParseExact($yyyymmddHHMMSS, $template, $null) 
         
         $xxxxxxsUUU = $installdatestring.Split('.')[1];     

         [long]$microsecs = [convert]::ToInt64($xxxxxxsUUU.Substring(0, 6),10); 
         [int]$UTCoffsetinMins = [convert]::ToInt32($xxxxxxsUUU.Substring(6, 4),10);

         [long]$millisecs =  $microsecs*0.001  #https://msdn.microsoft.com/en-us/library/aa387237(v=vs.85).aspx
                         
         $installdatetime = $installdatetime.AddMilliseconds($millisecs)  #block output use assingment 
         $installdatetimePE = $installdatetime

         #timezone added from InstallDate
         $installdatetimePETMZ = $installdatetimePE.AddMinutes($UTCoffsetinMins)

       
         #[TimeZoneInfo]::Local.BaseUtcOffset # does not work when you swith timezones! 
         #[TimeZoneInfo]::Local.SupportsDaylightSavingTime
          
         #UTC = local time - bias #https://msdn.microsoft.com/en-us/library/aa394498%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
         $installdatetimeTMZ =  $installdatetime.AddMinutes(($timeZone.Bias*-1)); #remove bias
                  
         $isDST = $installdatetimeTMZ.IsDaylightSavingTime()
         if ($isDST){
            $installdatetimeTMZ =  $installdatetimeTMZ.AddMinutes(($timeZone.DaylightBias*-1)); #remove daylight saving bias, this was in the past not today, but doable
         }                 
         
         $installdatetimeTMZ =  $installdatetimeTMZ.AddMinutes($UTCoffsetinMins); #add UTC Offset
         
         #direct call to .NET library
         [datetime]$InstallDateMngmtDTC = [System.Management.ManagementDateTimeConverter]::ToDateTime($installdatestring)
         
         #add a member to iterate over in our table  - re http://windowsitpro.com/powershell/powershell-basics-custom-objects - great tip
         Add-Member -InputObject $data -MemberType NoteProperty `
        -Name InstallDateRAW `
        -Value  $installdatetime
       
         Add-Member -InputObject $data -MemberType NoteProperty `
        -Name InstallDateMngmtDTC `
        -Value $InstallDateMngmtDTC
       
         Add-Member -InputObject $data -MemberType NoteProperty `
        -Name InstallDatePE `
        -Value $installdatetimePE
        
         Add-Member -InputObject $data -MemberType NoteProperty `
        -Name InstallDatePETMZ `
        -Value $installdatetimePETMZ
        

         Add-Member -InputObject $data -MemberType NoteProperty `
        -Name InstallDateTMZ `
        -Value $installdatetimeTMZ

         Add-Member -InputObject $data -MemberType NoteProperty `
        -Name TimeZone `
        -Value $timeZone.Caption
       

         #Format the results and write an object to the pipeline         
         $data | Select-Object -Property @{Name="Computername";Expression={$_.__SERVER}},
         @{Name="Windows Install Current Time Zone ";Expression={ $_.TimeZone }},
         @{Name="Windows Install Date (ConvertToDateTime)";Expression={ $_.ConvertToDateTime($_.InstallDate) }},
         @{Name="Windows Install Date (MmgmtDTC::ToDateTime)";Expression={ $_.InstallDateMngmtDTC }},
         @{Name="Windows Install Date (ParseExact)";Expression={ $_.InstallDateRAW }},
         @{Name="Windows Install Date (ParseExact with TMZ offset $UTCoffsetinMins)";Expression={ $_.InstallDatePETMZ }},
         @{Name="Windows Install Date (ParseExact BIAS removed, with TMZ offset $UTCoffsetinMins)";Expression={ $_.InstallDatePE }},
         @{Name="Windows Install Date (above & DST adjusted? - $isDST)";Expression={ $_.InstallDateTMZ }},
         @{Name="Windows Install Date Raw Value";Expression={$_.InstallDate}},
         @{Name="Age";Expression={"{1:N0} years, {2:N0} months, {3:N0} days & {0:hh} hours, {0:mm} mins, {0:ss} secs, {0:fff}  msecs" -f (  ((Get-Date) - ($_.InstallDateTMZ)), [Math]::Truncate( (((Get-Date) - ($_.InstallDateTMZ)).Days/365.2425) ), [Math]::Truncate( ((((Get-Date) - ($_.InstallDateTMZ)).Days%365.2425)/30.436875)),  ((((Get-Date) - ($_.InstallDateTMZ)).Days%30.436875))   ) }}
         
        } #try
       
        Catch {
            Write-Warning "Failed to retrieve registry information from $($Computer.ToUpper())"
            Write-Warning $_.Exception.Message
        }#Catch
   
    }#foreach $computer
} #Process
 
End {
    Write-Verbose "Ending $($myinvocation.mycommand)"
} #End
 
} #end function

Get-WindowsInstallDateTMZAdjusted

No comments:

Post a Comment