XenDesktop ClientName AppLauncher

Posted: January 2, 2014 in Applications, Citrix
Tags: , , ,

In my opinion, one of the great missteps that Citrix has made (and I pray they will fix it!) is that by default, XenDesktop does not set the CLIENTNAME variable.  Now that Citrix has become common place, a lot of software vendors are beginning to take advantage of the CLIENTNAME variable in their programs.  I work in the financial industry, and many of the products are not only Citrix aware, they also assign licensing on a per-physical seat basis by using CLIENTNAME in a Citrix environment. But, since XenDesktop doesn’t provide this, it’s a challenge to make this work properly.

My solution was to create a launcher executable that would get the actual clientname out of the registry, set it the variable and execute the application.  (I’ve had mixed results with the Virtual Channel Driver).  My language of choice for this is Winbatch.  (A trial version can be downloaded from http://www.winbatch.com.)  [The normal version of Winbatch lists for $99, and the compiler to create redistributable executables is $499.  However, the compiled exe’s can be distributed without limit].

There are 2 parts to my solution – the actual script file, and the INI file. I use an INI file for the configuration data, simply because the INI functions in Winbatch are extremely easy to use, and Winbatch’s ability to process command line parameters is not very good.. it’s not terrible, but it could be a lot better.

The INI File:
As is my pattern with most of my winbatch scripts, the INI file has the same root name as the script itself. The INI file has one section called Configuration, and only two entries.  To execute an external program, Winbatch needs the executable name, and the parameters as a separate item.  So, the CLAppLauncher.INI file is very simple:

[Configuration]
EXEName=<full path of executable>
EXEParams=<command line parameters>

The script itself is fairly simple, and includes some of my “boilerplate” Winbatch code.  But, it basically extracts the clientname from the registry, adds it to the environment, and then executes the program.  Part of the design was to make it very flexible so that it can be used with any executable file (hence the INI file).

Boilerplate Code:

WinHide("")
IntControl(12,5,0,0,0) ;Allow clean exit
;IntControl(38, 1, 0, 0, 0) ;Hide all errors
IntControl (28, 2, 0, 0, 0) ;Set the controls to use the system GUI font

WinHide(“”) – By default, Winbatch displays a large white dialog box while it is processing the script.  For me, this is virtually always undesirable, so as part of my boilerplate, I use WinHide(“”) to suppress this.

IntControl(12,5,0,0,0) – this is one of the internal control calls that Winbatch provides. This particular call lets Winbatch exit cleanly when closed, and to acknowledge a Windows shutdown in order to exit.  As an aside, the IntControl functions all have a numerical code, followed by up to 4 built-in parameters for the call.

IntControl(38,1,0,0,0) – this call suppresses all of the errors in the script.  As long as the error is not a terminating error, it prevents the script from exiting unnecessarily.  I typically leave it commented out (;), because I want to know when an error happens.

IntControl (28, 2, 0, 0, 0) – this call tells the Winbatch dialogs to use the system font by default.  Out of the box, Winbatch uses a much older “crude” looking font that is not visually appealing.  By using this call, I have the script look exactly like the default dialogs & menus etc.

The next step is to load the Extenders.  Extenders are DLL’s that can be downloaded freely from Winbatch’s website.  These DLL’s provide ‘wrapper’ functions for Windows API calls.  This allows you to perform programming functions without actually knowing how to do the programming API calls.  This allows scripts to really run like a program.  (I’ve actually downloaded programs online that were actually Winbatch scripts..)  In this case, I’m using the WILX extender which provides several “basic” functions such as MessageBox.

AddExtender("WILX44i.DLL")

Because I will be using some of the registry call functions from Winbatch, I like setting some default constants so that I can use plain english when making calls, and I don’t need to remember or lookup the numerical values.

REG_SZ = 1
REG_EXPAND_SZ = 2
REG_BINARY = 3
REG_DWORD = 4
REG_MULTI_SZ = 7
REG_QWORD = 11
The next line isn’t *critical* per-se.. but I like to keep my functions at the bottom of the script, so I jump to the Functions section to read them in and then return.  Processing wise it really doesn’t cost any processing power, but it makes it more legible in general.  (Most Winbatch scripts look like regular batch files, and I try to format all of my scripts in a much more “formal” looking manner.)  This particular script does not have any user-defined functions (UDF’s).
GoSub Functions
Further down in the script is the Functions section, along with the one function in this script and the Return statement.
:Functions

#DEFINEFUNCTION GetINIFile(inifile)
    mbOKOnly               = 0 ;Display OK button only.
    mbOKCancel             = 1 ;Display OK and Cancel buttons.
    mbAbortRetryIgnore     = 2 ;Display Abort, Retry, and Ignore buttons.
    mbYesNoCancel          = 3 ;Display Yes, No, and Cancel buttons.
    mbYesNo                = 4 ;Display Yes and No buttons.
    mbRetryCancel          = 5 ;Display Retry and Cancel buttons.
    mbCritical             = 16 ;Display Critical Message icon.
    mbQuestion             = 32 ;Display Warning Query icon.
    mbExclamation          = 48 ;Display Warning Message icon.
    mbInformation          = 64 ;Display Information Message icon.
    mbDefaultButton1       = 0 ;First button is the default.
    mbDefaultButton2       = 256 ;Second button is the default.
    mbDefaultButton3       = 512 ;Third button is the default.
    mbDefaultButton4       = 768 ;Fourth button is the default.
    mbApplicationModal     = 0 ;Application modal. 
                               ;The user must respond to the message box before continuing
                               ;work in the current application.
    mbSystemModal          = 4096 ;System modal. On Win16 systems, all applications are suspended 
                               ;until the user responds to the message box. On Win32 systems, 
                               ;this constant provides an application modal message box that always
                               ;remains on top of any other programs you may have running.

    mbOK                   = 1 ;OK button was clicked.
    mbCancel               = 2 ;Cancel button was clicked.
    mbAbort                = 3 ;Abort button was clicked.
    mbRetry                = 4 ;Retry button was clicked.
    mbIgnore               = 5 ;Ignore button was clicked.
    mbYes                  = 6 ;Yes button was clicked.
    mbNo                   = 7 ;No button was clicked.
    sINIFile = FileLocate(inifile)
    if sINIFile == ""
        sMessage = strcat("Could not locate - ", inifile, @CRLF, @CRLF, "Please contact IT")
        xMessageBox("Missing INI File", sMessage, mbCritical)
        exit
    endif 
 return sINIFile
#ENDFUNCTION
return
#DEFINEFUNCTION Name(Parameters)  – this defines a function, along with parameters.  A function call is limited to 5 parameters.  One major limitation of the Winbatch functions is that the variables are 100% isolated to the scope of the functions.  Any variables set outside of the function itself are NOT accessible.  To that end, the variables sometimes have to be defined a second time inside of the function to work properly.
[sINIFile = FileLocate(infile)]
The next several lines define the constants for xMessageBox.  xMessageBox is a wrapper for the standard MessageBox function. The next line after that uses the FileLocate command.  This is a default command for Winbatch (it does not need an extender).  FileLocate actually searches the local directory, followed by the Windows path.  If the file is located, the full path to the file (including the file name) is returned.  If not, a blank string is returned.  inifile is a variable that represents the file we are looking for.
[if sINIFile == “”]
In the next line, I check to see if a blank string is returned.  Notice the 2 = signs (==).  In Winbatch, assignments use a single =, while a comparison uses ==.  If you use an single = in a comparison, it will generate an error.  You’ll notice that the “feel” of the script is similar to vbscript.

[sMessage = strcat(“Could not locate – “, inifile, @CRLF, @CRLF, “Please contact IT”)]
In this line, I’m creating a message prompt for the Messagbox.  Using strcat(), I am concatenating several string fragments.  You can basically concatenate an unlimited number of strings together with strcat.  The limitation is that a single line in a Winbatch script is limited to 2048 characters. That’s a lot of data, but for legibility, it’s easier to concatenate it first, and then use the variable.  You’ll also notice the internal macro.  The macro starts with @, and there are a number of them that get used.  Most of them are pretty obvious what the represent (@CRLF, @CR, @LF, @REGMACHINE, @REGCURRENT, @TAB, etc.)

[xMessageBox(“Missing INI File”, sMessage, mbCritical)]
This pops up the actual messagbox and notifies the user that the file can’t be located.  The first parameter is the Title of the message box, the second parameter is the prompt (body) of the message box, and the last one indicates the settings for the buttons, icons etc.  Because it is a numerical value, it’s easy to add them together.  For example, you could do mbCritical + mbYesNo. and that will show the stopsign icon of a critical error and the Yes and No buttons. If you want to use the results of the message box, then you would set a variable equal to the xMessageBox call.  So, continuing this example, you could do sResult = xMessageBox(“Test dialog”, “Do you like the test dialog?”, mbCritical + mbYesNo).  And then you can test the results with the other variables that are defined (mbYes or mbNo).

[Exit]
The next line is a simple Exit which just exits the script.  This prevents the script from trying to continue if it can’t find the INI file.  It’s a graceful way to keep it from erroring out if the INI file is not available.

[EndIf]
Every IF call must be terminated with an EndIf call.  This tells Winbatch where the IF test is completed.  (And just like vbscript, you can add in ElseIf and/or Else clauses.

[return sINIFile]
In this function, we are returning the full path of the INI file.

[#ENDFUNCTION]
Just like the EndIf, we need to tell Winbatch where the end boundary of the function is.  (Also notice that definefunction and endfunction begin with # signs.  This is critical!

[sINIFile = FileLocate(“CLAppLauncher.ini”)]
Now, our function is defined.. and we’re able call it.  I set a variable equal to the function to get the path.  As mentioned above, if the file isn’t found, the script exits, so we don’t have to worry about anything.

[sRunString = IniReadPvt(“Configuration”, “ExeName”, “”, sINIFile)]
In this line, I’m getting the executable name to run.  Again, if the executable is not in the Windows path, then you need to specify the full path in the INI file.  But, the syntax is pretty self-explanatory (with one minor exception).  IniReadPvt – reads a “private” ini file which is basically anything that is not the win.ini file. The first parameter is the section name, the second value is the value name, the 3rd value is a default value that is used if the value is not set in the INI file, and the last parameter is the name of the INI file.  Because the 3rd parameter is set to blank “”, nothing is passed as the default.  If we specified something like Notepad.exe, then it would run notepad.exe if ExeName was not set.

[sRunParams = IniReadPvt(“Configuration”, “ExeParams”, “”, sINIFile)]
This is virtually identical to the line above.. we’re just getting the parameters of the command line.  Anything you would use in a command line would be put here.  As an example, Outlook has a command line switch /cleanfreebusy.  So, the parameters for the command line would be /cleanfreebusy.  The INI file line would show ExeParams=/cleanfreebusy.

[sClientName = RegQueryEx(@REGMACHINE, “Software\Citrix\ICA\Session[ClientName]”,””,REG_SZ)]
Now, we go extract the client name from the registry.  I am using the RegQueryEx function (Registry Query Extended) to read the value.  RegQueryEx requires an open registry handle – We’re using the macro @REGMACHINE to represent HKLM.  (By default, Windows always has handles open to the root keys like HKLM, HKCU, HKU, etc.).  The next parameter is the string that represents the path in the registry.  It’s important to note that the actual VALUE name is surrounded by [ ] marks in the string.  The next parameter is blank “”.  This is the delimiter used in REG_MULTI_SZ values, and unless the value is a REG_MULTI_SZ it is ignored.  The last parameter is the type of registry vale.  Here I am using the constant that I had previously defined for legibility. I could just as easily used RegQueryEx(@REGMACHINE, “Software\Citrix\ICA\Session[ClientName]”, “”, 1), but someone following up behind me would not necessarily know what the 1 meant.

[EnvironSet(“ClientName”, sClientName)]
This sets the environment variable. It is important to note that EnvironSet only affects the current environment for the script.. it does *not* affect the environment outside of the script execution itself.

[If sRunParams == “”]
I am validating if the sRunParams is blank or not.  I am checking this to make sure that I don’t generate an error on the next call.

[RunEnviron(sRunString, “”, @NORMAL, @WAIT)]
The RunEnviron command actually runs the executable using the environment that exists inside of the script.  By combining this statement with the previous EnvironSet, we’re able to modify the environment as we want and executing the specified program in that context.  It runs the executable string without any parameters (1st and 2nd parameters).  The 3rd parameter indicates the Window style.  Again, Winbatch provides several macros so that you do not need to remember what the actual numeric values are.  In this circumstance, there are options like @HIDDEN, @MAXIMIZED, etc.  The last parameter indicates if the script should wait for the program to exit and return.

[Else]
Pretty self-explanatory.

[RunEnviron(sRunString, sRunParams, @NORMAL, @WAIT)]
This is identical to the RunEnviron above, other than that I’m passing the runtime parameters to the RunEnviron command.

[EndIf]
This ends the IF construct.

That’s the sum of the file.. it simply reads the configuration file for the executable information, reads the ClientName, sets the variable, and then runs the program.  By providing the INI file it is very flexible, and using this setup, it is easy to get the needed ClientName variable.

Here is the code in it’s entirety

; ======================================================================
; 
; WinBatch Source File -- Created with SAPIEN Technologies PrimalScript 2012
; 
; NAME: CLAppLauncher.wil
; 
; AUTHOR: David Figueroa
; 
; COMMENT: Script to set the ClientName variable and launch the specified application
; 
; ========================================================================
WinHide("")
; ========================================================================
; Add Extenders
; ========================================================================
AddExtender("WILX44i.DLL")
; ========================================================================
; Set internal controls
; ========================================================================
IntControl(12,5,0,0,0) ;Allow clean exit
;IntControl(38, 1, 0, 0, 0) ;Hide all errors
IntControl (28, 2, 0, 0, 0) ;Set the controls to use the system GUI font

; ========================================================================
; Constants
; ========================================================================
REG_SZ = 1
REG_EXPAND_SZ = 2
REG_BINARY = 3
REG_DWORD = 4
REG_MULTI_SZ = 7
REG_QWORD = 11

; ========================================================================
; Main Routine
; ========================================================================

GoSub Function
sINIFile = FileLocate("CLAppLauncher.ini")
sRunString = IniReadPvt("Configuration", "ExeName", "", sINIFile)
sRunParams = IniReadPvt("Configuration", "ExeParams", "", sINIFile)
sClientName = RegQueryEx(@REGMACHINE, "Software\Citrix\ICA\Session[ClientName]","",REG_SZ)
EnvironSet("ClientName", sClientName)
if sRunParams == ""
    RunEnviron(sRunString,"", @Normal, @WAIT)
else
    RunEnviron(sRunString, sRunParams, @Normal, @Wait)
endif

:FUNCTIONS
#DEFINEFUNCTION GetINIFile(inifile)
    mbOKOnly = 0 ;Display OK button only.
    mbOKCancel = 1 ;Display OK and Cancel buttons.
    mbAbortRetryIgnore = 2 ;Display Abort, Retry, and Ignore buttons.
    mbYesNoCancel = 3 ;Display Yes, No, and Cancel buttons.
    mbYesNo = 4 ;Display Yes and No buttons.
    mbRetryCancel = 5 ;Display Retry and Cancel buttons.
    mbCritical = 16 ;Display Critical Message icon.
    mbQuestion = 32 ;Display Warning Query icon.
    mbExclamation = 48 ;Display Warning Message icon.
    mbInformation = 64 ;Display Information Message icon.
    mbDefaultButton1 = 0 ;First button is the default.
    mbDefaultButton2 = 256 ;Second button is the default.
    mbDefaultButton3 = 512 ;Third button is the default.
    mbDefaultButton4 = 768 ;Fourth button is the default.
    mbApplicationModal = 0 ;Application modal. 
                           ;The user must respond to the message box before continuing
                           ;work in the current application.
    mbSystemModal = 4096 ;System modal. On Win16 systems, all applications are suspended 
                         ;until the user responds to the message box. On Win32 systems, 
                         ;this constant provides an application modal message box that always
                         ;remains on top of any other programs you may have running.
    mbOK = 1 ;OK button was clicked.
    mbCancel = 2 ;Cancel button was clicked.
    mbAbort = 3 ;Abort button was clicked.
    mbRetry = 4 ;Retry button was clicked.
    mbIgnore = 5 ;Ignore button was clicked.
    mbYes = 6 ;Yes button was clicked.
    mbNo = 7 ;No button was clicked.

    sINIFile = FileLocate(inifile)
    if sINIFile == ""
        sMessage = strcat("Could not locate - ", inifile, @CRLF, @CRLF, "Please contact IT")
        xMessageBox("Missing INI File", sMessage, mbCritical)
        exit
    endif 
    return sINIFile
#ENDFUNCTION
return

That’s it till next time 🙂

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s