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

10 thoughts on “Clean Citrix UPM Profiles

  1. Hey Jeremy,

    thanks so much for your script! I’m having some trouble with it finding my profile stores though. Although it is able to find the path to the store (\\server\share\%username%\upm_profile) when I type in a valid username it comes back that it cannot find the store. Any idea’s what I’m missing? FYI i’m just using the default script as it is above.

    thanks!

    Like

    1. Hi Paul,
      The script just uses Test-Path to validate your input, maybe try in a separate PowerShell window using Test-Path and that share to confirm it’s actually got permissions/access to it etc.
      Cheers

      Like

      1. Sorry Paul I just had another look. It assumes you’re using #Samaccountname# in your PathToUserStore (not % username%), grabs the PathToUserStore setting from registry, and does a simple text replace on !samaccountname with your input.

        Im not sure that you can use %username% in your PathToUserStore

        Like

  2. Hey Jeremy, I changed it over to %username% and it appeared to work although I did get a lot of PS errors when the script ran (looks like it was related to permissions, as I am not using a DA account). Problem is, instead of cleaning the profile, it removed my profile 🙂 completely! Thankfully this was a test account so no harm/no foul but I’m wondering if that’s because of the variable change or something else?

    Like

  3. Hi Paul
    You’ll need to use the variable #SAMAccountName# in your Citrix Studio policy (not %username%)
    Also, make sure you’re running the script from a VDA, not from one of your controllers etc., as it looks in the registry for the settings deployed by your Citrix Studio policy – any of your controllers/management servers etc. won’t have these registry keys.

    If that still doesn’t work, try run the script one line at a time and let me know where it fails.

    Cheers

    Like

  4. Can Excluded files be added to the script as well as excluded folders. As I am using the Exclusion List File policy not the Exclusion list folder Policy ?

    AppData\Roaming\Microsoft\Windows\Recent\*.lnk
    AppData\Roaming\Microsoft\Windows\Recent\*.url
    AppData\Roaming\Microsoft\Office\Recent\*.lnk
    AppData\Roaming\Microsoft\Office\Recent\*.url

    Like

    1. Hi Mclovin

      The script should already handle Sync Inclusion List – Files and Sync Exclusion List – Files, however it looks like in both of them I made a typo and included an extra trailing backslash before the final asterisk
      foreach($exFile in (gci -Path “$($pathToUserStore)\$($parentDir)\*”

      I’ve updated both of these now to the below (and included -Force in case the files are hidden):

      foreach($exFile in (gci -Path “$($pathToUserStore)\$($parentDir)*” -Include “*$($fileExt)”

      I tested this briefly on a test profile and it looks like it cleaned up the extra files – can you test on your profiles and check?

      Thanks!

      Like

Leave a comment