Summary: Guest blogger, Rohn Edwards, discusses reusing code that obtains the last-modified time stamp from the registry.
Microsoft Scripting Guy, Ed Wilson, is here. Today is the last day of the year, but don’t despair. We are here to share some Windows PowerShell goodies with you. Welcome back Rohn Edwards as our guest blogger this week...
In yesterday’s post, Use PowerShell to Access Registry Last-Modified Time Stamp, we covered how to use Windows PowerShell and P/Invoke to get information from registry keys that isn’t normally available to WMI and the .NET Framework. We covered how to build a C# signature for the RegQueryInfoKey Win32 function, and how to use that signature from Windows PowerShell.
That wasn’t a full solution, though, because using the function directly requires a ton of work. You have to create empty variables and pass a very large number of parameters to the function. Today we’re going to create a reusable tool by wrapping that code in a Windows PowerShell function.
First we have to come up with a name for the function, and to do that properly, we need to know what it’s going to do. I plan to make it take a RegistryKey object (returned from Get-Item or Get-ChildItem) or a string to a registry path (which we would pass to Get-Item to get a RegistryKey object), get the extra key information by using the P/Invoke function, and then add the information as extra NoteProperties on the RegistryKey object. Because we’ll be adding properties, we’re going to name the function Add-RegKeyMember. With these requirements, let’s start with the following function declaration:
#requires -version 3.0
function Add-RegKeyMember {
[CmdletBinding()]
param(
[Parameter(Mandatory, ParameterSetName="ByKey", Position=0, ValueFromPipeline)]
# Registry key object returned from Get-ChildItem or Get-Item
[Microsoft.Win32.RegistryKey] $RegistryKey,
[Parameter(Mandatory, ParameterSetName="ByPath", Position=0)]
# Path to a registry key
[string] $Path
)
begin {
}
process {
}
}
First, we have a #requires statement because we’re using some features that require Windows PowerShell 3.0. Next, we declare the function and its parameters. Right now, there are only two parameters:
$RegistryKey This is an object that is returned from Get-ChildItem or Get-Item when a registry path has been passed.
$Path This is a string that represents a registry path, for example, HKLM:\Software. This path will be passed to Get-Item, and the RegistryKey object that is returned will be used.
Both parameters are mandatory, but they are in different parameter sets. This means that you must have one or the other, but not both. By creating the param() block, we’ve told Windows PowerShell how to do a good bit of parameter validation.
This is going to be an advanced function that will accept pipeline input, so we create begin and process blocks. When the function is used in a pipeline, the begin block will run once, and the process block will run one time for each input that is sent to it. When it’s not used in a pipeline, both blocks will run once.
We’re going to take the Add-Type call from yesterday and put that in the begin block (if the custom type has already been loaded in the current PS session, it won’t be reloaded again). We’ll also use the begin block to store our type in the $RegTools variable:
begin {
# Define the namespace (string array creates nested namespace):
$Namespace = "HeyScriptingGuy"
# Make sure type is loaded (this will only get loaded on first run):
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 { "}" })
"@
# Get a shortcut to the type:
$RegTools = ("{0}.advapi32" -f ($Namespace -join ".")) -as [type]
}
Next, it’s time to work on the process block:
process {
switch ($PSCmdlet.ParameterSetName) {
"ByKey" {
# Already have the key, no more work to be done :)
}
"ByPath" {
# We need a RegistryKey object (Get-Item should return that)
$Item = Get-Item -Path $Path -ErrorAction Stop
# Make sure this is of type [Microsoft.Win32.RegistryKey]
if ($Item -isnot [Microsoft.Win32.RegistryKey]) {
throw "'$Path' is not a path to a registry key!"
}
$RegistryKey = $Item
}
}
# Initialize variables that will be populated:
$ClassLength = 255 # Buffer size (class name is rarely used, and when it is, I've never seen
# it more than 8 characters. Buffer can be increased here, though.
$ClassName = New-Object System.Text.StringBuilder $ClassLength # Will hold the class name
$LastWriteTime = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
switch ($RegTools::RegQueryInfoKey($RegistryKey.Handle,
$ClassName,
[ref] $ClassLength,
$null, # Reserved
[ref] $null, # SubKeyCount
[ref] $null, # MaxSubKeyNameLength
[ref] $null, # MaxClassLength
[ref] $null, # ValueCount
[ref] $null, # MaxValueNameLength
[ref] $null, # MaxValueValueLength
[ref] $null, # SecurityDescriptorSize
[ref] $LastWriteTime
)) {
0 { # Success
# Convert to DateTime object:
$UnsignedLow = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($LastWriteTime.dwLowDateTime), 0)
$UnsignedHigh = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($LastWriteTime.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
$LastWriteTime = [datetime]::FromFileTime($FileTimeInt64)
# Add properties to object and output them to pipeline
$RegistryKey | Add-Member -NotePropertyMembers @{
LastWriteTime = $LastWriteTime
ClassName = $ClassName.ToString()
} -PassThru -Force
}
122 { # ERROR_INSUFFICIENT_BUFFER (0x7a)
throw "Class name buffer too small"
# function could be recalled with a larger buffer, but for
# now, just exit
}
default {
throw "Unknown error encountered (error code $_)"
}
}
}
What the process block does is very simple:
First, it checks to see if a RegistryKey object or a path was passed to the function. If it was a RegistryKey object, it moves along without doing anything else. If it was a path, it calls Get-Item to get a RegistryKey object.
Next, it calls the same code from yesterday’s post. We’re having it get the class name and the last write time (registry keys with class names are very rare, but there are a few). The only thing extra is that the return code from RegQueryInfoKey is being captured in a switch statement. If the result is 0, the call was successful, and the extra info is added to the original RegistryKey object by using Add-Member.
The only other known return code in the switch statement is 122, which means that the class name buffer was too small. Any other errors generate a generic termination error. If you ever come across a non-zero error code, you can easily add it to the switch block to give your user a specific error message.
Here is an example of using it:
That’s it for today. Tomorrow, we’re going to create a proxy function for Get-ChildItem so that it automatically gets this information.
~Rohn
Thank you, Rohn. Happy New Year’s Eve.
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