Tuesday, August 4, 2020

Stupid Citrix Trick #8: Forcing A Powershell Function To Return An Array

Problem: You want to return an array object from a Powershell function as an array under all circumstances.

Solution: No one's a bigger fan of Powershell than me. It's not perfect but it's powerful, flexible, and can use lots of other programming environments natively, like WMI objects and the .Net framework.  But today I found one super-duper stupid thing it does that made my blood boil with the heat of a thousand suns.

Specifically if you return an array from a function, and the array only has one element, than just that variable is returned.  So if you have an array of strings containing only a single value, that value will be returned as a string.

Why is this a big deal?  Because ArrayC = ArrayA + ArrayB behaves differently if you are adding two arrays than if you're adding a string object to an array.  Let's run through an example.

Let's pretend we have a couple of functions, GetArrayA and GetArrayB.  These functions just return an array containing three values:

function GetArrayA
{
   $Array = @("1", "2", "3")
   Return  $Array
}

function GetArrayB
{
   $Array = @("40", "50", "60")
   Return  $Array
}

$A = GetArrayA
$B = GetArrayB

$C = $A + $B

So what is the expected value of $C?  Here's what you'd expect:

1
2
3
40
50
60

And you'd be right, that is the value of $C.  Now let's make one small change:

function GetArrayA
{
   $Array = @("1")
   Return  $Array
}

function GetArrayB
{
   $Array = @("40", "50", "60")
   Return  $Array
}

$A = GetArrayA
$B = GetArrayB

$C = $A + $B


Now the array $A only contains a single entry.  You would expect the contents of $C to look like this:

1
40
50
60

But you would be wrong.  Here's what you would actually get:

140
50
60

WHYYYY would it do such a thing?  Because the function GetArrayA did not return an array.  Since the array held only a single entry, it returned that object, a string, instead of the array.  And you can confirm that with the .GetType() function.  It then appended that string object to the first entry in array $B and went on its merry way.

You'd think when I realized this, realized that my code was correct but Powershell was sabotaging me, that I would've been relieved, but instead my wrath burned with the fire of a thousand suns. I trusted Powershell, Powershell was my friend, and suddenly it turned around and stabbed me in the back.

However there's a simple solution.  You just have to use a type declaration on the variables that will hold the array values, like this:

[array]$A = GetArrayA
[array]$B = GetArrayB

Despite the title of this post you're not actually forcing the function to return an array, you're forcing the variable to receive an array.  Which means you use that function frequently, you'll have to update a lot of code! But at least it WILL work.

I found that solution here:

https://stackoverflow.com/questions/11107428/how-can-i-force-powershell-to-return-an-array-when-a-call-only-returns-one-objec

If this helps even one of you avoid the painful consequences, then my suffering was worthy it.

Thursday, July 30, 2020

Stupid Citrix Trick #7: Getting User Profiles Right

Problem: User profiles, man.

Solution: So who doesn't love user profiles?  Well, the users aren't too fond of them when their profile gets corrupted, and as a systems engineer who works on Citrix I can say that I get awfully tired of resetting them.  It's a rare instance when users and sysadmins are in full agreement.

But like many crummy things, profiles are the best of a bad lot.

This post won't delve into quite as many technical details as some of my others, but I thought I'd pass along the best practices that I've learned, most of which won't be new if you've worked on profiles before.  But it may help you avoid some of the pitfalls I've tumbled headlong into.

Profile Location.  A profile is just a folder with a bunch of files in it, so it can go on pretty much any shared folder.  I always like to name this folder "CitrixProfiles" so other sysadmins will leave it alone so they don't get Citrix all over themselves. You can just put it at the root of a share, but I normally do it like this:


You'll see that this path includes a template for what each user's profile will be.  I highly encourage you to use %username%.%userdomain% as in the picture, even if your environment only has a single domain.  Someday that might change and it's very difficult to go back and reorganize profiles.

