How to retrieve lingering FSLogix profiles on Windows Virtual Desktop, mounted from an Azure File share

In the last couple of months, we’ve seen the following strange behavior coming from an FSLogix profile, mounted on a Windows Virtual Desktop host with an Azure File share as an underlying storage provider.

The issue

In some very particular cases it happens that when a user logs off its session from a WVD (Windows Virtual Desktop) host, the corresponding FSLogix profile is not dismounted from the host.

When the user tries to login again to the environment, this results in the following error.

Status : 0x0000000B : Cannot open virtual disk
Reason : 0x00000000 : The container is attached
Error code : 0x00000020 : The process cannot access the file because it is being used by another process

Normal behavior

During normal behavior of the login and log off process to Windows Virtual Desktop in combination with an FSLogix profile, the profile is mounted from the underlying storage provider and correctly dismounted upon successful log off of the Windows Virtual Desktop host.

Root cause

The root cause of why the profile container is not dismounted from the host is hard to find, in most cases, an update of the FSLogix components is required, please make sure to read through the latest FSLogix release notes.

Looking for the lingering VHD(X) container

During the days that we had our profile shares/data hosted on a traditional IaaS fileserver, we would just open up an MMC console and look for any open files or sessions.

Since our profiles are now being hosted on an Azure File share, this process is slightly different. I’ve written a small PowerShell script for you to use and/or alter to your needs.

What it does or can do

The input variables are pretty straightforward :

  • Mode: You can alert or react to a possible lingered FSLogix profile (under construction)
  • ProfileStorageAccount: You need to provide the storage account name where you store your FSLogix containers
  • ProfileShare: Following your storage account, we also need the specific file share
  • StorageAccountResourceGroupName: Our resource group name where our storage account is located is required

Note: The script is currently “designed” to query only one storage account/file share, and only one host pool per run. You could of course alter this to check all host pools and related storage accounts.

The script loops through your active Windows Virtual Desktop sessions and active storage handles.

It then checks each storage handle, whether or not it has a corresponding active WVD session. If not you are presented with the virtual machine name where the FSLogix container is mounted.

Powershell Script

Save this PowerShell script as “Clean-LingeringFSLogixProfiles.ps1” Read through the blog post to retrieve the InVM script. The scripts can be download from my GitRepo as well.

<#
.SYNOPSIS
    Dismount lingering FSLogix VHD(X) profiles.

.DESCRIPTION
    Dismount lingering FSLogix VHD(X) profiles.

.PARAMETER Mode
    Provide the execution mode of the script.
    Alerting : Generates an alert whenever a lingering FSLogix VHDX profile is found
    React : Tries to dismount the lingering FSLogix Profile on the host where it is attached

.PARAMETER ProfileStorageAccount
    Provide the storage account where the FSLogix profiles are located

.PARAMETER ProfileStorageAccount
    Provide the fileshare where the FSLogix profiles are located

.PARAMETER StorageAccountResourceGroupName
    Provide the resource group name of your storage account

.PARAMETER OverrideErrorActionPreference
    Provide the ErrorActionPreference setting, as descibed in about_preference_variables.
    (https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7#erroractionpreference).
    When running locally we should use "Break" mode, which breaks in to the debugger when an error is thrown.

.EXAMPLE
    PS C:\> .\Clean-LingeringFSLogixProfiles.ps1 -Mode "Alerting" -ProfileStorageAccount "storageaccountname" -ProfileShare "profileshare" -StorageAccountResourceGroupName "resourcegroupname"

#>
[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)]
    [ValidateSet('alerting', 'react')]
    [string]
    $Mode,
    [Parameter(Mandatory = $true)]
    [string]
    $ProfileStorageAccount,
    [Parameter(Mandatory = $true)]
    [string]
    $ProfileShare,
    [Parameter(Mandatory = $true)]
    [string]
    $StorageAccountResourceGroupName,
    [Parameter(Mandatory = $false)]
    [string]
    $OverrideErrorActionPreference = "Break"
)

$ErrorActionPreference = $OverrideErrorActionPreference

# The following cmd retrieves your storage account details and puts it in a context variable
$context = Get-AzStorageAccount -ResourceGroupName $StorageAccountResourceGroupName -Name $ProfileStorageAccount

