Introduction
For a while, I have been trying to find out how to get the last write time of a registry key, but after digging through 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 a lot 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 CodeProject called VB.NET wrappers for much of the Windows API by andrewbb@gmail.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 through the information of the MSDN page, you cannot just use that function by itself. You must first call one of four other functions passing in the Base Key as an 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
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 unmanaged data type of “Long
”, “Platform Invoke Data Types” we see 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 too big or too 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 had forgotten about that and had to add more handling of the input.
If Not input.Contains("\") Then
If isabasekey(input) = True Then
BaseKey = input
ElseIf isabasekey(input) = False Then
tbOutput.Text = "Input not in Correct format"
Exit Sub
Else
End If
ElseIf input.Contains("\") Then
BaseKey = input.Substring(0, input.IndexOf("\"))
trimlength = input.IndexOf("\")
SubKey = input.Substring(trimlength + 1)
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 through 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 through 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
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
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 pointer we got back and a variable to hold the return value for the FILETIME
structure.
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 exists.
See if you can find what is wrong here.
Now look closely, what is wrong with the output here? Compare the two if you need to.
Ok time is 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, it 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 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.
References
Filed under:
CodeProject Tagged:
PInvoke,
Registry,
VB.Net