I'd really prefer if Citrix allowed us to use the environment variable %userdnsdomain% but if you can read the text near the bottom of the profile policy image, that's not allowed.

Profile Permissions When setting profile permissions, you'll have two goals:  1) Users who are logging on for the first time must be able to create a profile folder, and 2) users should not have access to other people's folders.  To implement that, we'll use two different sets of permissions.

(As for share permissions, users will need modify rights and administrators will need full control.)

The first set of permissions will be for "Authenticated Users" - anyone who can log on to a Citrix server - and will apply to "This folder only":


It's not obvious what those permissions mean, but what it amounts to is that all authenticated users can create a subfolder in this folder, CitrixProfiles, but they cannot see into other user's folders. When a user creates a folder, they become the Creator Owner of that folder.  Creator Owner is an object that can be assigned permissions.  In fact that's how we'll grant the user rights.  Here is the second set of permissions we'll use:


As you can see, setting rights for the Creator Owner object, but not for the CitrixProfiles folder - for subfolders and files only (subfolders of the user's own profile folder, not CitrixProfiles itself).  By the way, I have seen some documents that suggest giving users the "Full control" permission on their profile folder.  DO NOT DO THIS!! I cannot emphasize that enough.  Users only need rights to modify the contents of their folder, they do not need to be able to set permissions on their folder as well.  Of course no user would ever do that but the process that saves their settings runs as the user.  Sometimes things go wrong, and if the account has the ability to change permissions then the permissions will get bunged up from time to time.

You should also apply permissions for whatever administrators' group you're using.  This group will need full control, and it will be applied to "This folder, subfolders, and files" so it will have permission to everything.

One other thing - if your organization has an IT Security team, and if that team ever has to update permissions on your profile folder, they'll probably take ownership of the file system, which will make all the users' profiles unavailable since they rely on the user being the owner.  I do have a Powershell script that can get a list of all the profiles, look up the user with the profile name (which is the username) and profile extension (the domain), and then grant the user explicit rights to the folder.  I'll post it in a few days, so watch this space.

Tuesday, July 28, 2020

Stupid Citrix Trick #6: Showing the Web Server Name in StoreFront

Problem: Sometimes when users are logging on through the StoreFront web page, you need to know what web server they are reaching.

Solution:  This is a pretty common thing, mostly for troubleshooting or finding other inconsistencies between how two StoreFront web servers are behaving.  When I started out a co-worker would just hard code the store name in the StoreFront HTML.  He couldn't figure out why, after he replicated from one server to another, only one server name showed up!

The solutions I found online typically involved setting a value in Internet Information Services that's unique to that server and the retrieving it, but I believe I've found a better way.

First you're going to want to create a file called GetServerName.aspx, and if you want the domain too then you may want to create GetServerDomain.aspx.  You can do that all in one file, but having separate ones gives you a little flexibility in how you display it.

Since these files will only be used on the web page and not the Citrix client, we'll put them in the customweb folder.

What I realized was that the server name and domain are available in environment variables on the server, and we can use very simple ASP.NET code to query and return them.  Here's GetServerName.aspx:

<%@ Page Language="C#"%>
<%=Environment.GetEnvironmentVariable("COMPUTERNAME")%>

That's the whole file.  GetServerDomain.aspx is almost as simple:

<%@ Page Language="C#"%>
<%=System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName%>

As I said, you drop those two files in the customweb folder, and then you go to the custom folder and edit script.js.

I am not a Javascript expert. I know just enough endanger my livelihood, but I always like to make sure my Javascript variables have some value, because if they don't it could cause issues in the code. So let's set some variables to an empty string:

var ServerName = ""; // The name of this StoreFront server.
var ServerDomain = ""; // The DNS name of the StoreFront server's domain.

Next we're going to jump down to a portion of script.js where the function CTXS.Extensions.afterDisplayHomeScreen gets called.  This is when the home screen has already been loaded, and that's when we want to make the changes.  Here's the code to insert the variable:

CTXS.Extensions.afterDisplayHomeScreen = function (callback)
{
    // Get the server name.
    $.ajax(
    {
        async: false,
url: "customweb/GetServerName.aspx",
success: function(Name)
{
ServerName = Name;
}
    });

    // -----------------------------------------------------------

    // Get the server domain.
    $.ajax(
    {
async: false,
url: "customweb/GetServerDomain.aspx",
success: function(Domain)
{
ServerDomain = Domain;
}
    });
};
 
Again, my Javascript skills are somewhat rudimentary.  In particular this is considered poor practice because it's a synchronous operation, and if there are any delays getting the data back it could cause problems.  Use at your own risk, and your mileage may very, but this has worked in our environment.

(Also, if someone has a simple method for doing an asynchronous callback, please drop it in the comments to I can be "with it" like all the cool kids.)

Saturday, July 25, 2020

Stupid Citrix Joke #1

I had a Citrix joke but everyone else is using it.

Thanks, I'm here all week.

Tuesday, July 21, 2020

Stupid Citrix Trick #5: Updating INI files

Problem: To configure some older programs we have to manipulate INI files. There's not a built-in way to do this in our preferred scripting language, Powershell.

Solution: Like so many things in IT, we must begin with a story about Lotus Notes.

For those not familiar with Notes, it's an email and workflow program, and its workflow capabilities are especially useful.  You can use it to set up a change request system, for example, with tickets being routed to various users for their approval.  For the time it was considered advanced.

So about ten years ago we had just upgraded Notes from version 7 to version 8.  The new version came with a redesigned graphical user interface which was much easier to use, but it still included the old GUI for users who preferred it.  Crucially, each interface had a different set of licenses, so if your account was licensed for the old interface, you needed to run that interface to stay in compliance.  Which interface the application launched with was configured by a command-line parameter, and since we had separate Active Directory groups for the users of each interface, it was simple to set up two Citrix published apps, each one assigned to the appropriate group and with the proper command-line settings.  And all was right with the world.

Then we changed our email system to Microsoft Exchange.  Users would not launch Notes to get their email as before; instead their workflow notifications would go to their Outlook inbox.  When they received one they'd just click a link in the message and it would launch Notes and take them directly to the right document.

And this was a huge problem! No longer could I use command-line parameters to configure the Notes interface.  Due to the limited number of licenses I couldn't just always use one or the other.  I had to find another way.

Oh, and just to add a dash of urgency, our senior executives were going on a retreat in a couple of weeks where they would only be able to access Outlook and Notes via Citrix, so I had to get this working ASAP.

But then, hallelujah! I found that you could use an INI setting in the Notes configuration file for each user that would configure the interface.  This INI file was stored in a known location in the user's home folder, so we could use a launch script for Outlook that would make sure that the INI file had the correct settings before launching, and when the user clicked a link to a Notes document, Notes would already be set up.

Too bad there's no way to change INI settings in Powershell.

Well, no native way.

Fortunately Powershell is the Swiss army knife of programming languages, and if something can be done, it can be done in Powershell, and in this case what we need to do is to use some functions from kernel.dll.

(Quick note: I am not an expert at this, and likely found it on the Internet. I'd like to give credit but this was at least eight years ago.)

For our purposes we'll pretend we're working with a file called "sample.ini", which contains this text:

[Section1]
Key1=FirstKey
Key2=SecondKey

[Section2]
Key3=ThirdKey

Here's the first function, Get-IniSetting, which reads from an INI file.  You can see the parameters are all required, and used to specify the file name, the section we want to work with, and the key we want to fetch.  The first and last are simple but for the section you'll want to use the name without the brackets. To get a key from [Section2] you'd just pass "Section2" to this function.


Function Get-IniSetting
{

[CmdletBinding()]
Param
(
    [Parameter(Mandatory = $True)]
    [string]$File,

    [Parameter(Mandatory = $True)]
    [string]$Section,

    [Parameter(Mandatory = $True)]
    [string]$Key
)

$Signature = @’
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
‘@

    $type = Add-Type -MemberDefinition $Signature -Name Win32Utils -Namespace GetPrivateProfileString -Using System.Text -PassThru

    $builder = New-Object System.Text.StringBuilder 1024

    $type::GetPrivateProfileString($Section, $Key, "", $builder, $builder.Capacity, $File) | Out-Null
   
    $builder.ToString()

}


Here's what a call to that function would look like. In this case the result would be the string value "SecondKey":

Get-IniSetting -File "C:\Users\Graham\Documents\sample.ini" -Section "Section1" -Key "Key2"

Next up is the function to write settings to an INI file.  If you look at the parameters you'll see that it has a parameter set, so based on the parameters passed it can do a couple of different things.


Function Write-IniSetting
{

[CmdletBinding()]
Param
(
    [Parameter(Mandatory = $True)]
    [string]$File,

    [Parameter(Mandatory = $True)]
    [string]$Section,

    [Parameter(ParameterSetName = "SetKey")]
    [string]$Key,

    [Parameter(ParameterSetName = "SetKey")]
    [string]$Value
)

$Signature = @’
[DllImport("kernel32.dll")]
public static extern bool WritePrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpString,
    string lpFileName);
‘@


    Switch ($PSCmdlet.ParameterSetName)
    {
        "SetKey"
            {
                If ($Value -ne (Get-IniSetting -File $File -Section $Section -Key $Key))
                {
                    $type = Add-Type -MemberDefinition $Signature -Name Win32Utils -Namespace WritePrivateProfileString -Using System.Text -PassThru

                    $type::WritePrivateProfileString($Section, $Key, $Value, $File) | Out-Null
                }
            }
        default
            {
                $type = Add-Type -MemberDefinition $Signature -Name Win32Utils -Namespace WritePrivateProfileString -Using System.Text -PassThru

                $type::WritePrivateProfileString($Section, [NullString]::Value, [NullString]::Value, $File) | Out-Null
            }
    }
}

This function does two different things based on what parameters are passed.  The first two, $File and $Section, are exactly the same as before, but the $Key and $Value parameters are optional.  If all four are specified then the function works as you'd expect, setting the value of the specified section and key, and creating it if it doesn't already exist.  Here's a what that call would look like:

Write-IniSetting -File "C:\Users\Graham\Documents\sample.ini" -Section "Section2" -Key "Key2" -Value "AnotherValue" 


But if you only include the $File and $Section parameters, it does something much different: it deletes the entire section. This can come in handy if you need to make wholesale changes.  This function call would delete Section2 and all the data it contains:

Write-IniSetting -File "C:\Users\Graham\Documents\sample.ini" -Section "Section2"

This is really a Stupid Powershell Trick more than a Stupid Citrix Trick - something that will come up many times - but it's extremely useful when dealing with older applications, all of which seem to live out their golden years on the Citrix farm.

Wednesday, July 8, 2020

Stupid Citrix Trick #4: Profile Bloat and INetCache

Problem: User profiles are unusually large.  Logons and logoffs are taking an extremely long time, and disk space is running low in the network share where profiles are saved.

Solution: Almost every user with a Citrix profile will have a folder in that profile called INETCACHE.  Based on the name alone, what would you expect that folder to do?  Cache internet files, right?  And that's exactly what it does, but unfortunately there's more to the story.

First of all, that's not the folder where Internet Explorer files are cached.  They are actually cached in a subfolder, INetCache\IE.  It's the IE folder that gets bloated and cause issues. There's another good reason for putting in in its own subfolder: the INetCache folder contains quite a bit of stuff besides cached Internet Explorer files.

I don't know if it's this was for all Office versions, but I do know that Office 2010 and Office 2013 also store cached resources in the INetCache folder, each in as separate subfolder.  Not only that but Office does not like it one bit if you delete those folders.  It will complain about it without doing anything useful, such as automatically recreating them at application launch.  If they do get deleted you'll have to recreate them manually or reset the user's profile.

So how to limit bloat without affecting Office or other apps that use that folder.  Naturally, Citrix has you covered.

In the Profile section of XenApp 7 policies there are a lot of settings for user profiles, from the most basic, such as the path to the profile share and the names of the user folders, to such things as... folder inclusion and exclusion policies.

First, here is the exclusion policy to make sure that INetCache is included in the user's profile:


(Profile paths are always relative to the root of the user's profile, natch.)

So that setting will include the entire INetCache folder.  Great!  Well, sorta.  We don't want to include the IE folder.  Remember the catchphrase "There's an app for that!"  You guessed it: there's a setting for that!  And here it is:


Yes it's JUST THAT SIMPLE.  You can explicitly exclude subfolder of folders you have already explicitly included.  Apply these two policies and your profile woes are over.

But Graham (you say), we still have all these bloated folders on disk.  Is there any way we can clean out the IE folder that's already there?  I'm glad you asked!  This is Citrix, of course there's a way.

May I present to you the Logon Exclusion Policy.


As you can see we have selected to delete excluded files and folders from the user's profile.  Once we apply the two policies above, this policy will delete the files in the IE folder (and any other excluded folders).  The other values for this setting are to synchronize the excluded files and folders with the user's profile, or to ignore the files.

Disinfecting your profiles this way reduce disk usage and improve logon performance, which should keep both IT and your end users happy.

Sunday, July 5, 2020

Stupid Citrix Trick #3: Fixing Provisioning Services Client Drive Letters

Problem: Citrix virtual machines using Provisioning Services are getting wrong drive letters.

Solution: Provisioning Services (hereafter "PVS") is a Citrix method of deploying and updating virtual machines using a standard image (or several different images).  I believe it predates Machine Creation Services ("MCS"), which is simpler to implement, but PVS is still very useful as the server image is stored as a file completely independent of any particular virtual machine; in MCS, a VM disk or an snapshot of one is replicated.

On a server running on PVS, the C: drive is virtual. It physically exists only as a file on the PVS server; each VM running the image has a local hard drive to cache the image file. But if you are not using the default Windows drive letters, this can cause problems if two circumstances are met: 1) not all of your VMs are deployed from the same template, and 2) you are not using the default drive letters.