#region retrieve details per hostpool
# Retrieves the hostpools => Alter the script here to check for additional host pools
$hostpools = get-azwvdhostpool
foreach ($hostpool in $hostpools) {
    $wvdrg = Get-AzResource -ResourceId $hostpools.Id
    # This is tricky, so if you only need 1 host pool remove the foreach loop completely and comment the line below
    $hostpools = $hostpool


    #region gather all open files & sessions
    $OpenFiles = Get-AzStorageFileHandle -Context $Context.Context -ShareName $ProfileShare -Recursive
    $UserSessions = Get-AzWvdUserSession -HostPoolName $hostpools.Name -ResourceGroupName $wvdrg.ResourceGroupName | Select-Object ActiveDirectoryUserName, ApplicationType, SessionState, UserPrincipalName, name
    #endregion

    #region fill Open Files array
    $pathusers = @()
    foreach ($openfile in $OpenFiles) {

        If ($openfile.path) {
            #Write-host $openfile.Path
            $FilePath = $openfile.Path.Split("/")[0]
            $pathusers += $FilePath
        }
    }
    $pathusers = $pathusers | Select-Object -Unique
    #endregion

    #region fill Open Sessions array
    $sessionusers = @()
    foreach ($usersession in $UserSessions) {

        If ($usersession) {
            #Write-host $usersession
            $Username = $UserSession.ActiveDirectoryUserName.Split("\")[1]

            $sessionusers += $Username
        }
    }
    $sessionusers = $sessionusers | Select-Object -Unique
    #endregion

    #region loop through every open file and find a corresponding user session
    foreach ($pathuser in $pathusers) {
        If ($sessionusers -contains $pathuser) {
            Write-host -ForegroundColor green "Active session user: " $pathuser
        } else {
            If ($mode -eq "alerting") {
                $OpenFilesDetails = Get-AzStorageFileHandle -Context $Context.Context -ShareName $ProfileShare -Recursive | Where-Object { $_.Path -like "*$($pathuser)*" }
                # the following retrieves the virtual machine name of the lingering VHDX file
                $IPNic = ((Get-AzNetworkInterface | Where-Object { $_.IpConfigurations.PrivateIpAddress -eq $($OpenFilesDetails.ClientIp.IPAddressToString[0]) }).virtualmachine).Id
                $vmname = ($IPNic -split '/') | Select-Object -Last 1
                $VM = Get-AzVm -Name $vmname
                Write-host -ForegroundColor red "Inactive session user: $pathuser has a FSLogix mounted on the following virtual machine $vmname"
            } Else {
                $OpenFilesDetails = Get-AzStorageFileHandle -Context $Context.Context -ShareName $ProfileShare -Recursive | Where-Object { $_.Path -like "*$($pathuser)*" }
                # the following retrieves the virtual machine name of the lingering VHDX file
                $IPNic = ((Get-AzNetworkInterface | Where-Object { $_.IpConfigurations.PrivateIpAddress -eq $($OpenFilesDetails.ClientIp.IPAddressToString[0]) }).virtualmachine).Id
                $vmname = ($IPNic -split '/') | Select-Object -Last 1
                $VM = Get-AzVm -Name $vmname
                Write-host -ForegroundColor red "Inactive session user: $pathuser has a FSLogix mounted on the following virtual machine $vmname"
                # double check whether or not you want to dismount the profile
                $YesNo = Read-Host "Are you sure you want to dismount the user profile off $pathuser on the following server $vmname: Yes/No"
                If ($YesNo -eq "Yes")
                {
                    $domainupn = Read-Host "Please enter your domain admin username:"
                    $domainpwd = Read-Host "Please enter your domain admin password:"
                    $runDismount = Invoke-AzVMRunCommand -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -CommandId 'RunPowerShellScript' -ScriptPath "scripts\AzVMRunCommands\Clean-InVMLingeringFSLogixProfiles.ps1"  -Parameter @{"Upn" = "$domainupn"; "Pass" = "$domainpwd";"pathuser" = $pathuser }
                    If ($runDismount.Status -Ne "Succeeded") {
                        Write-Error "Run failed"
                    }
                    else {
                        Write-Host "FSLogix profile has been dismounted for $($pathuser) on $($vmname)"
                    }
                }
            else {
                # Exit script
                Write-Host "We are now exiting the script, you've entered the wrong option: Yes/No is required"
                Exit
            }
            }
        }
    }
    #endregion
}
#endregion

InVM Powershell Script

Before launching the script above, make sure to save the script that needs to be run within the virtual machine.

Save the PowerShell script below as “InVMLingeringFSLogixProfiles.ps1” and alter the script path in the script above. The scripts can be download from my GitRepo as well.

param (
    [Parameter(Mandatory = $true)]
    [string]
    $pathuser,
    [Parameter(Mandatory = $true)]
    [string]
    $upn,
    [Parameter(Mandatory = $true)]
    [string]
    $pass,
    [Parameter(Mandatory = $false)]
    [string]
    $OverrideErrorActionPreference = "Break"
)

#This script is run within the virtual machine

$ziptargetfolder = "c:\troubleshooting\"
$innerscriptlocation = $ziptargetfolder + "Dismount-VHD.ps1"

If (!(Test-Path $ziptargetfolder)) {
    mkdir $ziptargetfolder
}

@"
`$ProfileNamingConvention = "Profile-" + "$pathuser"
`$Volume = Get-Volume | Where-Object { `$_.filesystemlabel -eq `$ProfileNamingConvention } | % { Get-DiskImage -DevicePath `$(`$_.Path -replace "\\`$") }
Dismount-DiskImage -ImagePath `$Volume.ImagePath
"@ | Out-File -FilePath $innerscriptlocation

$taskName = "Dismount-FSLogixProfile"
$Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -File $innerscriptlocation" -WorkingDirectory $ziptargetfolder
$Settings = New-ScheduledTaskSettingsSet -Compatibility Win8
$TaskPath = "\CustomTasks"
Register-ScheduledTask -TaskName $taskName -User $upn -Password $pass  -RunLevel Highest -Action $Action -Settings $Settings


Start-ScheduledTask -TaskName $taskName -TaskPath $TaskPath
while ((Get-ScheduledTask -TaskName $taskName).State -ne 'Ready') {
    Start-Sleep -Seconds 2
}

Unregister-ScheduledTask -TaskName $taskName -Confirm:$False
Remove-Item -Path $innerscriptlocation -Recurse -Force


Warning!

The scripts are provided as-is, please be very careful and test run the scripts on a “test” environment or an environment that allows you to perform some quick checks and tests. Dismounting VHD(X) files can cause unwanted effects when performed against an Active user.

Thank you!

Thank you for reading through this blog post, I hope I have been able to assist in troubleshooting FSLogix profile mounting issues.

If you encounter any new insights, feel free to drop me a comment or contact me via mail or other social media channels

2 Comments on “How to retrieve lingering FSLogix profiles on Windows Virtual Desktop, mounted from an Azure File share

Leave a Reply

Your email address will not be published. Required fields are marked *

Please reload

Please Wait