App-V:  A Configuration Template for Deploying to Stateless RDS Clients on Citrix Published Desktops with Citrix UPM for Profile Management



UPDATED 11/17/2017

Updates:  Updated to reflect client versions supported.  Complete re-work of the script.  Added cleanup for LocalVFSSecuredUsers, Removed unused functions, Enhanced used functions.  Updated requirements to reflect compatibility with XenDesktop VDI.   Minor changes to format.

      

  • Requirements for using this configuration:
    • NTFSSecurity PowerShell module
    • App-V 5.0 SP2 (preferably Hotfix 4 or above), 5.1
    • Full App-V Infrastructure
    • RDS Client
    • SharedContentStore enabled (saves expensive storage space, less disk writes,  and overall performance has been proven to be better)
    • Preserve User Integrations On Login enabled
    • Citrix XenApp Published Desktops and XenDesktop VDI
    • Citrix UPM (or another profile solution)
    • No pre-caching of applications on core while in Read-Write mode
    • Startup Script (detailed later in this article) that will ensure that the provisioned disk, as well as the persistent disk are always "App-V healthy"
    • Preferably, your App-V packages, Virtual Disks, and High-Speed Storage all residing in the same Data Center
    • Applications are all User-Targeted in the scope of this scenario. 
    • If you have requirements for some Globally targeted applications, you will need to script the addition of those applications at server startup, but after the App-V cleanup scripts (mentioned below) are run.  This can be done via a Scheduled Task that is run after X number of minutes after server startup to ensure that the scripts are executed in the proper order (you can work out the timings and/or method you prefer).

We've been using this configuration for several years now in production with excellent performance.  Users report that many of the applications perform better than if they were installed locally.  The start menu integration (shortcuts) are available to users within a few seconds of getting fully logged in to their session. The users login to Citrix Published Desktops and their applications are delivered to them by the App-V full infrastructure.  The main benefit of this configuration is the ability to greatly reduce the amount of different stateless cores you have to support in your environment.  Using this methodology you should be able to reduce the amount of cores you support down to a handful in most cases depending on what you find performs better when installed locally (I've found that things like Oracle clients and Customer Service based apps like Screen Recording or Call Monitoring tend to perform better when installed locally).   The keys for this configuration to work is that the core image is always returned to a state where no App-V applications have ever been cached on it.  Leaving any of the following configuration points out leads to the integration points not integrating.  The reason for this is because the persistent drive that is attached to your core while in Read/Write mode will not be the same persistent drive that is attached to your cores that are Read-Only,



Key Points of Configuration:

  • I prefer to use the AppVClientUI as the test application in the script.  The script, after performing all of the cleanup operations, will attempt to add and publish this package to the machine globally, and then remove it.  If it is unable to do so, the client service is restarted and the test is performed again.  This feature was added because the AppV Client service will sometimes report to be in a running state but not actually functional after the cleanup process.  With this feature added, this symptom has been eliminated.
  • The script ensures that the following folders are deleted at system startup
    • C:\ProgramData\Microsoft\AppV
    • PackageInstallationRoot
  • The script ensures that the packages registry keys are removed at system startup
  • The script clears the LocalVFSSecuredUsers registry
  • Ensure that UPM (or whichever profile solution you are using) has the following registry exclusions
    • HKCU\SOFTWARE\Microsoft\AppV\Client\Integration
    • HKCU\SOFTWARE\Microsoft\AppV\Client\Publishing
  • Ensure that UPM (or whichever profile solution you are using) has the following file exclusions
    • AppData\Local\Microsoft\AppV
    • AppData\Roaming\Microsoft\AppV\Client\Catalog
  • Ensure that the following registry key is set to enable Preserving User Integrations On Login

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Client\Integration]
    "PreserveUserIntegrationsOnLogin"=dword:00000001







You will need to modify the BOLD text in the script for your environment (only 2 items to modify)

Pass the following parameters in the scheduled task when calling the script:   -LogEvent  -Verbose

Also, set a max run time for the scheduled task (5 minutes is way more than enough, but I set it to 15 just because).





-SCRIPT STARTS BELOW-

$VerbosePreference = 'Continue'

 $ErrorActionPreference = 'Continue'

 $Global:EventLogName = 'AppVMaint'

 $Global:EventLogSource = 'AppVClientStartup'


#region Module add information

 $ModulePath = '\\SharedPathForNTFSSecurityModule'

  $NTModule = 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\NTFSSecurity'

 #endregion


