Summary: Guest blogger, Rohn Edwards, talks about using Windows PowerShell to access the last-modified time stamp in the registry.
Microsoft Scripting Guy, Ed Wilson, is here. Welcome back guest blogger, Rohn Edwards. Rohn is one of the cofounders of the Mississippi PowerShell User Group.
I’m not sure how many of you know this, but the registry stores the last-modified time for every registry key. Unfortunately, it’s not that accessible. Regedit won’t let you get it interactively, although you can get it if you export the key as a text file and then open the file. There are also third-party tools that will let you see this information. Today, I want to show you how to do it with Windows PowerShell.
As far as I can tell, WMI and .NET don’t offer a way to get the last-modified time. The only way that I know to get this information from Windows PowerShell is to use platform invocation services (P/Invoke). For some great information about P/Invoke, see the following Hey, Scripting Guy! Blog posts:
- Use PowerShell and Pinvoke to Remove Stubborn Files
- Use PowerShell to Duplicate Process Tokens via P/Invoke
After a little Internet searching, I came across two Win32 functions that will let you get this information: RegQueryInfoKey and RegEnumKeyEx.
In this post, I’m going to show you how to use RegQueryInfoKey. Hopefully, after reading this, you can create a signature for RegEnumKeyEx on your own, if you would like to use that instead.
If you follow the link to the MSDN page on RegQueryInfoKey, you can find the C++ signature:
Almost any time you hear anything about P/Invoke, you’ll see a reference to pinvoke.net. I’ll agree that this is a wonderful resource for creating C# signatures in Windows PowerShell, but I usually use it only as a starting point, and I make sure that I agree with the types that were chosen for each entry.
In this case, the C++ signature is simple enough to create a C# signature. If you look at the parameter types, you’ll see that there are only four unique types: HKEY, LPTSTR, LPDWORD, and PFILETIME. By using this Type Conversion table (Table 1), you can match the C++ to the following C# types:
HKEY According to the RegOpenKeyEx function documentation, HKEY is a handle.
hKey [in]
A handle to an open registry key. This handle is returned by the RegCreateKeyEx or RegOpenKeyEx function, or it can be one of the following predefined keys:
HKEY_CLASSES_ROOT
HKEY_CURRENT_CONFIG
HKEY_CURRENT_USER
HKEY_LOCAL_MACHINE
HKEY_USERS
The conversion table says that handles are represented by the IntPtr, UIntPtr, or HandleRef types in managed code. At the time of this writing, the pinvoke.net signature uses a UIntPtr. This would work just fine, but we’re going to save ourselves some trouble, and use a different type (more on this in a little bit).
LPTSTR This is handled by String or StringBuilder in .NET.
LPDWORD Use Int32 or UInt32.
PFILETIME This is a FILETIME structure:
typedef struct _FILETIME {
DWORD dwLowDateTime;
DWORD dwHighDateTime;
} FILETIME, *PFILETIME;
It turns out that .NET already has a FILETIME structure that we can use: System.Runtime.InteropServices.ComTypes.FILETIME. We’ll use that combined with a little bit of Windows PowerShell 3.0 script to convert it to a DateTime object.
There’s only one more thing: Remember how I said we’d come back to the hKey handle? Well, when using the Win32 functions to work with the registry, you have to open a handle to a key before you can do anything with it. This requires a call to RegOpenKeyEx, which would require its own C# signature.
After you open a handle and use it, you have to close it with a call to RegCloseKey, which requires yet another signature. The hKey parameter wants a handle to an open key. An IntPtr handles this, but you still need two more functions to get that solution working. Windows PowerShell 4.0 or Windows PowerShell 3.0 provide an open handle to a registry key when you get the key by using Get-ChildItem or Get-Item (you can actually thank the .NET Framework). Take a look at the Handle property on a RegistryKey object:
As far as I can tell, you can use the SafeRegistryHandle in place of an IntPtr in the signature.
Taking all of that into account, our call to Add-Type looks something like the following:
$Namespace = “HeyScriptingGuy”
Add-Type @”
using System;
using System.Text;
using System.Runtime.InteropServices;
$($Namespace | ForEach-Object {
“namespace $_ {”
})
public class advapi32 {
[DllImport(“advapi32.dll”, CharSet = CharSet.Auto)]
public static extern Int32 RegQueryInfoKey(
Microsoft.Win32.SafeHandles.SafeRegistryHandle hKey,
StringBuilder lpClass,
[In, Out] ref UInt32 lpcbClass,
UInt32 lpReserved,
out UInt32 lpcSubKeys,
out UInt32 lpcbMaxSubKeyLen,
out UInt32 lpcbMaxClassLen,
out UInt32 lpcValues,
out UInt32 lpcbMaxValueNameLen,
out UInt32 lpcbMaxValueLen,
out UInt32 lpcbSecurityDescriptor,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
}
$($Namespace | ForEach-Object {
“}”
})
“@
The $Namespace variable exists only so that you can easily change the namespace in one place, and it will be reflected throughout the script. You can assign an array of strings to have nested namespaces, too.
The LP and P prefixes on the parameter names mean that you’re actually passing pointers, so that’s why we’re using the Out keyword on almost all of the parameters (and when we use them, we’ll pass them by reference).
Here’s how to use the function:
# Store the type in a variable:
$RegTools = (“{0}.advapi32” -f ($Namespace -join “.”)) -as [type]
# Get a RegistryKey object (we need the handle)
$RegKey = Get-Item HKLM:\SOFTWARE
# Create any properties that we want returned:
$LastWrite = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
# Call function:
$RegTools::RegQueryInfoKey($RegKey.Handle, $null, [ref] $null, $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $LastWrite)
# Convert to DateTime object:
$UnsignedLow = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($LastWrite.dwLowDateTime), 0)
$UnsignedHigh = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($LastWrite.dwHighDateTime), 0)
# Shift high part so it is most significant 32 bits, then copy low part into 64-bit int:
$FileTimeInt64 = ([Int64] $UnsignedHigh -shl 32) -bor $UnsignedLow
# Create datetime object
[datetime]::FromFileTime($FileTimeInt64)
The call to** RegQueryInfoKey** should return 0 if the call was successful. Passing $null as a parameter means that we aren’t interested in it (notice in the C++ signature that they’re optional). The other parameters aren’t really that useful because they’re almost all available already. But here’s how you would get them (assuming you already ran the previous lines that define $RegTools and $RegKey):
# Create any properties that we want returned:
$SubKeyCount = $ValueCount = $null
$LastWrite = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
$StringBuffer = 255
$ClassName = New-Object System.Text.StringBuilder $StringBuffer
# Call function:
$RegTools::RegQueryInfoKey($RegKey.Handle, $ClassName, [ref] $StringBuffer, $null, [ref] $SubKeyCount, [ref] $null, [ref] $null, [ref] $ValueCount, [ref] $null, [ref] $null, [ref] $null, [ref] $LastWrite)
# Convert to DateTime object:
$UnsignedLow = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($LastWrite.dwLowDateTime), 0)
$UnsignedHigh = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($LastWrite.dwHighDateTime), 0)
# Shift high part so it is most significant 32 bits, then copy low part into 64-bit int:
$FileTimeInt64 = ([Int64] $UnsignedHigh -shl 32) -bor $UnsignedLow
# Return results:
[PSCustomObject] @{
Key = $RegKey.Name
ClassName = $ClassName.ToString()
SubKeyCount = $SubKeyCount
ValueCount = $ValueCount
LastWriteTime = [datetime]::FromFileTime($FileTimeInt64)
}
So now we can manually run this Win32 function when we want to get the last-modified time. The only issue is that using it is a lot of work—look at how many parameters there are!
Thanks to Colin Robertson and Lee Hart, Windows programming writers, who
Thanks to Colin Robertson and Lee Hart, Windows programming writers, who explained the risks of converting the FILETIME structure to UInt64. Please join me tomorrow to see how we can take what we’ve learned today and wrap it up into a much friendlier and easier-to-use reusable function. The complete script for this blog post is available in the Script Center Repository: Get Last Write Time and Class Name of Registry Keys.
~Rohn
Thanks, Rohn, for sharing your expertise.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy
0 comments