Archive for the ‘Scripting’ Category

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