Even if you configure your User Profile Manager in Citrix correctly, keep it up date and so on, you might end up with the same problem we had; every now and then a profile contains a file that is locked so the profile doesn’t get cleaned up after user log off.
When that user logs on again they might end up with a “username.domain001”-profile, then 002 and so on. And the user profile settings soon become pretty messy.
One way of making this problem less of a pain, is to have a script clean up the folders (and of course the registry values) to make sure the users can get their profiles loaded correctly.
As always, use any script you find on the internet with caution, and test them fully before deploying anything in your production environment.
A script walkthrough follows:
First of all, I usually set the variables that might differ from different environments and/or domains etc.
Most of these are pretty obvious in this script, but one that might require some explanation is this one:
$thishost=hostname
$thishostADSI = [ADSI]"WinNT://$thishost,computer"
$LocalAccountsOnThisHost=$thishostADSI.psbase.Children | Where-Object { $_.psbase.schemaclassname -eq 'user' } | % { $_.Name }
This just collects all the local users on a server/computer. We didn’t want these profiles to get cleaned up since they don’t have any central profiles.
The next ‘weird’ one:
# Tune performance with $SpeedBrake. Lower is quicker, but uses more CPU.
$SpeedBrake=1
This variable is used in a “Start-Sleep”-cmdlet later, this is to make sure the script doesn’t steal a lot of CPU-resources.
The next one is farily obvious, but I’ll explain it anyway:
# Add a regex that works for your usernaming standard
$UserNameRegEx="\w"
This should be changed to match whatever you have as a standard for your SamAccountNames. the “\w” will more or less match anything, so this should be changed to something that matches your environment. This is just used for extra safety, you might not want to end up deleting a service account profile for example.
Speaking of service accounts, if you have a naming standard for those, add it here:
# Add a list of other accounts you want to exclude
$ExcludedUsers="Public","ctx","svc"
If the account name contains for example “svc”, it will be ignored by the script.
But what about logged on users? We don’t want to remove their profiles, do we?
The hard thing about this, is that there was no easy way (like a cmdlet) to list logged on users. (at least not when I wrote this script a while back), so I went with the solution of listing all current processes and their owners, and select those that are unique. That looks like this:
# This get's all the currently logged on users
$LoggedOnUsers=Get-WmiObject win32_process|select name,@{n="owner";e={$_.getowner().user}} | Select-Object Owner -Unique | % { $_.owner }
And we finish by adding all of these lists togheter:
# Add them all togheter
$UsersToIgnore+=$LoggedOnUsers
$UsersToIgnore+=$ExcludedUsers
$UsersToIgnore+=$LocalAccountsOnThisHost
$UsersToIgnore=$UsersToIgnore | select -Unique
Now we should have all the information we need to safely go through all the profiles and remove those that we don’t want anymore.
I wont go through this part “line by line”, but I think you will get the point when reading the code comments.
The first part of the code looks like this:
# Start to loop through all the profile folders
foreach ($LocalProfileFolder in $LocalProfileFolders)
{
# Sleep to prevent CPU load
sleep -m $SpeedBrake
# Set this variable to True, it will be changed if it should stay later.
$ThisProfileShouldBeDeleted=$True
# This this folder match the username regex?
if ($LocalProfileFolder.Name -match $UserNameRegEx)
{
# Make sure it doesn't match any of the "ignored" users, (logged on ones etc...)
foreach ($UserToIgnore in $UsersToIgnore)
{
# Again, sleep to prevent CPU load.
sleep -m $SpeedBrake
# Check if it matches a "ignored" user
if ($LocalProfileFolder.Name -like "*$UserToIgnore*")
{
# If it did, it should not be deleted.
$ThisProfileShouldBeDeleted=$False
}
}
So it starts by setting $ThisProfileShouldBeDeleted to $True. It then does a couple of checks (regex matching etc) to make sure that the users profile is OK to delete. (If not, it sets $ThisProfileShouldBeDeleted to false.)
When this is done, it continues with the actual delete-process:
# Get the full path
$CurrentProfileName=$LocalProfileFolder.FullName
# Check if it should be deleted
if ($ThisProfileShouldBeDeleted -eq $True)
{
$ProfileToLookFor=$BaseProfilePath + $LocalProfileFolder.Name
$ProfileToClean=$ProfileToLookFor -replace "\\","\\"
$WMIQuery = $null
$WMIQuery=Get-WmiObject Win32_UserProfile -computer '.' -filter "localpath='$ProfileToClean'"
if ($WMIQuery -ne $null) {
$TheProfileToDrop=$WMIQuery | Select-Object LocalPath
$WMIProfileToDrop=$TheProfileToDrop.LocalPath
Write-Output "Deleting $WMIProfileToDrop through WMI."
$WMIQuery.Delete()
}
else {
cmd /c rd /s /q "$CurrentProfileName"
Write-Output "No WMI-profile found, $CurrentProfileName folder was deleted though."
}
Write-Output "$CurrentProfileName has been deleted."
}
else {
Write-Output "$CurrentProfileName was skipped."
}
}
}
A few things to point out, the script always tries to delete the profile through WMI since this is the “cleanest” way of doing this (removes the registry values together with the folder). This is the same thing as going through “My Computer” to remove the profile.
If there was no “WMI-profile”, it just deletes the folder. I know it looks terrible when using “cmd /c rd /s /q” instead of Remove-Item to do this, but Remove-Item has a bug that makes it a bad idea to use it for this sort of thing. (It has to do with how it handles symbol links).
So I went with the quick and dirty way of solving that… sorry… 🙂
This code has been running in production for a few months now 🙂
Hope someone find parts of this code, or maybe even all of it useful,
The complete script can be downloaded from here.