Virtualization, technology, and random rantings with a focus on Citrix and VMware.

Category: Windows

Want To Update That HTML5 Workspace App? How About A Script! : Scripted Update HTML5 Workspace App

So you see that the HTML5 Workspace App updates pretty regularly. That’s a great thing! How about you may have several StoreFront servers. You want to do that manually EVERY time? Of course not! Here is a script that will do that for you! You will also need to go get this function from github that makes this work. The piece of resistance one could say! It lets you get the file version details metadata.

While trying to figure out how to target the HTML5 Workspace App version, I did not see it in registry or in Add/Remove Programs. So I was checking the location of the files to see what I could find. I ended up checking C:\Program Files\Citrix\Receiver StoreFront\HTML5Client and found a file SRI.js that had citrixHTML5Launcher version. I figured I could just check that file and get the version number as it reflected the correct version I had installed previously. I did an upgrade to check if that showed the correct version after the upgrade, and it did. Also found out that once you install it, you cannot go to an earlier version, so be sure to snapshot if you want to do some testing with versions.

Here is the link to the function by Lukas Wohrl that allowed me to get the file version from install file. Get-FileMetaData

Below is the script to do the upgrade:

# Script to check version of CitrixHTML5Client on StoreFront servers, copy upgrade file to upgrade folder, install upgrade, based on version detected.
# You will need the function Get-FileMetaData from This allows you to get the file version and convert to
# [System.Version] type to compare. You will also need to have access to the StoreFront servers as admin to be able run invoke commands and if you need to snapshot, access to your hypervisor. Format for txt file is server.fqdn.

$creds                         = Get-Credential
$storeFronts                   = Get-Content "C:\scripts\logs\storefronts.txt"
$date                          = Get-Date -Format MMddyyyy
$sourceWorkspaceAppLocation    = "fileserver\HTML5Client\upgrade"
$targetFileName                = "CitrixHTML5Client-x64.exe"
$fileInfo                      = Get-FileMetaData "\\$sourceWorkspaceAppLocation\$targetFileName"
$pathToCheckStoreFront         = "C$\Program Files\Citrix\Receiver StoreFront\HTML5Client\"
[System.Version]$targetVersion =  $fileInfo.'File version'
$storeFrontDestinationLocation = "C$\software\html5"
$totalItems                    = $storeFronts.Count
$vCenterAddress                = "vCenter.fqdn"
$storeFrontUpgradeReport       = [PSCustomObject]@{}
$currentItem                   = 0
$percentComplete               = 0

