Clean Citrix UPM Profiles

UPDATE: This is now built into Profile Manager, and I’d recommend you use this instead of the below script.
https://docs.citrix.com/en-us/profile-management/current-release/configure/include-and-exclude-items/enable-logon-exclusion-check.html
Thanks Citrix!

I have a hate/love relationship with Citrix Profile Manager.

On the other hand, I HATE how much time I seem to spend on it, tweaking my UPM policies to troubleshoot slow logons, trying to figure out which parts of the Google Chrome User Data folder I need to make it work properly, trying to figure out what some obscure folder in AppData is for and what will potentially break if I exclude it.

On the one hand, I love the fancy features like Profile Streaming, Active Writeback, and mirroring of credentials (and browser sessions!) across different servers between logon/logoff. I also appreciate some handy tools that Citrix/the Citrix Community have produced for UPM, like the UPM Log Parser and UPMConfigCheck.

 

Now, I’m happy to be able to add to this with a useful script of my own!

One thing I’ve noticed with Profile Manager is that the profile store doesn’t get ‘cleaned’ when you change your Citrix Policies.

So, let’s say you’ve been including all of AppData\Local\Google Chrome in your “Folders to synchronize” list (or you selected “Migration of existing profiles: Local and Roaming”) , and start getting complaints of slow logins because the AppData\Local\Google Chrome\User Data\Default\Cache folder is growing large.

After some brief Googling and a few prayers, you add AppData\Local\Google Chrome\User Data\Default\Cache to your list of excluded folders.

But lo, and behold! Your logins are still slow? The AppData\Local\Google Chrome\User Data\Default\Cache folder still exists in your users’ profile store? Whaaa…?

Unfortunately, possibly for clever reasons, Profile Manager won’t retroactively clean up your profile store, nor do the exclusion lists etc. apply on logon (i.e., UPM just copies everything in your profile store down to the server).

This presents you with the difficult choice of either a) PowerShell’ing your way through each user profile and deleting the folders you no longer want, or b) resetting the profile.

 

Muralidhar Maram from Citrix wrote a handy little CLI tool that will do this for you, but with one caveat… it only works if you’re using Citrix GPOs to deploy your UPM settings – not Citrix Studio Policies. Muralidhar probably has a lot to do, so I’ve written what is hopefully a useful script to clean up a UPM profile.

Caveats:

  • It only works (has been tested) if you’re using Citrix Studio policies 😛
  • It will ignore the AppData\Roaming folder (i.e., just copy it in its entirety). If you don’t want it to do this, comment out line 100 (and test, because I haven’t)
    $savedFoldersList += “$($pathToUserStore)\AppData\Roaming”
  • It doesn’t delete the old UPM folder, but you could easily modify the script to.
  • It assumes your PathToUserStore setting is based on the #SAMAccountName# variable. If you’re using something different (e.g. #profilePath#), you’ll need to modify the top do-until loop.

How it works:

It looks in the registry for your Profile Manager policies (so you have to run this on a VDA), and copies any files/folders in your “Directories to synchronize”, “Folders to Mirror”, “Files to synchronize” list from your current UPM folder to a new UPM folder.

It then goes through your “Exclusion list – directories” and “Exclusion list – files” policies and deletes any files/folders in your new UPM folder that match these.

Then, it resets the permissions on the folder, and renames your old UPM folder to UPM_PROFILE_backup…. and renames your new UPM folder to UPM_PROFILE (so it’ll get used at next logon/logoff).

The script:

Ta da.

 


#########################################################################################
# Start up
# Run from a VDA
#########################################################################################

do{
	$user = Read-Host "Enter the SAMAccount name of the user you wish to clean up"
	#PathToUserStore
	$pathToUserStore = ((Get-ItemProperty "HKLM:\Software\Policies\Citrix\UserProfileManagerHDX").PathToUserStore -replace "#SAMAccountName#",$user) + "\UPM_PROFILE"
	if(!(Test-Path $pathToUserStore)){
		Write-Host "Can't find $($pathToUserStore), re-enter your username." -fore Red
	}
}until(Test-Path $pathToUserStore)

