For a while I have been trying to find out how to get the last write time of a registry key but after digging thru the object browser in VS 2008 and .Net Framework 3.5 there appears to be no built in function to get the last write time for the registry.
After allot of internet searching using various search terms I finally decided to try to use Platform Invoke (PInvoke). I understand what it does but have never been able to get anything to work that wasn’t just a matter of copy paste and maybe some tweaking of the code to get it to do what I wanted.
The hardest part for me is the data types in unmanaged C/ C++ and in VB.Net and the Marshaling of them between managed and unmanaged. It can get very confusing on what they are wanting or how to do it.
A big help to get this project off the ground quickly was a Win API wrapper class on Code Project called VB.NET wrappers for much of the Windows API by email@example.com
I was able to just copy/paste the parts that I needed to a Class and then try to figure out the one function I needed that was not in the download.
The C++ Function I found to get the last write time is “ RegQueryInfoKey ” on MSDN.
LONG WINAPI RegQueryInfoKey( _In_ HKEY hKey, _Out_opt_ LPTSTR lpClass, _Inout_opt_ LPDWORD lpcClass, _Reserved_ LPDWORD lpReserved, _Out_opt_ LPDWORD lpcSubKeys, _Out_opt_ LPDWORD lpcMaxSubKeyLen, _Out_opt_ LPDWORD lpcMaxClassLen, _Out_opt_ LPDWORD lpcValues, _Out_opt_ LPDWORD lpcMaxValueNameLen, _Out_opt_ LPDWORD lpcMaxValueLen, _Out_opt_ LPDWORD lpcbSecurityDescriptor, _Out_opt_ PFILETIME lpftLastWriteTime );
Reading thru the information of the MSDN page, you can not just use that function by itself you must first call one of four other another functions passing in the Base Key as a integer value and the sub key as a string, if it is not just a base key, and then return a handle to the key.
Since all I wanted to do was read an existing key and not set the value or write to a new one I used “RegOpenKeyEx” on MSDN.
LONG WINAPI RegOpenKeyEx( _In_ HKEY hKey, _In_opt_ LPCTSTR lpSubKey, _Reserved_ DWORD ulOptions, _In_ REGSAM samDesired, _Out_ PHKEY phkResult );
The input parameters of “RegOpenKeyEx” are the Integer value of the Base Registry key (below) as found in WINDDK in the file WINREG.h, Optional sub key as a string and the integer value of the “Registry Key Security and Access Rights” of KEY_QUERY_VALUE (0x0001) Required to query the values of a registry key (see “RegQueryInfoKey”On MSDN ).
#define HKEY_CLASSES_ROOT (( HKEY ) (ULONG_PTR)((LONG)0x80000000) ) #define HKEY_CURRENT_USER (( HKEY ) (ULONG_PTR)((LONG)0x80000001) ) #define HKEY_LOCAL_MACHINE (( HKEY ) (ULONG_PTR)((LONG)0x80000002) ) #define HKEY_USERS (( HKEY ) (ULONG_PTR)((LONG)0x80000003) ) #define HKEY_PERFORMANCE_DATA (( HKEY ) (ULONG_PTR)((LONG)0x80000004) ) #define HKEY_PERFORMANCE_TEXT (( HKEY ) (ULONG_PTR)((LONG)0x80000050) ) #define HKEY_PERFORMANCE_NLSTEXT (( HKEY ) (ULONG_PTR)((LONG)0x80000060) ) #if(WINVER >= 0x0400) #define HKEY_CURRENT_CONFIG (( HKEY ) (ULONG_PTR)((LONG)0x80000005) ) #define HKEY_DYN_DATA (( HKEY ) (ULONG_PTR)((LONG)0x80000006) ) #define HKEY_CURRENT_USER_LOCAL_SETTINGS (( HKEY ) (ULONG_PTR)((LONG)0x80000007) )
You can add the list above as a public Enum in your API class I’m just using the 4 basic ones as supplied with the API wrapper class.
In VB.Net it might look like.
Public Enum Hives As Integer HKEY_CLASSES_ROOT = &H80000000 HKEY_CURRENT_USER = &H80000001 HKEY_LOCAL_MACHINE = &H80000002 HKEY_USERS = &H80000003 HKEY_PERFORMANCE_DATA = &H80000004 HKEY_PERFORMANCE_TEXT = &H80000050 HKEY_PERFORMANCE_NLSTEXT = &H80000060 '#if(WINVER >= 0x0400) HKEY_CURRENT_CONFIG = &H80000005 HKEY_DYN_DATA = &H80000006 HKEY_CURRENT_USER_LOCAL_SETTINGS = &H80000007 End Enum
Next use the handle returned from “RegOpenKeyEx” as input to “RegQueryInfoKey” and return just the parts of the function you want. Since all I want here is “Last write time” then all I needed was the pointer for input and a output variable of type “FILETIME”.
When looking at the the the unmanaged data type of “Long” , “Platform Invoke Data Types” we see that that data type in managed (CLR) is System.Int32 and on Data Type Summary (Visual Basic) we see that the System.Int32 is Integer in VB.Net.
So the “Public Enum Hives As Integer” should be the correct data type.
Another thing I noticed, was when I tested changing from Integer to Uint32 which is what I thought it might be till I looked it up, I ended up with some strange errors, one was a math overflow and another was something about a number being to big or to small for the data type. After changing back to type Integer the problems were gone.
I was testing using a long set of sub keys and then reducing by 1 level till I got to the base key, it was there that the errors showed up until I changed the data type back to Integer.
Here is what the current test project looks like.
This version inputs a registry path to a key and then outputs what you see above.
I split the base key from the sub key section, Output what the value of the pointer was for the key returned by “RegOpenKeyEx”, returned the HResult for both API calls, if it returns anything but 0(Zero) for the Result then there is an error.
Next I have the returned File time listed as the high and low parts. Finally I output the converted “FILETIME” structure to date time using a API converter found in the API Wrapper class.
I was wanting to do some kind of validation for the input so I created two crazy functions to parse the input.
The first thing I always test for is if the input text box has anything in it or not.
Next is when it gets strange, I was first testing with just longs paths that had several “\” in them so I was using that for my split character but when I tried to test just a base key I forgotten about that and had to add more handling of the input.
'Test first if we are dealing with a base key or malformed entry If Not input.Contains("\") Then If isabasekey(input) = True Then ' just do the work on the base key BaseKey = input ElseIf isabasekey(input) = False Then 'this is a messed up input tbOutput.Text = "Input not in Correct format" Exit Sub Else End If 'if the input has a base key and subkeys then parse the string to get the base key and the subkey values ElseIf input.Contains("\") Then BaseKey = input.Substring(0, input.IndexOf("\")) 'Trims everything to the right of and including the first "\" trimlength = input.IndexOf("\") 'The string index location of "\" SubKey = input.Substring(trimlength + 1) 'Trims everything to the Left of and including "\" End If
As you can see I have lots of comments in the code.
The First part takes the input string and if it contains a “\” then just proceeds on to the split function in the ElseIf statement , if it doesn't contain a “\” then is it a Base key or just a malformed entry. So it gets put thru the “isabasekey” function (great naming, I know)
Private Function isabasekey(ByVal akey As String) As Boolean Dim key As String = akey.ToUpper If _ key = "HKLM" OrElse _ key = "HKEY_LOCAL_MACHINE" OrElse _ key = "HKCU" OrElse _ key = "HKEY_CURRENT_USER" OrElse _ key = "HKCR" OrElse _ key = "HKEY_CLASSES_ROOT" OrElse _ key = "HKU" OrElse _ key = "HKEY_USERS" Then Return True Else Return False End If End Function
Here I simply take the input value and change it to upper case then compare the input to the allowed values and if it matches one it returns true if not then false.
Next after returning from the function if it is a valid allowed base key then just set the variable, BaseKey = input value. if it returns false then set the output text box text to the error message and then exit the sub, no reason to continue.
Next I take the “BaseKey” value and run it thru a function to validate it and return the integer value of the base key to use in the first API call.
Private Function ParseInput(ByVal key As String) As Integer Dim op As String If key.StartsWith("HKLM") OrElse key.StartsWith("HKEY_LOCAL_MACHINE") Then op = Hives.HKEY_LOCAL_MACHINE ElseIf key.StartsWith("HKCU") OrElse key.StartsWith("HKEY_CURRENT_USER") Then op = Hives.HKEY_CURRENT_USER ElseIf key.StartsWith("HKCR") OrElse key.StartsWith("HKEY_CLASSES_ROOT") Then op = Hives.HKEY_CLASSES_ROOT ElseIf key.StartsWith("HKU") OrElse key.StartsWith("HKEY_USERS") Then op = Hives.HKEY_USERS Else op = -1 '"Input Value Not Supported or Not in correct format" End If Return op End Function
Next we can finally make our first API call passing in the Integer value we just got for the BaseKey, the Sub Key string part that we split above if we are checking that along with the base key,then a variable to hold the return value for the pointer to the key.
Dim regkeyptr As Integer 'Here is our first API call to RegOpenKeyEx inputing the base key value and the subkeys value if any. 'openregkeyResult is th Hresult of the call. regkeyptr is the returned intptr for the input key. 'The fourth parameter is set at a default Constant value. Dim openregkeyResult As Integer = RegOpenKeyEx(BaseKeyValue, SubKey, 0, KEY_QUERY_VALUE, regkeyptr)
The third parameter is Reserved and the MSDN site says it must be set to 0(zero) the call also worked with it set as Nothing, but I changed it once rereading the description.
Next we can make our call to “RegQueryInfoKey” imputing the the pointer we got back and and a variable to hold the return value for the FILETIME structure.
'create a filetime structure to recieve the returned time Dim lpftLastWriteTime As System.Runtime.InteropServices.ComTypes.FILETIME Dim returnvalue As Integer returnvalue = RegQueryInfoKey( _ regkeyptr, _ Nothing, _ Nothing, _ Nothing, _ Nothing, _ Nothing, _ Nothing, _ Nothing, _ Nothing, _ Nothing, _ Nothing, _ lpftLastWriteTime)
As you can see above all the parameters we don’t need are set to nothing. Just the input of the pointer and output of the FILETIME have values. The “returnvalue” is the HResult and should be 0(zero) or else something went wrong.
Next we use a convert function supplied by the API class to convert this to a date time, then output and we are done.
While doing more testing I realized that I had forgotten to add one other input validation at the start, you always test in some way the users input path to see if it exist.
See if you can see what is wrong here.
Now look close, what is wrong with the output here ? Compare the two if you need to.
Ok times up.
The first thing you may notice is the last letter is missing from the path.
The next thing you may notice is that the returned pointer from the first API call is 0(zero) ,this return should be non 0 ,bad sign.
Next the result from the open key API is 2 in decimal System Error Codes (0-499) which translates to “ERROR_FILE_NOT_FOUND 2 (0x2) The system cannot find the file specified” which is kind of strange for this one. Next we see the result from the second API call as 6 in decimal. “ERROR_INVALID_HANDLE 6 (0x6) The handle is invalid.” now that one makes more sense.
The last thing you may notice is the output date/time.
The FILETIME structure page tells us.
”Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC).” But looking at that is shows the low and high times as 0(zero) and the output is off 6 hours from what it should be. To answer that I just happen to be in the time zone –6 UTC but my current time offset is –5 for daylight saving time
Now that API class had two conversion functions, the one above should be for Local time without adjusting for Daylight Saving Time. The other returns the UTC time.
Now after changing which date/time converter was used all of the output is the same except the date/time output which now aligns with the date/time it is supposed to be for UTC time if the FILETIME value is not added to it.
Also if you input a path that contains a “Value Name” at the end it returns the same as if it was a bad path. So it only works with valid key paths.
One more thing. After we get done using the ‘RegOpenKeyEx” function we need to close the key so we use the “RegCloseKey” function supplied by the API wrapper class.
Just pass in the handle returned from ‘RegOpenKeyEx” and that’s it.
So after a little more thought, time, and effort I should be able to turn this into a wrapper class to be able to drop into any project that requires the the ability to return the last write time of a registry key. Just input a valid path and get back a Date/Time and hide the rest of the nuts and bolts like most .Net classes do.
I hope someone found this useful.
System Error Codes
Platform Invoke Data Types
Registry Key Security and Access Rights
Data Type Summary (Visual Basic)
VB.NET wrappers for much of the Windows