if($global:defaultviserver -eq $null){

  Connect-VIServer $vcenter


foreach($store in $storeFronts){
  $storeFrontUpgradeReport | Add-Member -NotePropertyName "StoreFrontName" -NotePropertyValue "$store"
  Write-Progress -Activity "Checking StoreFront Server $store and upgrading if applicable" -Status "$percentComplete% Complete:" -PercentComplete $percentComplete
  if (!(Test-Path -Path "\\$store\$pathToCheckStoreFront")){

    Write-Host "Path not found on $store. HTML5 Client does not appear to be installed." -BackgroundColor Yellow -ForegroundColor Black

  $html5Version = Invoke-Command -ComputerName $store -ScriptBlock {Get-Content 'C:\Program Files\Citrix\Receiver StoreFront\HTML5Client\SRI.js' | Select-String -Pattern "citrixHTML5Launcher\.([0-9]+(\.[0-9]+)+)" | foreach {$_.Matches[0].Groups[1].Value} }
  [System.Version]$testVersion = $html5Version
  if($testVersion -lt $targetVersion){
      $storeFrontUpgradeReport | Add-Member -NotePropertyName "VersionBeforeUpgrade" -NotePropertyValue "$html5Version"
      Write-Host "Working on $store"
      if (!(Test-Path -Path \\$store\$storeFrontDestinationLocation)) {
        New-Item -ItemType Directory -Path \\$store\c$\software -Name html5
        Copy-Item "\\$sourceWorkspaceAppLocation\$targetFileName" -Destination \\$store\$storeFrontDestinationLocation -Force
      else {
        Copy-Item "\\$sourceWorkspaceAppLocation\$targetFileName" -Destination \\$store\$storeFrontDestinationLocation -Force

      $shortName = $store.Split('.')[0]
      Get-VM $shortName | New-Snapshot -Name "$date-$shortName"

      $session       = New-PsSession -ComputerName $store -Credential $creds
      $remoteSession = Invoke-Command -ScriptBlock {Start-Process -FilePath "C:\software\html5\CitrixHTML5Client-x64.exe" -Verb RunAs} -Session $session
      Start-Sleep -Seconds 30
      $remoteSession2 = Invoke-Command -ComputerName $store -ScriptBlock {Get-Content 'C:\Program Files\Citrix\Receiver StoreFront\HTML5Client\SRI.js' | Select-String -Pattern "citrixHTML5Launcher\.([0-9]+(\.[0-9]+)+)" |foreach {$_.Matches[0].Groups[1].Value} }
      $storeFrontUpgradeReport | Add-Member -NotePropertyName "VersionAfterUpgrade" -NotePropertyValue "$remoteSession2"

  $percentComplete = [int](($currentItem / $totalItems) * 100)
  Start-Sleep -Milliseconds 1000
 Get-PSSession | Remove-PSSession

 $storeFrontUpgradeReport | Export-Csv -Path "C:\scripts\logs\storefront-HTML5-upgrade.csv" -Append -NoTypeInformation

Check That .NET And Install It!

Maybe you need to check the .NET version installed and if it is below a certain version, go install it. When upgrading to 2203LTSR VDA, you have to have 4.8 installed as a prerequisite. This will create a PSSession and remove it when it is done. In your upgrade.txt file, use the FQDN of the target machines. .NET installs seem to take a bit to complete, so be patient in it upgrading. Using the [System.Version] lets you compare major, minor, build, and revision so you can get more granular with checking versions in your test cases.

# Script to check if version is below 4.8 .NET and install 4.8 .NET if so. This requires running from an account that has admin on the destination and also a reboot to complete update.
# Tested on Server 2016 and 2019.
$dotNetSource  = "uncshare\DotNET-48"
$dotNetInstall = "ndp48-x86-x64-allos-enu.exe"
$machineList   = Get-Content "C:\scripts\logs\upgrade.txt"
$dest          = "c$\software\upgrade"
$creds         = Get-Credential

foreach($machine in $machineList){
  $dotnetTest                 = Invoke-Command -ComputerName $machine -ScriptBlock {Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Client' | Get-ItemProperty -name Version}
  $dotnetVersion              = $dotnetTest.Version
  [System.Version]$testResult = $dotnetVersion
  if($testResult.Major -eq 4 -and $testResult.Minor -lt 8){

    Write-Host "Working on $machine"
    if (!(Test-Path -Path \\$machine\c$\software\upgrade)) {
        New-Item -ItemType Directory -Path \\$machine\c$\software -Name upgrade
        Copy-Item "\\$dotNetSource\$dotNetInstall" -Destination \\$machine\$dest -Force
    else {
        Copy-Item "\\$dotNetSource\$dotNetInstall" -Destination \\$machine\$dest -Force

    $session       = New-PsSession -ComputerName $machine -Credential $creds
    $remoteSession = Invoke-Command -ScriptBlock {Start-Process -FilePath "c:\software\upgrade\ndp48-x86-x64-allos-enu.exe" -ArgumentList @("/q")  -wait -Verb RunAs} -Session $session
    Remove-PSSession -Id $

Script To Monitor Group Membership Changes And Report

Sometimes you do adds to a group for reasons. Maybe you are doing a staged migration of things and you are using a group to do that. Maybe someone wants a report of new users added in the last week. This can help do that for you. To complete this, you just need to run a command or two and generate your starting file to compare to. Then you setup a scheduled task to run on a weekly basis and there you have it! If you have had no changes, then it will not email you. If you do have changes, it will email with attachment and count of number of objects added. This also does this based on the named dates of the files so if you need to run it earlier, you will need to do a copy of the file and change the name to reflect a week earlier, or change the logic to not use the date names.

To get your starting file, you will need to create your folder to store the reports and run the command below in PowerShell with a machine that has the AD module loaded as part of the RSAT tools. You will also need the ScheduledTasks module loaded to create the scheduled tasks via PowerShell.

$outputLocation         = "C:\scripts\logs\GroupChanges"
$domain                 = "DomainOfGroup"
$groupName              = "GroupToMonitor"
$date                   = Get-Date
$dateReport             = ($date).ToString("MMddyyyy")
$outputFileName         = "$dateReport-group-members.csv"

$getCurrentGroupMembership = (Get-ADGroup -Server $domain $groupName | Get-ADGroupMember -Recursive | Select-Object -Unique | Sort | Select-Object SamAccountName)

$getCurrentGroupMembership | Export-CSV -Path $outputLocation\$outputFileName -Append -NoTypeInformation

Second verse, somewhat similar to the first.

# Script to do a weekly compare of Active Directory group memberships. Requires AD module to be loaded and an account / service account with read permissions to the objects.

$outputLocation         = "C:\scripts\logs\GroupChanges"
$domain                 = "DomainOfGroup"
$groupName              = "GroupToMonitor"
$date                   = Get-Date
$dateLastWeek           = ($date).AddDays(-7).ToString("MMddyyyy")
$dateReport             = ($date).ToString("MMddyyyy")
$outputFileName         = "$dateReport-group-members.csv"
$importFileNameLastWeek = "$dateLastWeek-group-members.csv"
$deltaFileName          = "$dateReport-delta.csv"
$report                 = @()

$getCurrentGroupMembership = (Get-ADGroup -Server $domain $groupName | Get-ADGroupMember -Recursive | Select-Object -Unique | Sort | Select-Object SamAccountName)

$getCurrentGroupMembership | Export-CSV -Path $outputLocation\$outputFileName -Append -NoTypeInformation

$inputFileNameLastWeek     = Import-Csv -Path "$outputLocation\$importFileNameLastWeek"

$objectCompare     = @{

  ReferenceObject  = ($getCurrentGroupMembership)
  DifferenceObject = ($inputFileNameLastWeek)


$deltaNames = (Compare-Object @objectCompare).InputObject

if($deltaNames -ne $null){

  $deltaNames | Export-Csv -Path $outputLocation\$deltaFileName -Append -NoTypeInformation
  $numberNewUsers = "New Users: " + ($deltaNames).Count

  $report += $deltaNames
  $report += $numberNewUsers

if($deltaNames -eq $null){



$style = "<style>BODY{font-family: Arial; font-size: 10pt;}"
$style = $style + "TABLE{border: 1px solid black; border-collapse: collapse;}"
$style = $style + "TH{border: 1px solid black; background: #dddddd; padding: 5px; }"
$style = $style + "TD{border: 1px solid black; padding: 5px; }"
$style = $style + "</style>"
$body  = $report | ConvertTo-Html -Head $style

$dateEmail        = Get-Date -Format "MM-dd-yyyy"
$emailFrom        = ""
$emailTo          = ""
$subject          = "Weekly Group Check | $dateEmail" 
$email            = New-object System.Net.Mail.MailMessage 
$email.From       = New-Object $emailFrom
$email.Subject    = $subject
$email.IsBodyHtml = $true
$email.body       = $body
$attachment       = "$outputLocation\$deltaFileName"

$smtpserver       = "" 
$smtp             = new-object Net.Mail.SmtpClient($smtpServer)

And after you save that file, you will need to create the scheduled task. Make sure this is a machine that will be on and available during the time you want to run the script. I’m using “weeklychanges.ps1” as the example saved file name, running at 10am on Saturdays. Below will need to be ran from an elevated prompt / ISE in order to create the task without getting an “Access denied” error.

$taskName    = 'WeeklyGroupCheck'
$time        = "10am"
$user        = "domain\user"
$credentials = Get-Credential -Credential $user
$password    = $credentials.GetNetworkCredential().Password
$scriptPath  = "c:\scripts\weeklycheck.ps1"
$trigger     = New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek Saturday
$action      = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -noprofile -file $scriptPath" 

Register-ScheduledTask -TaskName $taskName -Trigger $trigger -User $user -Password $password -Action $action -RunLevel Highest -Force

So Tell Me A Storyboard…

So I had a user that had requested the Storyboard feature of PowerPoint. I found a couple ways that you could do it. You can install the Visual Studio Community Edition with installing either the Universal Platform Windows development (~8GB) or Office / Sharepoint development (~17GB). That looked rather large install for the feature. Also found another option for enabling the feature, installing the Microsoft Team Foundation Office Integration. That was less than ~1GB. That seemed the better option. So went with that! So if you need some storyboarding powers, the TFS Office Integration seems the easier and faster method to get it going.

Article with links: Storyboard with Visual Studio

Microsoft Team Foundation Server Office Integration: Microsoft TFS Office Integration

Order Of Operations, YMMV : Authentication Class Type Options For SAML Authentication Server

So you got your SAML Authentication server all configured. You got your MFA rolling. You start your day. You open up another app that has an assigned enterprise application to it with conditional access set. Then you open up your Citrix tab. You go to the site. It redirects you. And BOOM. Just like that. ERROR!!!

You think think think and think about why you are getting the error. You know when you opened Citrix FIRST yesterday the world was all sunny and bright. But today, that is not the case. But you really read the error closely. And you notice something… Authentication method ‘Password.’ You know that when you opened Citrix yesterday with your password and MFA, then continued on, it all worked. But change the order, and it does not. So. You go and check your SAML authentication settings.

You’ll see that by default, the “Password” class type is selected when you create the SAML authentication server. If you click on it so it is no longer blue, then save it, you notice that everything seems to work. So anything that is set there is EXPECTED in the assertion, not what is ACCEPTED. This would happen more if you have conditional access to not prompt on prem for one app, and prompt always on the other enterprise app. If you clear that, it will allow you to use the SAML assertion you got from the other app, assuming it is with the same IDP. There is also another option that you see outlined. “Force Authentication.” This option, if set, will force the session you start to redo the authentication and not use anything that you have cached. This is also good for testing purposes to force it to go through the authentication process.

Replacing Your Certificate On Session Recording Server

With the wonders of doing your certificates on a much more frequent basis now, this becomes a yearly task. If you are using the WebPlayer feature of Session Recording on-prem (it is really nice), there is a little more tedious process you have to complete.

This link has the overview you need to get you through the process. The only step I did not see in the article was the startwebsocketserver command at the very end. The location of the SsRecWebSocketServer.exe.config file you can find in the C:\Program Files\Citrix\SessionRecording\Server\Bin folder. I recommended making a copy of the file before you start.

But for your steps….

Get the certificate from your certificate provider of choice.

Import the certificate onto the Session Recording server.

Bind the certificate in IIS.

Bind the certificate in the Session Recording Server properties.

Export the cert as PFX to a local folder.

Perform the operations in the link above using OpenSSL to convert the exported PFX into a PEM file and extract the key file.

Modify the SsRecWebSocketServer.exe.config file in the C:\Program Files\Citrix\SessionRecording\Server\Bin.

Enter the location for the cert file and the key file.

Save file.

Open an elevated command prompt.

Navigate to the C:\Program Files\Citrix\SessionRecording\Server\Bin folder.

Enter “TestPolicyAdmin.exe -stopwebsocketserver” and press enter.

Enter “TestPolicyAdmin.exe -startwebsocketserver” and press enter.

The WebPlayer should be working as expected. If you do not update the SsRecWebSocketServer.exe.config file, the WebPlayer will give a WebSocket error.

Dazed And ConFAS’d : Cipher Suites For FAS And EndGame Exceptions For VDI

Ran into some fun with setting up FAS for MFA. I was testing a shorter list of ciphers on a test SSL profile on ADC on the test vServer. Come to find out, when accessing a machine that was using MFA from outside the network, I was getting an SSL error 4 on Windows machines and SSL error 47 on Stratodesk machines. I hadn’t seen that error since Receiver 4.x. It appears there are some additional ciphers needed in regards to the Citrix Workspace App. It appeared to work fine with the other cipher set using the HTML5 Workspace App. This article has the updated cipher set you need to have or it may cause you some issues (Changes To FAS Ciphers). These would be applied to your SSL profile assigned to the vServer on the ADC.

Ciphers needed in the SSL profile that are in link above

I also ran into an issue with EndGame.

When trying to connect from to VDI Windows 10 machines, you would encounter an incorrect user name or password error if EndGame was enabled, instead of it SSO logging you in.

Checking the event log on the machine, you encounter a Smart Card Logon Event 5.

There are 2 DLLs you have to add to a global exclusion, scardhook.dll and scardhook64.dll. These are located under C:\Program Files\Citrix\ICAService. Just excluding those DLLs got rid of the Event 5 Smart Card Logon error and allowed the Provider DLL to initialize.

After getting these exclusions applied, SSO works normally for accessing the VDI machines.

How Many Users Are In There? : Getting Group Membership Counts

So you want to know how many people are in the groups. As luck would have it, you can get that. There was an interesting thing that I encountered with Get-ADGroupMember when trying to return a count. If there were 0 members, it returned correctly. If there were 2 or more, it returned correctly. If there was 1 member, it returned nothing. It wasn’t null as I checked that. It just returned nothing. Found the answer on this site as to why it was doing it:Why it returned nothing. TLDR: From Martin9700, PowerShell, when only 1 object is returned it is returned AS that object. Count is property of an array (and you can have an array of pretty much any variable/object type)

So with that in mind, I went the route below to do a Measure-Object, then do the count. That returned the results I expected. I also wanted to only select the unique users in the group just in case there were nested groups that a user might have been in more than one of.

# Script to get user group counts. This requires the AD Powershell module, access rights to AD, a central location of Citrix groups, a naming convention, and used in Powershell ISE 5.1.
$domain          = "domain"
$searchBase      = "OU=CitrixGroups,OU=Groups,DC=somecompany,DC=com"
$getCtxGroups    = (Get-ADGroup -server $domain -SearchBase $searchBase -Filter {SamAccountName -like "CitrixGroupNamePattern*"} | Select-Object SamAccountName)
$totalItems      = ($getCtxGroups).Count
$date            = Get-Date -Format MMddyyyy
$report          = @()
$currentItem     = 0
$percentComplete = 0

ForEach($ctxGroup in $getCtxGroups){
  Write-Progress -Activity "Processing user count on ($ctxGroup).SamAccountName" -Status "$PercentComplete% Complete:" -PercentComplete $PercentComplete
  $userCount      = (Get-ADGroup -Server $domain $ctxGroup.SamAccountName | Get-ADGroupMember -Recursive | Select-Object -Unique | Measure-Object).Count
  $line           = "" | Select GroupName, UserCount
  $line.GroupName = $ctxGroup.SamAccountName
  if($userCount -ne 0){
    $line.UserCount = $userCount
  if($userCount -eq 0){
    $line.UserCount = "Empty"
  $percentComplete = [int](($currentItem / $totalItems) * 100)
  $report += $line

$report | Export-Csv c:\scripts\logs\$date-ADUserGroup-Counts.csv -Append -NoTypeInformation

Are You The Keymaster!? : Script To Change ListOfDDCs in Registry

You have an upcoming change and some new DDCs you brought online. You may be changing out to Citrix Cloud (you better be), and you may need to change the ListofDDCs to you Cloud Connector. Sometimes GPO may take a minute to reflect what you want set. You can use this to change the ListOfDDCs quickly. You can also add the ListofSSIDs if that is something that you use by adding another registry name and value in your script block. I have the Get-ItemProperty used twice to get the result of what was set before the change and to show the reflected change. I just like to doubly confirm something and make sure something hinky was not afoot.

# Script to change DDCs on a group of Citrix servers. You will need access to the remote servers and firewall access with PowerShell.
$listServers = Get-Content c:\scripts\logs\svrlist.txt
$date        = Get-Date -Format MMddyyyy
$report      = @()

foreach($srv in $listServers) {

  $scriptBlock = {
    $regName  = "ListOfDDCs"
    $regValue = "DDC1 DDC2 or CC1 CC2"
    Get-ItemProperty -Path HKLM:\Software\Citrix\VirtualDesktopAgent
    Set-ItemProperty -Path HKLM:\Software\Citrix\VirtualDesktopAgent -Name $regName -Value $regValue
    Get-ItemProperty -Path HKLM:\Software\Citrix\VirtualDesktopAgent

  $ddcUpdate  = Invoke-Command -ComputerName $srv -ScriptBlock $scriptBlock
  $report += $ddcUpdate

$report | Out-File c:\scripts\logs\$date-ddcchange-list.txt

Powered by WordPress & Theme by Anders Norén