First lets talk about the drive letters.  In PVS the virtual disk running from an image will always be C:.  By default, Windows will name the physical drive with the next drive letter, D:.  If you want to change that drive letter to anything else, then Windows needs to be able to remember your settings, and it does this by saving the selected drive letter and the unique ID of the drive in the registry.

So at your first site, the artfully named Site A, everything is fine and dandy.  You deploy a bunch of virtual machines, all from the same VM template, and they load the image. Raises and promotions for everyone!

But there's a problem.  When you create some VMs at the notoriously backwards and incompetent Site B, the drive letters are not correct.  In fact they revert to the defaults. All published applications that point to drive J: (an extremely cromulent drive letter) are failing because what should be J: is actually D:. What possibly could have gone wrong?

Remember when I said Windows saves a non-default drive letter in the registry, along with the drive's unique ID?  Different drives have different signatures, and if the ID doesn't match what's in the registry, default drive letters are used.

Happily there's a simple solution.  First, you'll need to create a scheduled task that runs at startup, or better yet, startup plus two minutes.  This task will first examine what's called a personality file, which PVS manages.  From this file you can tell if the disk is in private (editing) mode, or standard (running) mode.

If the disk is in private mode, the task saves the disk ID to a folder on the C: drive.  If it's in standard mode, the disk compares the saved ID to the local disk.  If they are not the same then it updates the disk ID and reboots. This works because disk IDs don't have to be globally unique, unlike a MAC address for example; they only have to be unique within the same computer.

