Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / All-Topics

Get the Last Write Time of a Registry Key in VB.NET using PInvoke

5.00/5 (4 votes)
23 Jul 2013CPOL8 min read 11.2K   377  
Get the Last Write Time of a Registry Key in VB.NET using PInvoke

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.

C++
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.

C++
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 ).

C++
#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:

VB.NET
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 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:

RegLastWriteTime

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.

VB.NET
'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 through the “isabasekey” function (great naming, I know):

VB.NET
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.

VB.NET
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.

VB.NET
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 pointer we got back and a variable to hold the return value for the FILETIME structure.

VB.NET
'create a filetime structure to receive 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 exists.

See if you can find what is wrong here.

RegLastWriteTime2

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.

RegLastWriteTime3

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)