Application virtualization, IoT and Cloud Computing, Blog of Sacha Thomet


XenDesktop & XenApp FMA (7.x) HealthCheck – Oops!… I Did It Again

Some months ago I’ve created the Citrix PVS Health-Check Script which is a based on the idea and some parts of code from the Health-Check Script for XenApp 6.x of Jason Poyer ( .
Because now XenApp 7 with the Release 7.6 is finally in a state where considering an upgrade of the 6.x farms make sense, I belief that the demand for a XenApp 7.x Health Check Script grows.

So I did it again and took the “HealthCheck framework” to build a new version which combines the Power of the Citrix PS-Snappins for XenDesktop/XenApp and  the HTML-Output-Script of the existing HealthCheck Scripts.

The result is a new HealthCheck Script which is usable for XenApp and XenDesktop 7.x and what makes me also happy, with only a few line of more code the Script is downwardly compatible for XenDesktop 5.6 environments.

  XenDesktop XenApp Health Check HTML Output

This is just the first version and I’m sure that more check’s need to be added. Feedback and “Feature requests” are welcome … And to be honest I have not yet a big environment to test my Script, so please be insightfully if you find some bugs and report them to me.

In the first part of the Script you are able to configure some parameters. You can decide if you only want to see the “bad” Desktops on which something it’s going wrong or if you want see everything. In huge XenDesktop environments you want definitely only see the bad machines …  ( $ShowOnlyErrorVDI = 1 ) Also you can decide if you want only report XenApp or only XenDesktop or both. The Desktops and XenApps are in two different Tables. It’s also possible to exclude Collections ($ExcludeCatalogs) from the Check, so virtual Desktops which are for testing purposes are not checked.



If you have a feature request or a bug report please post it direct on GitHub.

Update 12.05.2016 (Version 0.95):
– Check CPU, Memory and C: of Controllers
– XenApp: Add values: CPU & Memory and Disk Usage
– XenApp: Option to toggle on/off to show Connected Users
– XenApp: DesktopFree set to N/A because not relevant
If you need a Health Check Script for XenApp Version which are older than XenApp 7.x see where it’s an excellent work and the inspiration for my HealthCheck-Scripts!

The code is on GitHub:

For Bug Reports or feature Request please use GitHub, of course you can also contribute on this code!

Citrix PVS HealthCheck

Problem: There is currently no free tool to check the health status of a Citrix Provisioning Services farm with all Citrix relevant parameters. There are monitoring tools which tell you if a server is up and running or if it’s not, but now tool who gives you a special overview for Citrix Provisioning Services according to how much targets on which server, how much communication retries on which target an so on.

Since some year’s I’m using the XenApp Health Check from Jason Poyner ( to get daily a status of my environment(s). I really appreciate the work from Jason, if you don’t know it until now, you really need have a look on his blog and on the Script . The mentioned XenApp Farm Health Check Script gave me the inspiration to create a similar Health Check Script for my Citrix Provisioning Server farm. To be honest, and I’m happy that Jason doesn’t mind, that I have “recycled” some parts, or say the “framework” of his XenApp Farm Health Check script to build my Script.

The script I built can be scheduled with a Windows task which trigger a run once a day and send out a email with the attached HTML-Report:

I’ve written the Script to perform the checks in 3 Parts: 

Target Devices:

  • Ping
  • Retry (Threshold configurable, default is 15)
  • PVS Disk (Value which I read from PVS farm with get-mcli)
  • PVS Disk (Value which is written in the personality.ini on the Target)
  • PVS disk version (V1.2)
  • The vDisk-Store *
  • PVS Server
  • WriteCache Size

* like you can read in my BlogPost “Achieve fastest Citrix Provisioning Target Device” I propose to have the vDisk Store local on each provisioning server. So I can check with that column if the Target is booting from the correct vDisk store. The Master Store is on CIFS and only used for updates of the Image.


  • Replication state (PVS don’t replicate anything – but can check if the vDisks on all servers are equal)
  • DeviceCount to see how many targets are currently reading from this Image
  • Load Balancing algorithm
    (Green if Best Effort because I want to have subnet affinity – feel free to change for your need.  I have two data centers with two separated subnet’s – so I can guarantee that each target stream from his own data centre as long the server is available. Just in case of an outage of all PVS server in one DC it fail over to the other DC  )
  • WriteCache Type
  • Name & Date of the vDisk

PVS Servers:

  • Ping
  • Active
  • How many device are connected

Don’t forget to register the PVS SnapIn DLL e.g. with .Net Framework 2.0 with the command:

“C:\WINDOWS\Microsoft.NET\Framework64\v2.0.50727\InstallUtil.exe” “C:\Program Files\Citrix\Provisioning Services Console\McliPSSnapIn.dll”

Otherwise you get a blank output. If you have another .Net Framework version or path, please adjust the path in the command! 

# Created on: 08.2014            Version: 1.63
# Created by: Sacha Thomet, /
# Filename: Citrix-PVS-Farm-Health-toHTML.ps1
# Special Thanks to:
# - Jason Poyner ... I've borrowed parts of the script and ideas to create this 
#   PVS health check script. Check his excellent XenApp Health Check @
# - Martin Hartmann to share his PowerShell KnowHow with me.
# Description: This script checks Citrix Provisioning Server, Farm, vDisk & Target devices.
# Prerequisite: Script must run on a PVS server, where MCLI snap-in is registered.
# Register SnapIn with command: C:\WINDOWS\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe 
# "C:\Program Files\Citrix\Provisioning Services Console\McliPSSnapIn.dll"
# Call by : Scheduled Task, e.g. once a day
# Change Log: 
#      V1.1:  Consolidated code
#      V1.2:  Add possibility to check only specified Versions
#      V1.3:  New version after Citrix Synergy 2015 GeekOvation Award nomination. 
#		      With correction of typos and add some documentation lines. 
#      V1.4:  Possibility for multiple stores (Thanks to Kafedzhiev  for the code) (08-2015) 
#      V1.5:  Show create date of vDisk, FileName and the count of the used vDisk (09-2015)
#      V1.6:  Add RamCache used from each target, added code by Jonathan Pitre, 
#		      code from Matthew Nics (10-2015) 
#      V1.61: Changed RamCache to general WriteCache, add possibility to get Size of Cache on HD
#      V1.62: Correction in Header to show correct farm name instead a "6", correction in 
#		      LoadBalancingAlgorithm, Error if a disk is assigned fix to a server. 
#      V1.63: Check of Stream-, Soap-, and TFTP-Service (12-2015)
if ((Get-PSSnapin "McliPSSnapIn" -EA silentlycontinue) -eq $null) {
try { Add-PSSnapin McliPSSnapIn -ErrorAction Stop }
catch { write-error "Error loading PVS McliPSSnapIn PowerShell snapin"; Return }
# Change the below variables to suit your environment
# Target Device Health Check threshold:
$retrythresholdWarning= "15" # define the Threshold from how many retries the color switch to red
# Include for Device Collections, type "every" if you want to see every Collection 
# Example1: $Collections = @("XA65","XA5")
# Example2: $Collections = @("every")
$Collections = @("every")
# Information about the site you want to check:
$siteName="site" # site name on which the according Store is.
# E-mail report details
$emailFrom = ""
$emailTo = ""#,""
$smtpServer = ""
$emailSubjectStart = "PVS Farm Report"
$mailprio = "High"
$currentDir = Split-Path $MyInvocation.MyCommand.Path
$logfile = Join-Path $currentDir ("PVSHealthCheck.log")
$resultsHTM = Join-Path $currentDir ("PVSFarmReport.htm")
$errorsHTM = Join-Path $currentDir ("PVSHealthCheckErrors.htm") 
#Header for Table 1 "Target Device Checks"
$TargetfirstheaderName = "TargetDeviceName"
$TargetheaderNames = "CollectionName", "Ping", "Retry", "vDisk_PVS", "vDisk_Version", "WriteCache", "PVSServer"
$TargetheaderWidths = "4", "4", "4", "4", "2" , "4", "4"
$Targettablewidth = 1200
#Header for Table 2 "vDisk Checks"
$vDiksFirstheaderName = "vDisk"
$vDiskheaderNames = "Store", "vDiskFileName", "deviceCount", "CreateDate" , "ReplState", "LoadBalancingAlgorithm", "WriteCacheType"
$vDiskheaderWidths = "4", "8", "2","4", "4", "4", "4"
$vDisktablewidth = 1200
#Header for Table 3 "PV Server"
$PVSfirstheaderName = "PVS Server"
$PVSHeaderNames = "Ping", "Active", "deviceCount","SoapService","StreamService","TFTPService"
$PVSheaderWidths = "4", "4", "4","4","4","4"
$PVStablewidth = 600
#Header for Table 4 "Farm"
$PVSFirstFarmheaderName = "FarmChecks"
$PVSFarmHeaderNames = "Setting", "Value"
$PVSFarmWidths = "4", "8", "8"
$PVSFarmTablewidth = 400
#log function
function LogMe() {
[parameter(Mandatory = $true, ValueFromPipeline = $true)] $logEntry,
 if ($error) {
$logEntry = "[ERROR] $logEntry" ; Write-Host "$logEntry" -Foregroundcolor Red}
elseif ($warning) {
Write-Warning "$logEntry" ; $logEntry = "[WARNING] $logEntry"}
elseif ($progress) {
Write-Host "$logEntry" -Foregroundcolor Green}
elseif ($display) {
Write-Host "$logEntry" }
 #$logEntry = ((Get-Date -uformat "%D %T") + " - " + $logEntry)
$logEntry | Out-File $logFile -Append
function Ping([string]$hostname, [int]$timeout = 200) {
$ping = new-object System.Net.NetworkInformation.Ping #creates a ping object
try {
$result = $ping.send($hostname, $timeout).Status.ToString()
} catch {
$result = "Failure"
return $result
Function writeHtmlHeader
param($title, $fileName)
$date = ( Get-Date -format R)
$head = @"
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>

<STYLE TYPE="text/css">
<!-- td { font-family: Tahoma; font-size: 11px; border-top: 1px solid #999999; border-right: 1px solid #999999; border-bottom: 1px solid #999999; border-left: 1px solid #999999; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; overflow: hidden; } body { margin-left: 5px; margin-top: 5px; margin-right: 0px; margin-bottom: 10px; table { table-layout:fixed; border: thin solid #000000; } -->


<table width='1200'>

<tr bgcolor='#CCCCCC'>

<td colspan='7' height='48' align='center' valign="middle">
<font face='tahoma' color='#003399' size='4'>
<strong>$title - $date</strong></font>



$head | Out-File $fileName
# ==============================================================================================
Function writeTableHeader
param($fileName, $firstheaderName, $headerNames, $headerWidths, $tablewidth)
$tableHeader = @"

<table width='$tablewidth'>


<tr bgcolor=#CCCCCC>

<td width='6%' align='center'><strong>$firstheaderName</strong></td>

$i = 0
while ($i -lt $headerNames.count) {
$headerName = $headerNames[$i]
$headerWidth = $headerWidths[$i]
$tableHeader += "

<td width='" + $headerWidth + "%' align='center'><strong>$headerName</strong></td>

$tableHeader += "</tr>

$tableHeader | Out-File $fileName -append
# ==============================================================================================
Function writeTableFooter

"| Out-File $fileName -append
Function writeData
param($data, $fileName, $headerNames)
 $data.Keys | sort | foreach {
$tableEntry += "

$computerName = $_
$tableEntry += ("

<td bgcolor='#CCCCCC' align=center><font color='#003399'>$computerName</font></td>

#$data.$_.Keys | foreach {
$headerNames | foreach {
#"$computerName : $_" | LogMe -display
try {
if ($data.$computerName.$_[0] -eq "SUCCESS") { $bgcolor = "#387C44"; $fontColor = "#FFFFFF" }
elseif ($data.$computerName.$_[0] -eq "WARNING") { $bgcolor = "#FF7700"; $fontColor = "#FFFFFF" }
elseif ($data.$computerName.$_[0] -eq "ERROR") { $bgcolor = "#FF0000"; $fontColor = "#FFFFFF" }
else { $bgcolor = "#CCCCCC"; $fontColor = "#003399" }
$testResult = $data.$computerName.$_[1]
catch {
$bgcolor = "#CCCCCC"; $fontColor = "#003399"
$testResult = ""
 $tableEntry += ("

<td bgcolor='" + $bgcolor + "' align=center><font color='" + $fontColor + "'>$testResult</font></td>

 $tableEntry += "</tr>

 $tableEntry | Out-File $fileName -append
# ==============================================================================================
Function writeHtmlFooter


<table width='1200'>

<tr bgcolor='#CCCCCC'>

<td colspan='7' height='25' align='left'>

<font face='courier' color='#000000' size='2'><strong>Retry Threshold =</strong></font><font color='#003399' face='courier' size='2'> $retrythresholdWarning


<tr bgcolor='#CCCCCC'>


<tr bgcolor='#CCCCCC'>


"@ | Out-File $FileName -append
rm $logfile -force -EA SilentlyContinue
"Begin with Citrix Provisioning Services HealthCheck" | LogMe -display -progress
" " | LogMe -display -progress
# ======= PVS Target Device Check ========
"Check PVS Target Devices" | LogMe -display -progress
" " | LogMe -display -progress
$allResults = @{}
$pvsdevices = mcli-get device -f deviceName | Select-String deviceName
foreach($target in $pvsdevices)
 $tests = @{} 
 # Check to see if the server is in an excluded folder path
$target | Select-String deviceName 
 $_targetshort = $target -replace "deviceName: ",""
 $pvcollectionName = mcli-get deviceinfo -p devicename=$_targetshort | select-string collectionName
$short_collectionName = $pvcollectionName.ToString().TrimStart("collectionName: ")
 #Only Check Servers in defined Collections: 
 if ($Collections -contains $short_collectionName -Or $Collections -contains "every") { 
 $target | Select-String deviceName 
 $_targetshort = $target -replace "deviceName: ",""
$_targetshort | LogMe -display -progress
 # Ping server 
 $result = Ping $_targetshort 100
if ($result -ne "SUCCESS") { $tests.Ping = "ERROR", $result }
else { $tests.Ping = "SUCCESS", $result 
$pvcollectionName = mcli-get deviceinfo -p devicename=$_targetshort | select-string collectionName
$short_collectionName = $pvcollectionName.ToString().TrimStart("collectionName: ")
$tests.CollectionName = "NEUTRAL", "$short_collectionName"
 # Test Retries
$devicestatus = mcli-get deviceinfo -p devicename=$_targetshort -f status
$retrycount = $devicestatus[4].TrimStart("status: ") -as [int]
if ($retrycount -lt $retrythresholdWarning) { $tests.Retry = "SUCCESS", "$retrycount Retry = OK" }
else { $tests.Retry = "WARNING","$retrycount retries!" }
 #Check assigned Image
$devicediskFileName = mcli-get deviceinfo -p devicename=$_targetshort | select-string diskFileName
$short_devicediskFileName = $devicediskFileName.ToString().TrimStart("diskFileName: ")
$tests.vDisk_PVS = "SUCCESS", "$short_devicediskFileName"
 #Check assigned Image Version
$devicediskVersion = mcli-get deviceinfo -p devicename=$_targetshort | select-string diskVersion:
$short_devicediskVersion = $devicediskVersion.ToString().TrimStart("diskVersion: ")
$tests.vDisk_Version = "SUCCESS", "$short_devicediskVersion"
$PVSServername = mcli-get deviceinfo -p devicename=$_targetshort | select-string serverName
$short_PVSServername = $PVSServername.ToString().TrimStart("serverName: ")
$tests.PVSServer = "Neutral", "$short_PVSServername"

################ PVS WriteCache SECTION ###############

		if (test-path \\$_targetshort\c$\Personality.ini)

			$wconhd = ""
			$wconhd = Get-Content \\$_targetshort\c$\Personality.ini | Where-Object  {$_.Contains("WriteCacheType=4") }
			If ($wconhd -match "$WriteCacheType=4") {Write-Host Cache on HDD
			#WWC on HD is $wconhd

				# Relative path to the PVS vDisk write cache file
				$PvsWriteCache   = "d$\.vdiskcache"
				# Size of the local PVS write cache drive
				$PvsWriteMaxSize = 10gb # size in GB
				$PvsWriteCacheUNC = Join-Path "\\$_targetshort" $PvsWriteCache 
				$CacheDiskexists  = Test-Path $PvsWriteCacheUNC
				if ($CacheDiskexists -eq $True)
					$CacheDisk = [long] ((get-childitem $PvsWriteCacheUNC -force).length)
					$CacheDiskGB = "{0:n2}GB" -f($CacheDisk / 1GB)
					"PVS Cache file size: {0:n2}GB" -f($CacheDisk / 1GB) | LogMe
					#"PVS Cache max size: {0:n2}GB" -f($PvsWriteMaxSize / 1GB) | LogMe -display
					if($CacheDisk -lt ($PvsWriteMaxSize * 0.5))
					   "WriteCache file size is low" | LogMe
					   $tests.WriteCache = "SUCCESS", $CacheDiskGB
					elseif($CacheDisk -lt ($PvsWriteMaxSize * 0.8))
					   "WriteCache file size moderate" | LogMe -display -warning
					   $tests.WriteCache = "WARNING", $CacheDiskGB
					   "WriteCache file size is high" | LogMe -display -error
					   $tests.WriteCache = "ERORR", $CacheDiskGB
				$Cachedisk = 0
				$VDISKImage = get-content \\$_targetshort\c$\Personality.ini | Select-String "Diskname" | Out-String | % { $_.substring(12)}
				if($VDISKImage -Match $DefaultVDISK){
					"Default vDisk detected" | LogMe
					$tests.vDisk = "SUCCESS", $VDISKImage
				} else {
					"vDisk unknown"  | LogMe -display -error
					$tests.vDisk = "SUCCESS", $VDISKImage
			{Write-Host Cache on Ram
			#Get-RamCache from each target, code from Matthew Nics
			$RAMCache = [math]::truncate((Get-WmiObject Win32_PerfFormattedData_PerfOS_Memory -ComputerName $_targetshort).PoolNonPagedBytes /1MB)
			$tests.WriteCache = "Neutral", "$RamCache MB on Ram"
		{Write-Host WriteCache not readable
		$tests.WriteCache = "Neutral", "Cache not readable"	
		############## END PVS WriteCache SECTION #############

#Forward results to $allResult array which will be written in HTM-File
$allResults.$_targetshort = $tests
# ======= PVS vDisk Check #==================================================================
"Check PVS vDisks" | LogMe -display -progress
" " | LogMe -display -progress
$storenames = mcli-get store | Select-string storename
$vdiskResults = @{}
foreach ($storenameA in $storenames)
$storename = $storenameA -replace "storename: ",""
$storeid = mcli-get store -p storeName=$storename | Select-String storeId
$storeid_short = $storeid -replace "storeId: ",""
$alldisks = Mcli-Get disklocator -p siteName=$siteName, storeId=$storeid_short | Select-String diskLocatorName
foreach($disk in $alldisks)
$disk1 = $disk | Select-String diskLocatorName
$disklocator_short = $disk1 -replace "diskLocatorName: ",""
foreach($diksloc in $disklocator_short)
 $VDtests = @{} 
 $DiskVersion = Mcli-Get DiskVersion -p diskLocatorName=$disklocator_short, siteName=$siteName, storeName=$storename
$diskreplstatus = $DiskVersion | Select-String goodInventoryStatus
$diskreplstatus_short = $diskreplstatus -replace "goodInventoryStatus: ","" 
 # vDiskFileName & createDate 
 $pathA = mcli-get store -p storeName=$storename | Select-String path -casesensitive
$path = $pathA -replace "path: ",""
 $diskfilenameA = Mcli-Get DiskVersion -p diskLocatorName=$disklocator_short, siteName=$siteName, storeName=$storename | Select-String diskFileName 
 $diskfilename = $diskfilenameA -replace "diskFileName: ","
 $createDateA = Mcli-Get DiskVersion -p diskLocatorName=$disklocator_short, siteName=$siteName, storeName=$storename | Select-String createDate 
 $createDate = $createDateA -replace "createDate: ","
 $VDtests.vDiskFileName = "OK", " $diskfilename"
Write-Host ("Path is $path $disklocator_short $diskfilename")
 $VDtests.createDate = "OK", " $createDate"
Write-Host ("Path is $path $disklocator_short $createDate")
 $vdiskResults.$disklocator_short = $VDtests
 #Check if correct replicated
if($diskreplstatus_short -eq 1 ){
"$disklocator_short correct replicated" | LogMe
$VDtests.ReplState = "SUCCESS", "Replication is OK"
 } else {
"$disklocator_short not correct replicated " | LogMe -display -error
$VDtests.ReplState = "ERROR", "Replication is NOT OK"
 # Check deviceCount: 
 $diskdevicecount = $DiskVersion | Select-String deviceCount
$diskdevicecounts_short = $diskdevicecount -replace "deviceCount: ","
 $VDtests.deviceCount = "OK", "$diskdevicecounts_short "
 #Label Storename 
 $VDtests.Store = "OK", " $storename "
Write-Host ("Store is $storename")
 $vdiskResults.$disklocator_short = $VDtests
# Check for LB-Algorithm
# ----------------------
# Feel free to change it to the the from you desired State (e.g.Exchange a SUCCESS with a WARNING)
# In this default configuration "BestEffort" or "None" is desired and appears green on the output.
# is desired)

#ServeName must be empty! otherwise no LB is active!
$LBnoServer = ""
$LBnoServer_short = ""
$LBnoServer = Mcli-Get disklocator -p siteName=$siteName, storeName=$storename, diskLocatorName=$disklocator_short | Select-String serverName
$LBnoServer_short = $LBnoServer -replace "serverName: ","" 
Write-Host ("vDisk is fix assigned to $LBnoServer")
#not assigned to a server
if ($LBnoServer_short -eq "")
		$LBAlgo = Mcli-Get disklocator -p siteName=$siteName, storeName=$storename | Select-String subnetAffinity
		$LBAlgo_short = $LBAlgo -replace "subnetAffinity: ","" 
		#SubnetAffinity: 1=Best Effort, 2= fixed, 0=none
		if($LBAlgo_short -eq 1 ){
		"LB-Algorythm is set to BestEffort" | LogMe
		$VDtests.LoadBalancingAlgorithm = "SUCCESS", "LB is set to BEST EFFORT"} 
		 elseif($LBAlgo_short -eq 2 ){
		"LB-Algorythm is set to fixed" | LogMe
		$VDtests.LoadBalancingAlgorithm = "WARNING", "LB is set to FIXED"}
		 elseif($LBAlgo_short -eq 0 ){
		"LB-Algorythm is set to none" | LogMe
		$VDtests.LoadBalancingAlgorithm = "SUCCESS", "LB is set to NONE, least busy server is used"}


#Disk fix assigned to a server
$VDtests.LoadBalancingAlgorithm = "ERROR", "vDisk is fix assigned to $LBnoServer, no LoadBalancing!"}
 #Check for WriteCacheType
# -----------------------
# Feel free to change it to the the from you desired State (e.g.Exchange a SUCCESS with a WARNING)
# In this default configuration, only "Cache to Ram with overflow" and "Cache to Device Hard disk" is desired and appears green on the output.
 $WriteCacheType = Mcli-Get DiskInfo -p diskLocatorName=$disklocator_short, siteName=$siteName, storeName=$storename
$WriteCacheType_short = $WriteCacheType -replace "WriteCacheType: ",""
 #$WriteCacheType 9=RamOfToHD 0=PrivateMode 4=DeviceHD 8=DeviceHDPersistent 3=DeviceRAM 1=PVSServer 7=ServerPersistent 
 if($WriteCacheType_short -eq 9 ){
"WC is set to Cache to Device Ram with overflow to HD" | LogMe
$VDtests.WriteCacheType = "SUCCESS", "WC Cache to Ram with overflow to HD"}
 elseif($WriteCacheType_short -eq 0 ){
"WC is not set because vDisk is in PrivateMode (R/W)" | LogMe
$VDtests.WriteCacheType = "Error", "vDisk is in PrivateMode (R/W) "}
 elseif($WriteCacheType_short -eq 4 ){
"WC is set to Cache to Device Hard Disk" | LogMe
$VDtests.WriteCacheType = "SUCCESS", "WC is set to Cache to Device Hard Disk"}
 elseif($WriteCacheType_short -eq 8 ){
"WC is set to Cache to Device Hard Disk Persistent" | LogMe
$VDtests.WriteCacheType = "Error", "WC is set to Cache to Device Hard Disk Persistent"}
 elseif($WriteCacheType_short -eq 3 ){
"WC is set to Cache to Device Ram" | LogMe
$VDtests.WriteCacheType = "WARNING", "WC is set to Cache to Device Ram"}
 elseif($WriteCacheType_short -eq 1 ){
"WC is set to Cache to PVS Server HD" | LogMe
$VDtests.WriteCacheType = "Error", "WC is set to Cache to PVS Server HD"}
 elseif($WriteCacheType_short -eq 7 ){
"WC is set to Cache to PVS Server HD Persistent" | LogMe
$VDtests.WriteCacheType = "Error", "WC is set to Cache to PVS Server HD Persistent"}

# ======= PVS Server Check ==================================================================
"Check PVS Servers" | LogMe -display -progress
" " | LogMe -display -progress
$PVSResults = @{}
$allPVSServer = mcli-get server | Select-String serverName
foreach($PVServerName in $allPVSServer)
$PVStests = @{} 
 $PVServerName1 = $PVServerName | Select-String serverName
$PVServerName_short = $PVServerName1 -replace "serverName: ","" 
 # Ping server 
 $result = Ping $PVServerName_short 100
if ($result -ne "SUCCESS") { $PVStests.Ping = "ERROR", $result }
else { $PVStests.Ping = "SUCCESS", $result 
 #Check PVS Service Status
$serverstatus = mcli-get ServerStatus -p serverName=$PVServerName_short -f status
$actviestatus = $serverstatus[4].TrimStart("status: ") -as [int]
if ($actviestatus -eq 1) { $PVStests.Active = "SUCCESS", "active" }
else { $PVStests.Active = "Error","inactive" }

# Check services
		if ((Get-Service -Name "soapserver" -ComputerName $PVServerName_short).Status -Match "Running") {
			"SoapService running..." | LogMe
			$PVStests.SoapService = "SUCCESS", "Success"
		} else {
			"SoapService service stopped"  | LogMe -display -error
			$PVStests.SoapService = "ERROR", "Error"
		if ((Get-Service -Name "StreamService" -ComputerName $PVServerName_short).Status -Match "Running") {
			"StreamService service running..." | LogMe
			$PVStests.StreamService = "SUCCESS","Success"
		} else {
			"StreamService service stopped"  | LogMe -display -error
			$PVStests.StreamService = "ERROR","Error"
		if ((Get-Service -Name "BNTFTP" -ComputerName $PVServerName_short).Status -Match "Running") {
			"TFTP service running..." | LogMe
			$PVStests.TFTPService = "SUCCESS","Success"
		} else {
			"TFTP  service stopped"  | LogMe -display -error
			$PVStests.TFTPService = "ERROR","Error"
 #Check PVS deviceCount
$serverdevicecount = mcli-get ServerStatus -p serverName=$PVServerName_short -f deviceCount
$numberofdevices = $serverdevicecount[4].TrimStart("deviceCount: ") -as [int]
if ($numberofdevices -gt 1) { $PVStests.deviceCount = "SUCCESS", " $numberofdevices active" }
else { $PVStests.deviceCount = "WARNING","No devices on this server" }
 $PVSResults.$PVServerName_short = $PVStests
# ======= PVS Farm Check ====================================================================
"Read some PVS Farm Parameters" | LogMe -display -progress
" " | LogMe -display -progress
$PVSFarmResults = @{}
$PVSfarms = mcli-get Farm #| Select-String FarmName

$farmname = mcli-get Farm | Select-String FarmName
$farmname_short = $farmname -replace "farmName: ",""

foreach($PVSFarm in $PVSfarms)
$PVSFarmtests = @{}
# remove not needed record parts
if ($PVSFarm -like '*description*'){continue;}
if ($PVSFarm -like '*record*'){continue;}
if ($PVSFarm -like '*failover*'){continue;}
if ($PVSFarm -like '*executing*'){continue;}
if ($PVSFarm -like '*defaultSiteName*'){continue;}
if ($PVSFarm -like '*autoAddEnabled*'){continue;}
if ($PVSFarm -like '*role*'){continue;}
if ($PVSFarm -like '*audit*'){continue;}
if ($PVSFarm -like '*defaultSiteId*'){continue;}
if ($PVSFarm -like '*maxVersions*'){continue;}
if ($PVSFarm -like '*databaseInstanceName*'){continue;}
if ($PVSFarm -like '*farmId*'){continue;}
if ($PVSFarm -like '*merge*'){continue;}
if ($PVSFarm -like '*adGroups*'){continue;}
 if ($PVSFarm -ne '') {
$Nr += 1
$arr = $PVSFarm -split ': '
$farmsetting = $arr[0]
$PVSFarmtests.Setting = "NEUTRAL", "$farmsetting"
$arr = $PVSFarm -split ': '
$farmsettingvalue = $arr[1]
$PVSFarmtests.Value = "NEUTRAL", "$farmsettingvalue"
$PVSFarmResults.$farmnr = $PVSFarmtests
# ======= Write all results to an html file =================================================
Write-Host ("Saving results to html report: " + $resultsHTM)
writeHtmlHeader "PVS Farm Report $farmname_short" $resultsHTM
writeTableHeader $resultsHTM $TargetFirstheaderName $TargetheaderNames $TargetheaderWidths $TargetTablewidth
$allResults | sort-object -property collectionName | % { writeData $allResults $resultsHTM $TargetheaderNames}
writeTableFooter $resultsHTM
writeTableHeader $resultsHTM $vDiksFirstheaderName $vDiskheaderNames $vDiskheaderWidths $vDisktablewidth
$vdiskResults | sort-object -property ReplState | % { writeData $vdiskResults $resultsHTM $vDiskheaderNames }
writeTableFooter $resultsHTM
writeTableHeader $resultsHTM $PVSFirstheaderName $PVSheaderNames $PVSheaderWidths $PVStablewidth
$PVSResults | sort-object -property PVServerName_short | % { writeData $PVSResults $resultsHTM $PVSheaderNames}
writeTableFooter $resultsHTM
writeTableHeader $resultsHTM $PVSFirstFarmheaderName $PVSFarmHeaderNames $PVSFarmWidths $PVSFarmTablewidth
$PVSFarmResults | % { writeData $PVSFarmResults $resultsHTM $PVSFarmHeaderNames}
writeTableFooter $resultsHTM
writeHtmlFooter $resultsHTM
#send email
$emailSubject = ("$emailSubjectStart - $farmname_short - " + (Get-Date -format R))
$mailMessageParameters = @{
From = $emailFrom
To = $emailTo
Subject = $emailSubject
SmtpServer = $smtpServer
Body = (gc $resultsHTM) | Out-String
Attachment = $resultsHTM
# Send mail if you wish
Send-MailMessage @mailMessageParameters -BodyAsHtml -Priority $mailprio

or Download the file on GitHub: Citrix-PVS-Farm-Health-toHTML.ps1

Question or suggestion for improvement? Just contact me.

Update 12.11.14: Now in version 1.2 it’s possible to define the device collection which should be checked. An other additional feature is that the script shows the version of the vDisk.
Update 15.08.15: Now in version 1.4 it’s possible to have more than just one vDisk store.
Update 16.09.15: Now in version 1.5 the vDisk file name, it’s date and the count of the used disk will be showed.
Update 14.10.15: Version 1.61 New feature, PVS Cache (on disk or Ram) will be reported. / 20.10.2015 1.62: fixed some bugs (see comments)

Update 29.12.15: If you use PVS 7.7 or higher check out the new blog post with the 7.7 Script:

Because of the Release of PVS with the new PoSh API the further development of this Script is discontinued. Sorry.

Create published applications XenApp to each server

For testing purposes it can be helpful to have a published application without loadbalancing to every single server of your Citrix Farm. The creation of such application can be easily done with the following example script which create a CMD on every farm server:

Add-PSSnapin Citrix.XenApp.Commands -ErrorAction SilentlyContinue
$serverlist = Get-XAServer #Get all server from farm

# loop through all servers

 foreach($srv in $serverlist)

 echo $srv.ServerName

 New-XAApplication -BrowserName "cmd on $srv" -ApplicationType "ServerInstalled" -DisplayName "cmd on $srv" -FolderPath "Applications/Published Apps/z_admin" -ClientFolder "Administration\cmd" -Enabled $true -CommandLineExecutable "C:\Windows\system32\cmd.exe" -WorkingDirectory "C:\Windows\system32" -AnonymousConnectionsAllowed $false -AddToClientStartMenu $false -InstanceLimit "-1" #-WindowType "1024×768" -ColorDepth "Colors256"

 Add-XAApplicationAccount -BrowserName "cmd on $srv" -Accounts "domain\USER1"
 Add-XAApplicationAccount -BrowserName "cmd on $srv" -Accounts "domain\USER2"
 Add-XAApplicationServer -BrowserName "cmd on $srv" -ServerNames $srv.ServerName


By the way … if you made a mistake you can remove the created applications by the same foreach-loop with Remove-XAApplication:


$serverlist = Get-XAServer #Get all server from farm
# loop through all servers
foreach($srv in $serverlist)
 echo $srv.ServerName
 Remove-XAApplication -BrowserName "cmd on $srv"


All scripts are provided AS IS without warranty of any kind.


Check Citrix XenApp published application properties with a PowerShell script

To avoid troubles with session sharing, you have to keep in mind that you set the application properties which are relevant for the session and his virtual channels in the same way.
This is for example the properties like color depth or the audio setting.

To check this, I’ve written a small PS script which loops trough all application, reads the application properties and shout if something is not like expected.

Show all apps which are not published as 32bit Color:

# Created on: 5.11.2013 
# Created by: Sacha T. 
# Filename: apps_not32bit_Office.ps1
write-host "This Script show all Apps in Workergroup National which are not in 32 bit color depth published"
Add-PSSnapin Citrix.XenApp.Commands

 #$applications = Get-XAApplication #Gets the published applications
 $applications = Get-XAApplication -WorkerGroupName National

foreach($app in $applications){
 #Check to see if the application audio is disabled
 if($app.ColorDepth -ne "Colors32Bit"){
 $app_no32b+= "$app is not published in 32 bit.`n"
 echo "`nApps not 32bit: "
 echo $app_no32b
Write-Host "Press any key to continue …" 
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")


Change the green Workergroup to your workergroupname, it’s also possible to run this script without the parameter -WorkerGroup, but then all published content will be recognized as “non-32-bit”.

If you want see which application has audio disabled just change the attribut in the loop: 

---<snip>--- foreach($app in $applications)
{ #Check to see if the application audio is disabled if($app.AudioType -eq "none")
{ $app_noaudio+= "$app audio is currently disabled.`n" } } echo "`nApps Audio disabled: " echo $app_noaudio


This easy loop you can recycle to check every application attribute, all possible attributes of an app you can get with an

Get-XAApplication -BrowserName "thebrowsernameofourapp"

Have a look into the StoreFront 2.x Subscription Database

Last days I had to deal with Citrix StoreFront 2.0 and found out that a numerous issues still exists and that some things are still not implemented to configure in the StoreFront MMC SnapIn.

Session timeouts and settings like enable or disable of features like workspace control still must be handled over the config-files. But this is all well documented the official documentation of Citrix.

But seems there is no possibility to have a look into the subscription database from Citrix Storefront which is now with version 2.0 in a proprietary non-MSSQL format. For this reason I created this small script (execute it on the StoreFront server):

# Created on: 22.10.2013
# Created by: Sacha T.
# Filename: GetSubscriptedStoreFrontApps.ps1
#define some variables
#$domainname = "anotherDomain" # use that if your user is in another domain
$domainname = $env:userdomain
$storename = "StoreFront"      # Change this to your Store-Name
$subinfofile = "C:\temp\temp-subscriptions.csv"
Write-Host Query StoreFront apps in domain $domainname in Store $storename
$username = Read-Host "Please enter username to query"
#Add Module for Citrix StoreFront
Import-Module "C:\Program Files\Citrix\Receiver StoreFront\Scripts\ImportModules.ps1"
#Change username to domain SID
$objUser = New-Object System.Security.Principal.NTAccount($domainname, $username)
$strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier])
#delete old Subcriptionexportfile
Remove-Item $subinfofile -ErrorAction SilentlyContinue
#Create new Subcriptionexportfile
Export-DSStoreSubscriptions -StoreName $storename -FilePath $subinfofile
##Add a caption to the file, this is needed to process csv
$content = Get-Content $subinfofile
Set-Content $subinfofile
Add-Content $subinfofile $content
#Read all Lines in CSV from this User which are not unsubscribed
import-csv C:\Temp\temp-subscriptions.csv -delimiter "."| Where-Object {$_.sid -like "$strSID*" -and $ -notlike "*unsubscribed*"}| Format-Table -Property app
#wait to read the result - usefull if started from WindowsExplorer with "Run in PowerShell"
Write-Host "Press any key to continue …"
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")


When I’ve created this script I found out that deleted apps wont be removed from the subscription database and as well users who leave the company will still have records in the database after they are deleted in the AD. So within the years, the subscription database will have a lot of orphan data in the database. Seems that an automated clean-up does not exists.

There is a way to delete the records, have a look to forum post of Duncan Gabriel Thread:

Delete user subscriptions?