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 in Windows 10.

Given the following, I was changing time zone in Powershell. 

TL;DR
You must restart Powershell to produce the correct calculation, taking into account UTC time zone. If you change it while running current Powershell session it will be wrong.


Most Common Solutions for getting Windows Install Date are All Wrong

Here are most common way to find Windows Install Date; 

Note: I have changed the time zone from the system tray - expecting the correct result to appear. These will not adjust.

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. 



Culprit - Windows 10 resets install date now

In Windows 10, the install date will reset on each major named version release, like the last one - Version 1803 (April 2018 Update).
So what can you do? 
You can get the date the System Volume Information was created for your Windows OS drive. This is sort of a proxy to the when the disk was originally created or in technically jargon partitioned (given, you never have reformatted your drive).

How to get Window OS partitioned create date

In Windows 10, search for “File Explorer Options” -> View tab and set “Show hidden files, folders, and drives”
On your system disk (C:\) where Windows is installed, and look for System Volume Information and hove over it and will show create date. Better still, you can choose "Date created" column to display as in image below.
There are caveats to this method, one is, if you reformat your drive you loose the first time it the initial partition was created. 
Also, if you don't create the partition directly from Windows, this folder doesn't get created with it, automatically. The folder is created the first time Windows identifies the new partition. If you open Windows a few days later after the partition was created, the creation date listed for the System Volume Information folder will be different than the actual partition creation date.
Lastly, if you have only one partition, which you used to install/remove/install different operating systems, Windows will show the date when it created the folder, not when the partition started existing.

What is System Volume Information

System Volume Information is automatically created by Windows when it detects a new partition and it stores important things such as:
  1. System Restore points created by Windows for that partition (if it is set to create them).
  2. Distributed Link Tracking Service databases used to keep information about the creation and movement of linked files across your NTFS partitions.
  3. Indexing Service databases used for making fast file searches.
  4. Shadow Copies created by the Volume Snapshot Service that backups files specified by System Restore or Windows Backup.


Additional Notes - Powershell scripts may fail 

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

Changing timezone while running same Poweshell_ISE instance will fail

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

You must restart Powershell to produce the correct calculation, taking into account UTC time zone. If you change it while running current Powershell session it will be wrong.

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?


 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