Here's a sample script in Powershell:

# This program is used to ensure that all Citrix PVS session hosts
# have the same disk UniqueID value as the editing server where the
# image was last editied.
#
# To do this, it first looks in C:\Personality.ini and checks to see
# if the disk is in Private (_DiskMode=P) or Standard (_DiskMode=S)
# mode.  If Private mode, it SAVES the UniqueID to
# C:\SetDiskID\DiskID.txt.
#
# If Standard mode, it checks to see if the UniqueID matches the
# contents of C:\SetDiskID\DiskID.txt, and if it does not match,
# it SETS the ID using Diskpart.

# Set some variables.
$PvsDiskCaption = "Citrix Virtual Disk SCSI Disk Device"

$LaunchDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$LaunchDir = $LaunchDir.TrimEnd("\")

$DiskIDPath = $LaunchDir + "\DiskID.txt"
$DPInputPath = $LaunchDir + "\DpInput.txt"


# First, check to see what mode we are in.

$PersonalityFile = "c:\Personality.ini"

If (Test-Path -Path $PersonalityFile)
{

    # Read in the file.
    $Personality = Get-Content -Path $PersonalityFile

    # Now examine each line until you find the _DiskMode field.

    ForEach ($PrsLine in $Personality)
    {
        $Matched = $PrsLine -match "(.*)\="

        # If the first match group equals _DiskMode, save the value.
        If ($Matched -and ($Matches[1] -eq "_DiskMode"))
        {
            $PrsLine -match "\=(.*)"

            $DiskMode = $Matches[1]
        }
    }


    # Identify which disk is the non-PVS disk.

    # Get the disks in this server.  There will be at least two, a fixed disk and a PVS disk.
    $Disks = Get-Disk

    # Find the first disk that does NOT have type "Citrix Virtual Disk SCSI Disk Device".
    $TargetDisk = $null
    $DiskCount = $Disks.Count

    For ($CurrDisk = 0$CurrDisk -lt $DiskCount$CurrDisk++)
    {
        $Disk = $Disks.Item($CurrDisk)
                       
        If (-not ($Disk.Friendlyname -eq $PvsDiskCaption))
        {
            $TargetDisk = $Disk
            $TargetDiskIndex = $CurrDisk     # Disk index is important if we have to use Diskpart below.
            Break     # Exit once the disk is found.
        }
    }

    # If we found a disk to use...
    If (-not ($TargetDisk -eq $null))
    {
        # Get the UniqueID of the non-PVS disk in hexadecimal format.
        $UniqueID = $TargetDisk.Signature.ToString("X")

        # Now check to see what the mode is.
        switch ($DiskMode)
        {
            "P"     {
                        # Private mode. We need to capture the disk's UniqueID and save it to a file.
                        Set-Content -Value $UniqueID -Path $DiskIDPath
                    }


            "S"     {
                        # Standard mode.  We must check to see if the Unique ID matches what's in the file
                        # If not, add the correct value and then reboot.

                        # Read in the target UniqueID from file.
                        $TargetID = Get-Content -Path $DiskIDPath

                        If (-not ($TargetID -eq $UniqueID))
                        {
                            # Unique ID does not match, set it using Diskpart.

                            # Write Diskpart commands out to a text file, DpInput.txt.
                            Remove-Item -Path $DPInputPath -Force
                            Add-Content -Value ("Select Disk " + $TargetDiskIndex-Path $DPInputPath
                            Add-Content -Value ("UniqueID Disk ID=" + $TargetID-Path $DPInputPath
                            Add-Content -Value ("Exit"-Path $DPInputPath

                            # Run Diskpart to update the ID.
                            Start-Process -FilePath "C:\Windows\System32\Diskpart.exe" -ArgumentList ("/s " + $DPInputPath-Wait

                            Start-Sleep -Seconds 5

                            # Reboot to fix the drive letters.
                            Restart-Computer -Force
                        }
                    }
        }
    }
}