Azure Virtual Desktop user profile cleanup (FSlogix cleanup)
This is script what cleans up folders in C:\Users\ folder if the user is no longer on the server, as well as session registry keys.
This prevents users to have issue where they cannot sign in to RDP because their previous session still exists on some server.
The script serves only as Cleanup. It also requires to set time outs via GPO after user inactivity the session will be signed out after X hours.
Also it helps with situation when the user profile folder stays on server, and it does not allow the FSlogix to create temporary user profile folder where are all user files in sync.
The script saves what was removed in CSV file, what you can store on server. I recommend on some hidden share with limited priviledges.
I have deployed this script to be run after server starts every hour, using GPO to deploy Scheduled task on Remote Desktop servers.
It requires Admin priviledges. So the script must be protected from someone changing it in some malicious way.
# Location where CSV log of the cleanup will be saved: $log_Path_csv = "\\server\ScriptLogs$\UserProfileCleanup.csv" # Test variable, if there was any cleanup to record who was active at that time $cleanup_check = 0 # Define the list of usernames to exclude from cleanup $excludedUsernames = @("admin", "localadmin", "superman") # Function to check if user is member of AD group using 'net user /domain' command function IsUserMemberOfADGroup($username, $groupName) { try { $userGroups = net user $username /domain 2>&1 if ($userGroups -like "*The user name could not be found*") { return $true } $groupMemberships = $userGroups | Select-String -Pattern "Group\s+Memberships" -Context 0,10 | ForEach-Object { $_.Line + " " + ($_.Context.PostContext -join " ") } if ($groupMemberships -like "*$groupName*") { return $true } else { return $false } } catch { return $true } } # Log to CSV file on file share function LogToCSV ($task, $msg, $LogPath=$log_Path_csv) { $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" if ($msg -is [System.Collections.IEnumerable] -and -not ($msg -is [string])) { $msg = $msg -join ', ' } $logEntry = [PSCustomObject]@{ Timestamp = $Timestamp Server = $(hostname) Task = $task Message = $msg } if (-Not (Test-Path -Path $LogPath)) { $logEntry | Export-Csv -Path $LogPath -NoTypeInformation } else { $logEntry | Export-Csv -Path $LogPath -NoTypeInformation -Append } } # Check if folder exists and remove it. function UserFolderCleanupSess($username) { try { $base_path = "C:\Users\$username" $local_path = "C:\Users\local_$username" if (Test-Path -Path $base_path) { LogToCSV "Removed folder" $base_path $cleanup_check = 1 # Removes user folder if not in active users and it exists: cmd.exe /c "rmdir /s /q $base_path" } if (Test-Path -Path $local_path) { LogToCSV "Removed folder" $local_path $cleanup_check = 1 # Removes user folder if not in active users and it exists: cmd.exe /c "rmdir /s /q $local_path" } } catch { return $false } } # Get the list of active users at the run of this script, removing "USERNAME" and any leading ">" # this includes any account who is just disconnected, but has session opened $activeUsers = (query user | Select-String -Pattern '^\s*>{0,1}(\S+)\s+' | ForEach-Object { $_.Matches[0].Groups[1].Value }).Where({ $_ -ne "USERNAME" }) # Define the list of system accounts to exclude from cleaning up $systemAccounts = @("S-1-5-18", "S-1-5-19", "S-1-5-20") # Check each profile key and remove if user is not member of AAD Administrators group and not in list of excluded users list $profileKeys = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" | Where-Object { $_.PSChildName -match "^S-1-5-21-" } foreach ($key in $profileKeys) { $profilePath = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$($key.PSChildName)").ProfileImagePath $userName = Split-Path $profilePath -Leaf if ($activeUsers -notcontains $userName -and $systemAccounts -notcontains $key.PSChildName -and $excludedUsernames -notcontains $userName) { if (IsUserMemberOfADGroup $userName "AAD DC Administrators") { Write-Host "$userName - Local acc or Administrator Skipping" } else { UserFolderCleanupSess $userName LogToCSV "Removed Sess. Profile Reg" "$userName - HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$($key.PSChildName)" $cleanup_check = 1 Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$($key.PSChildName)" -Recurse } } } # Remove session data for a user not active users and if user is not on excluded users list $sessionDataPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData" $sessions = Get-ChildItem -Path $sessionDataPath foreach ($session in $sessions) { $loggedOnUser = (Get-ItemProperty -Path "$sessionDataPath\$($session.PSChildName)").LoggedOnSAMUser -replace "^(AVD|$($hostname))\\", '' if ($activeUsers -notcontains $loggedOnUser -and $excludedUsernames -notcontains $loggedOnUser) { LogToCSV "Removed Session" "$loggedOnUser - $sessionDataPath\$($session.PSChildName)" $cleanup_check = 1 Remove-Item -Path "$sessionDataPath\$($session.PSChildName)" -Recurse } } # Saves to CSV who was active at time of cleanup, if there was anything to clean. if ($cleanup_check -eq 1) { LogToCSV "Active Users" $activeUsers }