Write-Host "Scanning UPM folder..." -fore yellow
$savedFoldersList = @()
$savedFilesList = @()
$deleteFoldersList = @()
$excFoldersList = @()
$excFilesList = @()
#SyncFileList
foreach($file in (Get-ItemProperty "HKLM:\Software\Policies\Citrix\UserProfileManagerHDX\SyncFileList").SyncFileList){
    $file = $file `
        -replace "!ctx_localappdata!","AppData\Local" `
        -replace "!ctx_internetcache!","AppData\Local\Microsoft\Windows\Temporary Internet Files" `
        -replace "!ctx_localsettings!","AppData\Local" `
        -replace "!ctx_roamingappdata!","AppData\Roaming" `
        -replace "!ctx_startmenu!","AppData\Roaming\Microsoft\Windows\Start Menu"
        # Also need to check if the file has a wildcard in it
        if($file -match "\*" -and $file -match "\."){
                # Get the parent directory of the file
                $periodIndex = $file.LastIndexOf(".")
                $parentDir = $file.Substring( 0,$periodIndex ) -replace "\*"
                $fileExt  = $file.Substring( $periodIndex, ($file.Length - $periodIndex) )
                foreach($wildcardFile in (gci -Path "$($pathToUserStore)\$($parentDir)*" -Include "*$($fileExt)" -Force)){
                        $savedFilesList += $wildcardFile.FullName
                }
        }else{
            $savedFilesList += "$($pathToUserStore)\$($file)"
        }

}
#SyncDirList
foreach($folder in (Get-ItemProperty "HKLM:\Software\Policies\Citrix\UserProfileManagerHDX\SyncDirList").SyncDirList){
    $folder = $folder `
        -replace "!ctx_localappdata!","AppData\Local" `
        -replace "!ctx_internetcache!","AppData\Local\Microsoft\Windows\Temporary Internet Files" `
        -replace "!ctx_localsettings!","AppData\Local" `
        -replace "!ctx_roamingappdata!","AppData\Roaming" `
        -replace "!ctx_startmenu!","AppData\Roaming\Microsoft\Windows\Start Menu"
    $savedFoldersList += "$($pathToUserStore)\$($folder)"
}
#MirrorFoldersList
foreach($folder in (Get-ItemProperty "HKLM:\Software\Policies\Citrix\UserProfileManagerHDX\MirrorFoldersList").MirrorFoldersList){
    $folder = $folder `
        -replace "!ctx_localappdata!","AppData\Local" `
        -replace "!ctx_internetcache!","AppData\Local\Microsoft\Windows\Temporary Internet Files" `
        -replace "!ctx_localsettings!","AppData\Local" `
        -replace "!ctx_roamingappdata!","AppData\Roaming" `
        -replace "!ctx_startmenu!","AppData\Roaming\Microsoft\Windows\Start Menu"
    $savedFoldersList += "$($pathToUserStore)\$($folder)"
}
#SyncExclusionListDir
foreach($folder in (Get-ItemProperty "HKLM:\Software\Policies\Citrix\UserProfileManagerHDX\SyncExclusionListDir").SyncExclusionListDir){
    $folder = $folder `
        -replace "!ctx_localappdata!","AppData\Local" `
        -replace "!ctx_internetcache!","AppData\Local\Microsoft\Windows\Temporary Internet Files" `
        -replace "!ctx_localsettings!","AppData\Local" `
        -replace "!ctx_roamingappdata!","AppData\Roaming" `
        -replace "!ctx_startmenu!","AppData\Roaming\Microsoft\Windows\Start Menu"
    $excFoldersList += "$($pathToUserStore)\$($folder)"
}
#SyncExclusionListFiles
foreach($file in (Get-ItemProperty "HKLM:\Software\Policies\Citrix\UserProfileManagerHDX\SyncExclusionListFiles").SyncExclusionListFiles){
    $file = $file `
        -replace "!ctx_localappdata!","AppData\Local" `
        -replace "!ctx_internetcache!","AppData\Local\Microsoft\Windows\Temporary Internet Files" `
        -replace "!ctx_localsettings!","AppData\Local" `
        -replace "!ctx_roamingappdata!","AppData\Roaming" `
        -replace "!ctx_startmenu!","AppData\Roaming\Microsoft\Windows\Start Menu"
    # Also need to check if the file has a wildcard in it
    if($file -match "\*" -and $file -match "\."){
        # Get the parent directory of the file
        $periodIndex = $file.LastIndexOf(".")
        $parentDir = $file.Substring( 0,$periodIndex ) -replace "\*"
        $fileExt  = $file.Substring( $periodIndex, ($file.Length - $periodIndex) )
        $exFile = $null
        foreach($exFile in (gci -Path "$($pathToUserStore)\$($parentDir)*" -Include "*$($fileExt)" -Force).FullName){
            if($exFile){
                $excFilesList += $exFile
            }
        }
    }else{
        $excFilesList += "$($pathToUserStore)\$($file)"
    }
}

# Add in system folders/folders
$savedFoldersList += "$($pathToUserStore)\Citrix"
$savedFoldersList += "$($pathToUserStore)\WINDOWS"
$savedFoldersList += "$($pathToUserStore)\AppData\Roaming"
foreach($file in (gci "$($pathToUserStore)\*" -Include *.dat,*.log*,*.blf,*.ini,*.pol,*.bin -File -Force)){
    $savedFilesList += $file.FullName
}

#########################################################################################
# How many files...?
#########################################################################################

$preFileCount = (Get-Item $pathToUserStore).GetFiles("*",[System.IO.SearchOption]::AllDirectories).Count

#########################################################################################
# Copy only the saved FOLDERS to a new location
#########################################################################################

Write-Host "Copying saved folders to new location"

$folder = $null
$pathToNewUserStore = $pathToUserStore -replace "UPM_PROFILE","UPM_PROFILE_NEW"
if(!(Test-Path $pathToNewUserStore)){
    mkdir $pathToNewUserStore | Out-Null
}
foreach($folder in $savedFoldersList){
    $destDir = ( $folder -replace [regex]::Escape($pathToUserStore),$pathToNewUserStore )
    robocopy $folder $destDir /e /r:0 /w:0 /mt:64 /dcopy:t /copyall /log:robocopy.log | Out-Null
}

#########################################################################################
# Copy only the saved FILES to a new location
#########################################################################################

Write-Host "Copying saved files to new location"

$file = $null
foreach($file in $savedFilesList){
    $destFile = ( $file -replace [regex]::Escape($pathToUserStore),$pathToNewUserStore )
    if(Test-Path $file){
        New-Item -ItemType File -Path $destFile -Force | Out-Null
        Copy-Item -Path $file -Destination $destFile -Force | Out-Null
    }
}

#########################################################################################
# Now delete the $excFoldersList from our copied profile
#########################################################################################

Write-Host "Deleting excluded folders from new profile"

$folder = $null
foreach($folder in $excFoldersList){
    $folder = ($folder -replace [regex]::Escape($pathToUserStore),$pathToNewUserStore)
    if(Test-Path $folder){
        Remove-Item $folder -Recurse
    }
}

#########################################################################################
# Now delete the $excFilesList from our copied profile
#########################################################################################

Write-Host "Deleting excluded files from new profile"

$file = $null
foreach($file in $excFilesList){
    $destFile = ( $file -replace [regex]::Escape($pathToUserStore),$pathToNewUserStore )
    Remove-Item $destFile -Force
}

#########################################################################################
# Remove/rename the old profile folder
#########################################################################################

Write-Host "Renaming profile folders"

Rename-Item $pathToUserStore "$($pathToUserStore).upm_backup_$(Get-Date -Format dd_MM_yy)"
sleep 1
Rename-Item $pathToNewUserStore $pathToUserStore

#########################################################################################
# Reset permissions
#########################################################################################

Write-Host "Resetting permissions"

icacls.exe $pathToUserStore /setowner $user /T /C /Q | Out-Null
icacls.exe $pathToUserStore /reset /T /C /Q | Out-Null

$postFileCount = (Get-Item $pathToUserStore).GetFiles("*",[System.IO.SearchOption]::AllDirectories).Count

Write-Host "Removed a total of $($preFileCount - $postFileCount) files." -fore yellow