Archive for the ‘Scripting’ Category

In the last post, I ran through a function to convert an icon file into a here string for use in a script..  This lets you create in memory icons suitable for use in a GUI created through Powershell. In this post, I’m going to show the function to take that Base64 string, and turn it into an icon in memory, that you can then use as a graphical element in your form. The primary driver for doing this is so that you can distribute your script, with graphical elements intact and have the script be the only file sent.

And to give proper credit, most of this function came from https://adminscache.wordpress.com/2013/06/09/base64-encoding-of-images/.  He had a different purpose for this capability, but it paid off for me.

This function is called Get-IconFromBase64.  The first few lines are pretty much boilerplate:


function Get-IconFromBase64{
param(
[Parameter(Mandatory = $true,HelpMessage='The string to be converted to an in-memory icon', ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)]
[string]$Base64String
)

We’re setting up the function, and asking for a single parameter which is the icon as a Base64 encoded string as covered in part 1 of this post.

Now for the meat & potatoes 🙂  We add the System.Drawing assembly.  Way back in the day, most scripters used the [reflection.assembly]::LoadWithPartialName() construct, since none of us wanted to look up or remember the fully qualified path with the tokens etc. for a formal load.  Microsoft has been threatening to deprecated this functionality, but fortunately, you can use Add-Type to load the assembly and it works perfectly.

Add-Type -AssemblyName System.Drawing

This next line is an interesting combination – we use the built in [system.convert] assembly to read in the Base64 string and cast that as a memory stream.

$iconStream = [System.IO.MemoryStream][System.Convert]::FromBase64String($Base64String)

Now, we take that memory stream, and convert it to a bitmap.  The System.Drawing assembly provides a function to read in the image from a memory stream, and we cast it as a bitmap.

$iconBMP = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($iconStream)

The next step is to get the icon from the bitmap.  Again, we turn back to the System.Drawing assembly, and we use the GetHIcon function from System.Drawing.Bitmap to get a handle to the image.  https://msdn.microsoft.com/en-us/library/system.drawing.bitmap.gethicon(v=vs.110).aspx

$iconHandle = $iconBMP.GetHicon()

And as our second to last step, we take that handle and turn it into an icon.

$icon = [System.Drawing.Icon]::FromHandle($iconHandle)

And now we have an in-memory icon that is completely usable as the icon for buttons, as an image etc.  Here is a small form with a button that uses the drive icon from the first post.  (It doesnt’ *do* anything.. it just shows off the icon).  One important limitation – this function only pulls the 32 x 32 pixel icon.  I am sure there are other ways to pull larger icons, and I am looking into it..

But, anywhere you can use an icon in the script, you can use this technique.

Click here to get a pdf version of the demo script.. The script is long because of the Base64 string, and WordPress didn’t seem happy with it, so the PDF, and for some bizarre reason, WordPress treats text files as a security risk.

Here is a picture of what the sample form looks like using the icon in 2 places.

Show-SampleForm

David F.

One of the major things that occurs with GUI scripts is the generic form icons that come with the host, and of course there are buttons that almost always have a plain text label, and other controls that are just plain text labels (like a treeview).
However, one of the things that can be done is to use the .Image property of your controls, so that you can get an external icon.  I’ve seen some done like this, and along with the .ps1, they include a graphics file (icon, bitmap, etc.).. you’ll typically see some snippet like this:

 $button = New-Object -Type System.Windows.Forms.Button
 $button.Top = 10
 $button.Left = 10
 $button.Width = 35
 $button.Height = 35
 $button.Image = '.\MyButtonIcon.ico'
 

But, this also requires that the icon file be distributed with the script, and always put in the correct location etc.. This makes portability a little tougher..

To that end, I did some poking around and found I stumbled across this article [https://adminscache.wordpress.com/2013/06/09/base64-encoding-of-images] to embed some icons in a system tray notification.  With this, I was able to create a pair of functions that allow me to get the icons into Base64 string, and then read them back into memory and use them as the Image part of any control.  The two functions are Convert-FileToBase64 which can be used for pretty much any purpose, and Get-IconFromBase64 which reads in the Base64 string and converts it to an in memory icon.

Part 1 covers the Convert-FileToBase64 function.  This is surprisingly simple, and since the output is plain text, it can be used for any number of purposes.

As with any properly defined function, the first thing to do is specify the parameters, most of this is very straightforward

param(
   [parameter(mandatory = $true, ValueFromPipelineByPropertyName = $true)]
   [ValidateScript({Test-Path -path $_})]
   [string]$FilePath
)

We start with the typical param( ) block.. Normally, I would start with the [cmdletbinding()]  to provide the common parameters.  But this script is very simple, and does not really require the -verbose, -debug or other such parameters.  The items mentioned here in the param( ) block are very straightforward.  If there are questions, please post them in the comments section, and I’ll attempt to answer them there.

The next line is the primary work of the function.. it reads in the file as a series of bytes (no interpretation of the data) and uses the built in .Net convert function to change these into Base64 encoding.

$EncodedFile = [convert]::ToBase64String( (get-content -Path $FilePath -Encoding Byte) )

The “problem” with this is that the output is a long string, that is unbroken, and depending on the circumstances can be very unwieldy.  To that end, I added a line to break this up into the typical 64bit lines like you would see in a certificate file.

$EncodedFile = $EncodedFile -replace '.{64}', "$&`r`n"

The -replace function uses the ‘.’ to match any character, and the {64} tells it to grab 64 characters.  The replace uses $& which says “give me the entire match” (all 64 characters) and then it adds the carriage return, line feed (`r`n).

The last line of the function just returns the $EncodedFile

return $EncodedFile

I grabbed an icon from shell32.dll using Nirsoft’s IconsExt.exe
shell32_10

Now, using the script, I am able to grab the full Base64 string.. (not completely posted here for length)

Convert-FileToBase64 -FilePath c:\temp\icons\shell32_10.ico

AAABAAkAMDAQAAEABABoBgAAlgAAACAgEAABAAQA6AIAAP4GAAAQEBAAAQAEACgB
AADmCQAAMDAAAAEACACoDgAADgsAACAgAAABAAgAqAgAALYZAAAQEAAAAQAIAGgF
AABeIgAAMDAAAAEAIACoJQAAxicAACAgAAABACAAqBAAAG5NAAAQEAAAAQAgAGgE
AAAWXgAAKAAAADAAAABgAAAAAQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
...

Now, in order to use this icon in your GUI script, simply capture the full output (copy and paste, redirect into clip.exe etc.) into your script as a here string.

For example:

$DiskDriveIcon = @'
AAABAAkAMDAQAAEABABoBgAAlgAAACAgEAABAAQA6AIAAP4GAAAQEBAAAQAEACgB
AADmCQAAMDAAAAEACACoDgAADgsAACAgAAABAAgAqAgAALYZAAAQEAAAAQAIAGgF
AABeIgAAMDAAAAEAIACoJQAAxicAACAgAAABACAAqBAAAG5NAAAQEAAAAQAgAGgE
AAAWXgAAKAAAADAAAABgAAAAAQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
...
'@

The next article will show it in use..

[edit: I forgot the actual script!]


function Convert-FileToBase64
{
<#
.SYNOPSIS
Reads in a binary file and converts it to a Base64 string

.DESCRIPTION
Converts the binary file to a Base64 string, and breaks it up into 64 character lines

.PARAMETER FilePath
The path to the file to be converted

.EXAMPLE
PS C:\> Convert-FileToBase64 -FilePath C:\Temp\MySampleIcon.ico

AAABAAEAICAAAAEACACoCAAAFgAAACgAAAAgAAAAQAAAAAEACAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAADx8f8AoqL/AHx8/wCZmf8A7e3/ALGx/wANDf8AAAD/AAkJ
/wCnp/8AkpL/AAsL/wA0NP8ADw//ADAw/wDh4f8A39//AMzM/wDIyP8A3d3/AEBA
/wA2Nv8A////AP7+/wCpqf8AKCj/ABgY/wCTk/8A+fn/AElJ/wA3N/8Atrb/ABoa
/wAMDP8Al5f/APr6/wDS0v8AKSn/ABIS/wClpf8A4+P/AC8v/wABAf8Ar6//APz8
/wDs7P8AT0//ABsb/wC4uP8AXFz/AAIC/wADA/8AHBz/ALOz/wD9/f8Ad3f/AAcH
/wBUVP8AkZH/ACcn/wB+fv8AUVH/APLy/wCdnf8AEBD/ABUV/wCrq/8AOTn/ANra
/wD39/8Aj4//AC4u/wDT0/8AsrL/ACws/wDY2P8ABAT/ANXV/wDPz/8AFBT/APj4
/wB2dv8AZmb/AGpq/wATE/8AdXX/AAYG/wArK/8ASEj/AJiY/wD7+/8A8PD/AHFx
/wBWVv8Ag4P/AIaG/wB6ev8AIiL/ANDQ/wDq6v8AcHD/ADw8/wCNjf8A9PT/AOjo
/wDZ2f8APj7/AKio/wAAAAAAw8P/AKys/wBeXskABwf1AAQE9wBPT8cAv7//AIuL
rwAKCvAABgbzAHZ2rADv7/8AhIT/AGlp/wA5Od4ABAT5AAMD+gAxMd0A6en/AOTk
/wAtLf8AJCT/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbICBBwcHBwcHBwcHBwcHBwcH
BwcHBwcHBwcHBweCRGxseHl6enp6enp6enp7fAcHBwd9fnp6enp6enp6ejx/bGxs
bGxsbGxsbGxsbHR1BwcHB3Z3bGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsdHUHBwcH
dndsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx0dQcHBwd2d2xsbGxsbGxsbGxsbGxs
bGxsbGxsbGxsbHR1BwcHB3Z3bGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsdHUHBwcH
dndsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx0dQcHBwd2d2xsbGxsbGxsbGxsbGxs
bGxsbGxsbGxsbHR1BwcHB3Z3bGxsbGxsbGxsbGxsbG1ubm5ubm5ubm5ub3AHBwcH
cXJubm5ubm5ubm5uc2xJCwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc4axsH
MiEhISEhISEhISEhISEhISEhISEhISEhISEhMwcKCgdHaWlpaWlpaWlpaWlpaWlp
aWlpaWlpaWlpaWlqBwoKBx4WFmNkZWZnFhYWFhYWFhYWFhYWFlBoABcWFh0HCgoH
HhYWXioHTF8cFhYWFhYWFhYWFhYAYGEdYhYWHQcKCgceFhZYBwcHDVlaFhYWFhYW
FhYWW1xWBypdFhYdBwoKBxUWFlMHBwcHVAUWFhYWFhYWFlBVVgcHB1cWFh0HCgoH
FRYWTiUHBwcHTxI2FhYWFhZQUQgHBwcqUhYWHQcKCgceFhYXSSYHBwcHSksWFhYW
IxtMBwcHBylNFhYdBwoKBx4WFhY2QiEHBwcHQ0QWFkVGOAcHBwdHSBcWFh0HCgoH
HhYWFhY2PDgHBwcyPT4WP0AHBwcHQREXFhYWHQcKCgceFhYWFhY2NzgHBwcqOToh
BwcHBzsSFhYWFhYdBwoKBx4WFhYWFhYEMTIHBwczBgcHBwc0NRcWFhYWFh0HCgoH
HhYWFhYWFhYtLgcHBwcHBwcHLzAWFhYWFhYWHQcKCgceFhYWFhYWFhYoKSoHBwcH
BwYrLBYWFhYWFhYdBwoKBx4WFhYWFhYWFhYkJQcHBwcmJxYWFhYWFhYWFh0HCgoH
HhYWFhYWFhYWFhcfIAcHISIjFhYWFhYWFhYWHQcKCgcVFhYWFhYWFhYWFhcYGRob
HBYWFhYWFhYWFhYdBwoKBw4PDw8PDw8PDw8PDxAREhMPDw8PDw8PDw8PDxQHCgoH
CwwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQcKBQYHBwcHBwcHBwcHBwcHBwcH
BwcHBwcHBwcHBwcHCAkAAQICAgICAgICAgICAgICAgICAgICAgICAgICAgIDBIAA
AAGAAAAB//AP///wD///8A////AP///wD///8A////AP/4AAAAEAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

.NOTES
This was built to automatically convert an ICON file to a Base64 string for use in a powershell script, but
it can be used for any type of binary file. The only limitation is the amount of memory available.

.LINK
https://adminscache.wordpress.com/2013/06/09/base64-encoding-of-images/

.INPUTS
system.io.fileinfo
system.string

.OUTPUTS
system.string
#>

param(
[parameter(mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateScript({Test-Path -path $_})]
[string]$FilePath
)

$EncodedFile = [convert]::ToBase64String( (get-content -Path $FilePath -Encoding Byte) )
$EncodedFile = $EncodedFile -replace '.{64}', "$&`r`n"
return $EncodedFile
}

David F.

 

In my job, I am on a team that manages hundreds of Citrix servers, and as any one who manages a large farm knows, there is a lot of work to be done, and automation is critical.

We manage XenApp 6.5 farms, and I came up with this script to drain servers a lot easier to manage, especially when used with a large number of farms.  The intent is to run this from a central management point, from an elevated powershell prompt in the security context of one of the Farm Administrators with local admin rights on each of the servers.

function Run-SystemDrain
{
   cmdletbinding()]
   param()

   change.exe logon /drain</pre>

This is a very straightforward opening to the script. I use the [cmdletbinding()] and the param() block to enable the -verbose parameter.  This was primarily for troubleshooting and diagnostics, but this could easily be left out.  The change.exe logon /drain puts the system in drain mode, and under XenApp 6.5, Citrix will detect this change and communicate it to the farm, and raise the load level of that server to 10000.  That process typically only takes a few seconds.

The key to this script is the do/until loop, and a couple of WMI calls.  I used WMI since this script needed to be Powershell 2.0 compatible.  The loop opens and gets the current sessions on the server, gets the disconnected sessions, and the count of the disconnected sessions.  If there are disconnected sessions, those sessions are logged off.

do
   {
      $TotalSessions = (Get-WmiObject -class Win32_TerminalService -Property TotalSessions).TotalSessions
      $DiscSessions = Get-WmiObject -Namespace Root/Citrix -Class MetaFrame_Session -Filter 'SessionState = 4'
      $DiscCount = (DiscSessions | Measure-Object).Count

      if ($DiscCount -gt 0)

The loop starts and uses WMI to get the total session count.  This is a straight Microsoft call, and has an easy property TotalSessions that does show all the sessions.  This does include the listeners (ICA-TCP, RDP-TCP, etc.).  If your servers are configured to have more than these 2 listers, you’ll need adjust the until line.

Unfortunately, the Win32_TerminalService object really does not contain the individual sessions with the necessary capabilities.  The key capability that we need is the ability to get the SessionID and to log that session off.  I dug through the object, and could not find an easy way to get that information from this object.  The other Win32* objects didn’t have this information either.

The Citrix MetaFrame_Session object does contain the SessionID and very importantly, a Logoff method.  These 2 items made this a natural selection.  In an RDP situation, you’d need to rely on the builtin qwinsta/quser commands and parse their output to accomplish this.  Also note that MetaFrame_Session exists under the Root/Citrix namespace, and not the default Root/CIMV2. The SessionState property of the MetaFrame_Session object shows the current state of the session.  The complete list of states is:

  • 0 = userLoggedOn (Active)
  • 1 = connectedToClient (Connected but not active)
  • 2 = connectingToClient (starting to connect, but not connected yet)
  • 3 = shadowingOtherSession (Currently shadowing another session)
  • 4 = loggedOnButNoClient (Disconnected)\
  • 5 = waitingForConnection()
  • 6 = listeningForConnection(Listeners only)
  • 7 = resetInProgress (being reset)
  • 8 = downDueToError (Down)
  • 9 = initializing ()

Another interesting note was that the $DiscSessions object with all the disconnected sessions didn’t seem to have a .Count property like virtually every other object I’ve worked with.   With that, I had to turn to the Measure-Object command to get that count.  Alternatively, I could have gone with:

$DiscCount = DiscSessions | Measure-Object | Select-Object -PropertyCount

The Select-Object cmdlet is slower than directly accessing the property though the (). mechanics, although I haven’t measured it. From a day to day perspective, the difference is very minimal, so I stuck with my “usual” preferred method.

This is the key loop section

{
   foreach ($Session in $DiscSessions)
   {
      $Session.Logoff()
   }
}

As we go through each $Session in the $DiscSessions collection, and use the Logoff() method to gracefully log out the session.

And then I follow it up with a start-sleep statement and the loop repeats until all of the sessions other than the listeners are gone.

    start-sleep -seconds 30
  }
  until ($TotalSessions -le 2)

And as I mentioned above, if you have more listeners than the default 2, you have to adjust this number to match.

So, the total script is:

function Run-SystemDrain
{
   cmdletbinding()]
   param()

   change.exe logon /drain
   do
   {
	  $TotalSessions = (Get-WmiObject -class Win32_TerminalService -Property TotalSessions).TotalSessions
	  $DiscSessions = Get-WmiObject -Namespace Root/Citrix -Class MetaFrame_Session -Filter 'SessionState = 4'
	  $DiscCount = (DiscSessions | Measure-Object).Count

	  if ($DiscCount -gt 0)
	  {
		  foreach ($Session in $DiscSessions)
		  {
			 $Session.Logoff()
		  }
	  }
		  start-sleep -seconds 30
	}
	until ($TotalSessions -le 2)
}

In the next post, I’ll cover using this script in practice..

David F.

In part 1, I started building my advanced function for setting registry values on both the local machine, or a remote machine using the built-in .net methods.  Using the PSDrive called HKLM: or HKCU: both have significant limitations (like not being able to access a remote machine, and that registry values are considered ItemProperties of the registry key which is an Item.  There are other limitations also, but these are a substantial hassle for me, so I try to avoid using them).

The rest of the main body of the code looks like this:

Write-Debug -Message "Entering foreach (computer in computername) loop"
ForEach ($Computer in $ComputerName)
{
	Write-Debug -Message "ForEach ($Computer in ComputerName) pass"
	try
	{
		$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $Computer)
		Write-Debug -Message "Successfully able to open $hive on $computer"
	}
	catch
	{
		Write-Error -Message "Unable to open remote registry hive for $Computer. Please verify connectivity and permissions to retry"
		continue
	}
	Write-Debug -Message "Entering foreach loop to create full registry path if $force is enabled"
	foreach ($SubKey in $RegKeyPath.Split('\'))
	{
		$RegName = $Reg.Name
		Write-Debug -Message "SubKey is $subkey, and current registry path is $RegName"
		if ($Reg.GetSubKeyNames -contains $SubKey)
		{
			Write-Debug "\$Reg contains $SubKey, opening that key"
			$Reg = $Reg.OpenSubKey($SubKey)
		}
		else
		{
			Write-Debug -Message "\$Reg does not contain $subkey, checking for Force"
			if ($Force)
			{
				Write-Debug -Message "\$Force enabled, continuing"
				try
				{
				Write-Debug -Message "Trying to create SubKey ($SubKey)"
				$Reg = $Reg.CreateSubKey($SubKey)
				}
				catch
				{
					$Reg.Close()
					Write-Error -Message "Unable to create $subkey in $RegName, please check your permissions or the Remote Registry service may not be running" -ErrorAction 'Stop'
				}
			}
			else
			{
				$Reg.Close()
				Write-Error -Message "Force not specified, and subkey ($subkey) does not exist in $RegName" -ErrorAction 'Stop'
			}
		}
	}
	Write-Debug -Message "Registry either exists, or has been created. Proceeding with setting the value"
		try
		{
			$Reg.SetValue($ValueName, $ValueData, $ValueType)
			Write-Debug -Message "Succesfully set the value $ValueName on $Computer"
		}
		catch
		{
			Write-Debug -Message "Unable to set the value $ValueName in $RegName on $Computer; Please validate your access &amp; permissions and try again"
		}
		Finally
		{
			$Reg.Close()
		}
	}
}

The primary block of code is the

ForEach ($Computer in $ComputerName) {
}

This is a critical component of the advanced function. Since we declared that the $ComputerName parameter could contain multiple parameters from the pipeline, this construction allows the script to automatically process each of the passed computer objects.

The next block is where we attempt to open the registry hive on the remote computer

Write-Debug -Message "ForEach ($Computer in ComputerName) pass"
try
{
	$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $Computer)
	Write-Debug -Message "Successfully able to open $hive on $computer"
}
catch
{
	Write-Error -Message "Unable to open remote registry hive for $Computer. Please verify connectivity and permissions to retry"
	continue
}

I use the Write-Debug lines to provide inline comments if the script is run using the -debug parameter (thanks to Don Jones). The other option is to use write-verbose which is another text “stream” that can be used to provide inline comments. Typically, if you had a comment for your code that you would use in another language (cmd, vbscript, etc.) then write-verbose is your alternative. Write-debug is useful for “soft” errors that don’t necessarily need to stop your script and won’t necessarily have a huge effect.

The other major construction in this piece of code is the Try/Catch loops. Many other languages have the same type of error handling, but in my background (cmd, vbscript, kix, winbatch, etc.) that construction didn’t necessarily exist. This allows us to try a piece of code to see if it works, and then if it does *not* work, then we expect and error and we catch it. In the catch block, we provide whatever code we think is necessary. In my catch block, I am first writing a non-terminating error with write-error. I also use a continue statement to “break” out of the foreach loop and move on to the next computer.

The next section starts the actual registry processing..

Write-Debug -Message "Entering foreach loop to create full registry path if $force is enabled"
	foreach ($SubKey in $RegKeyPath.Split('\'))
	{
		$RegName = $Reg.Name
		Write-Debug -Message "SubKey is $subkey, and current registry path is $RegName"

First, is our inline comment explaining what is happening, and then we start a foreach loop. In the loop we are getting each element of the path specified since Split breaks it along the backslash dividers. We set $RegName equal to the name of the registry item. This sets RegName to the actual string of the path, and of course the inline comment.

Next is the section to test the path. We use the registry object’s method for GetSubKeyNmes which gives you an array of all the subkey names of the provided object, and then we use the standard -contains function to see if the subkey name from the foreach loop. If it does contain it, then we change the $Reg object to be the actual subkey that we found. This is the meat of the recursive part; with each round of the loop, we look at the registry key and if it is there, we set it one layer deeper.

if ($Reg.GetSubKeyNames -contains $SubKey)
	{
		Write-Debug "\$Reg contains $SubKey, opening that key"
		$Reg = $Reg.OpenSubKey($SubKey)
	}
	else
	{

Now, if the $Reg.GetSubKeyNames does not contain the $Subkey, then we perform the next piece of code.

Write-Debug -Message "\$Reg does not contain $subkey, checking for Force"
			if ($Force)
			{
				Write-Debug -Message "\$Force enabled, continuing"
				try
				{
				Write-Debug -Message "Trying to create SubKey ($SubKey)"
				$Reg = $Reg.CreateSubKey($SubKey)
				}
				catch
				{
					$Reg.Close()
					Write-Error -Message "Unable to create $subkey in $RegName, please check your permissions or the Remote Registry service may not be running" -ErrorAction 'Stop'
				}

In this piece of code, we check to see if the $Force switch is present which tells us to go ahead and create the path “piece” because it is missing. So, if $Force is there, we try to create the piece and then if it fails, we catch the error. And of course, with an error we follow the proper programming practice and Close() the registry key, and then write out an error indicating that there was a problem. The text message includes the most likely causes of the issue.
This takes us to the next piece of code, which is the else section of the main “if” block.

else
			{
				$Reg.Close()
				Write-Error -Message "Force not specified, and subkey ($subkey) does not exist in $RegName" -ErrorAction 'Stop'
			}
		}
	}

By closing out the registry section, we wrap up the loop to finish our recursive piece. In pseudo code, we are doing this:


<pre>for each piece of the path
     Open the registry key
          Did it open?
               If yes, then set the key to this level
               If no, then are we set to force create it?
                    If yes, then create it and set the key to this level
                    If no, then write out and error and close the registry level
go to top of the loop

Then the last portion of the main body of code is this piece:

Write-Debug -Message "Registry either exists, or has been created. Proceeding with setting the value"
		try
		{
			$Reg.SetValue($ValueName, $ValueData, $ValueType)
			Write-Debug -Message "Succesfully set the value $ValueName on $Computer"
		}
		catch
		{
			Write-Debug -Message "Unable to set the value $ValueName in $RegName on $Computer; Please validate your access &amp; permissions and try again"
		}
		Finally
		{
			$Reg.Close()
		}
	}
}

We try to set the registry value that we specified at the beginning of the function. If we can set the value, we write the information that we can, if we can’t then we write out a debug message indicating this, and then the Finally block follows the proper programming practices and closes the registry key (we don’t want memory leaks). And because we are accepting multiple computer names, the entire script will be repeated for each of the computers provided. I hope everyone finds this useful. This is now one of my standard “toolkit” scripts.

And here is a link to a PDF containing the script (it has to be a PDF because WordPress doesn’t like plain text documents as far as I can tell).
set-drfremoteregistry.pdf

Till next time,

David