#region AppV Information

  $ServiceName = 'AppVClient'

  $AppVClientPath = [System.Environment]::ExpandEnvironmentVariables((Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\AppV\Client\Streaming -Name PackageInstallationRoot | Select-Object -ExpandProperty PackageInstallationRoot







If (Get-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\AppV\Client\Streaming -Name PackageInstallationRoot -ErrorAction SilentlyContinue) { $AppVClientPath = [System.Environment]::ExpandEnvironmentVariables((Get-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\AppV\Client\Streaming -Name PackageInstallationRoot | Select-Object -ExpandProperty PackageInstallationRoot))}

  $AppVProgramData = '{0}\Microsoft\appv' -f $env:ProgramData

  $DeletePaths = $AppVClientPath, $AppVProgramData, 'HKLM:\SOFTWARE\Microsoft\AppV\Client\Packages', 'HKLM:\SOFTWARE\Microsoft\AppV\Client\Streaming\Packages', 'HKLM:\SOFTWARE\Microsoft\AppV\MAV\Configuration\Packages', 'HKLM:\SOFTWARE\Microsoft\AppV\Client\Virtualization\LocalVFSSecuredUsers'

 #endregion

$Global:Logfile = '{0}\appv_Maint_PoSh.txt' -f $AppVClientPath.Split('\')[0]

#region Test App Information

  $TestPackageID = '237CB45B-20C7-4C91-985C-39A94507975A'

  $TestPackageVersion = 'f53d7a26-7fe3-40ab-990e-f244dc59ff43'

 $TestPackageFolder = '\\SharedPathForTestPackages'

  $TestApp = '{0}\AppVClientUI\AppVClientUI.appv' -f $TestPackageFolder

  $TestCleanPaths = "HKLM:\SOFTWARE\Microsoft\AppV\Client\Integration\Packages\$TestPackageID", "HKLM:\SOFTWARE\Microsoft\AppV\Client\Integration\Packages\{$TestPackageID}"

 #endregion

#region Custom functions

  If((Get-Alias -Name 'WLG' -ErrorAction SilentlyContinue) -ne $null) { Remove-Item Alias:WLG }

  Function Write-Log { <# Required: Write-FunctionLog #>

   Param ([Parameter(Mandatory = $true, Position = 1)][Alias('M')] $Message, $EntryType = 'Information', $EventID = 0, $EventLogName = $Global:EventLogName, $EventLogSource = "$(ForEach ($Scope in 0,1,2) { Try { Get-Variable MyInvocation -Scope $Scope | Out-Null } Catch { $Scope -= 1; Break }}; If ((Get-Variable MyInvocation -Scope $Scope).Value.MyCommand.Name.Length -eq 0) { $EventLogName } Else { (Get-Variable MyInvocation -Scope $Scope).Value.MyCommand.Name })", [Switch] [Alias('NE')] $NoEventLog, [Switch] [Alias('NF')] $NoFile, [Switch] [Alias('NV')] $NoVerbose)

   If (!$NoEventLog) {

    If (!([System.Diagnostics.EventLog]::LogNameFromSourceName("$EventLogSource",'.') -eq "$EventLogName")) {

     New-EventLog -LogName "$EventLogName" -Source "$EventLogSource"

     Limit-EventLog -LogName "$EventLogName" -MaximumSize 8388608 -RetentionDays 180

    }

    If ($EventID -eq 0 -and $EntryType -eq 'Warning') { $EventID = 1 }

    If ($EventID -eq 0 -and $EntryType -eq 'Error') { $EventID = 2 }

    Write-EventLog -LogName $EventLogName -EventId $EventId -Message $Message -Source $EventLogSource -Category 0 -Entrytype $EntryType

   }

   $Message = "$(Get-Date -Format G): $Message"

   If ($NoVerbose -and !$NoFile) {

    $Message | Out-File -FilePath $Global:LogFile -Append

   } ElseIf ($NoFile -and !$NoVerbose) {

    $Message | Write-Verbose -Verbose

   } ElseIf (!$NoVerbose -and !$NoFile) {

    $Message | Tee-Object -FilePath $Global:LogFile -Append | Write-Verbose -Verbose

   }

  }

  New-Alias -Name WLG -Value Write-Log -Scope Global


  Function Confirm-Service {

   Param ($ServiceName, $Status) #We should limit status to the possible options at some point in the future

   If ((Get-Service -Name $ServiceName).Status -ne $Status) {

    WLG "Failed to adjust service $ServiceName to $Status"

   } Else {

    WLG "Successfully adjusted service $ServiceName to $Status"

   }

  }

 Function Confirm-Removal {

   Param ($Path)

   If (Test-Path -Path $Path) {

    WLG "Failed to remove: $Path" -EntryType 'Warning'

   } Else {

    WLG "Successfully removed: $Path"

   }

  }

 Function Clear-Paths {

   Param ($DeletePaths, $ServiceName, [Switch] $TestCleanUp)

   If (!$TestCleanUp) {

    If (((Get-Service -Name $ServiceName).Status -eq 'Running').Count -gt 0) {

     WLG "Attempting to stop service: $ServiceName"

     Stop-Service -Name $ServiceName -Verbose -ErrorVariable err *>> $Logfile

     $Count = 1

     While ((Get-Service -Name $ServiceName).Status -eq 'Running' -and $Count -lt 90) {

      Start-Sleep -Seconds 2

      $Count++

     }

     Confirm-Service -ServiceName $ServiceName -Status 'Stopped'

    }

   }

   ForEach ($DPath In $DeletePaths) {

    If (Test-Path -Path $DPath) {

     If ((Get-Item -Path $DPath).PSProvider.Name -eq 'Registry') {

      WLG "Attempting to remove: $DPath"

      Remove-item $DPath -recurse -force *>> $Logfile

     } Else {

      If (!$TestCleanUp) {

       # Set Owner of App-V Client files to 'NT AUTHORITY\SYSTEM' and add ACE for account to have FullControl.

       WLG "Taking ownership of Appv Cache: $DPath"

       # Use NTFSSecurity CmdLet Get-ChildItem2 which uses the AlphaFS module.

       Get-ChildItem2 -Path $DPath -Recurse | Set-Owner -Account 'NT AUTHORITY\SYSTEM' -Verbose -ErrorVariable err *>> $Logfile

       WLG "Completed attempt to take ownership of AppV Cache: $DPath"

       WLG "Attempting to adjust permissions: 'NT AUTHORITY\SYSTEM' FullControl ACL configured on child items of $DPath. Permissions inherited by files and folders (CI;OI)."

      # Use NTFSSecurity CmdLet Add-Access

       Get-ChildItem2 -Path $DPath -recurse | Add-Access -AccessRights FullControl -Account 'NT AUTHORITY\SYSTEM' -AppliesTo ThisFolderSubfoldersAndFiles -Verbose -ErrorVariable err *>> $Logfile

       WLG "Completed Attempt:  Adjusting permissions on $DPath"

      }

      WLG "Attempting to remove: $DPath"

      Get-ChildItem2 -Path $DPath -Recurse | Sort -Property @{Expression={$_.FullName.Length}} -Descending | %{ Remove-Item2 $_.FullName -Force -Recurse -ErrorVariable err *>> $Logfile }

      Remove-Item $DPath -Force -Recurse -ErrorVariable err *>> $Logfile

     }

     Confirm-Removal -Path $DPath

    } Else {

     WLG "Did not find path: $DPath"

    }

   }

   If (!$TestCleanUp) {

    WLG "Attempting to start service: $ServiceName"

    Start-Service -Name $ServiceName -Verbose -ErrorVariable err *>> $Logfile

    $Count = 1

    While ((Get-Service -Name $ServiceName).Status -ne 'Running' -and $Count -lt 15) {

     Start-Sleep -Seconds 2

     $Count++

    }

    Confirm-Service -ServiceName $ServiceName -Status 'Running'

   }

  }

 #endregion

#region Main script

  If (Test-Path $Logfile) { Remove-Item $Logfile -force }

  Set-Location $env:SystemDrive

  WLG "Starting AppV client Maintenance: $($MyInvocation.MyCommand.Name) from $PSScriptRoot."

  WLG 'Checking NTFSSecurity module dependency.'

  If (!(Get-Module -Name NTFSSecurity)) {

         If ((Test-Path -Path $NTModule) -and (!(Get-Item $NTModule).PSIsContainer)) { Remove-Item $NTModule -Force }

         If (!(Test-Path -Path $NTModule)) { If (Test-Path "$ModulePath\NTFSSecurity") { Copy-Item "$ModulePath\NTFSSecurity" -Destination "$NTModule\" -Recurse -Force }}

   Import-Module -Name NTFSSecurity -Verbose -ErrorVariable err

  }

  If (!(Get-Module -Name NTFSSecurity)) { WLG 'Unable to import NTFSSecurity module' -EntryType 'Error' -EventID 404 }

 WLG 'Removing AppVClientPackages.'

   Get-AppvClientConnectionGroup -All | Remove-AppvClientConnectionGroup -Verbose -ErrorVariable err *>> $Logfile

   Get-AppvClientPackage -All | Remove-AppVClientPackage -Verbose -ErrorVariable err *>> $Logfile

  WLG 'Completed attempt of removal of AppVClientPackages.'

 WLG "Attempting to stop service: $ServiceName"

  Stop-Service -Name $ServiceName -Verbose -ErrorVariable err *>> $Logfile

  $Count = 1

  While ((Get-Service -Name $ServiceName).Status -eq 'Running' -and $Count -lt 90) {

   Start-Sleep -Seconds 2

   $Count++

  }

  Confirm-Service -ServiceName $ServiceName -Status 'Stopped'


  Clear-Paths -DeletePaths $DeletePaths -ServiceName $ServiceName

 If (Test-Path $TestApp) {

   While (!(Add-AppVClientPackage -Path $TestApp -ErrorVariable err | Tee-Object -FilePath $Logfile -Append)) {

    WLG "Testing App-v failed, attempting to remediate - $err" -EntryType 'Warning' -EventID 90

    WLG "Attempting to restart service: $ServiceName"

    Restart-Service -Name $ServiceName -Force -Verbose -ErrorVariable err *>> $Logfile

    Start-Sleep -Seconds 6

    Confirm-Service -ServiceName $ServiceName -Status 'Running'

   }

   WLG 'Testing-App-V successful'

   Publish-AppvClientPackage -Global -PackageId $TestPackageID -VersionId $TestPackageVersion

  } Else {

   WLG 'Unable to find test application' -EntryType 'Warning' -EventID 91

  }

  Get-AppvClientPackage -All | Remove-AppVClientPackage -Verbose -ErrorVariable err *>> $Logfile

  Clear-Paths -DeletePaths $TestCleanPaths -TestCleanUp

 #endregion