Disconnect-RDUserProfileDisk.ps1

<#PSScriptInfo
.VERSION 0.99
 
.GUID 38eeb337-90da-4789-ae1f-ffc54c8baf4a
 
.AUTHOR Evgenij Smirnov
 
.COMPANYNAME it-pro-berlin.de
 
.COPYRIGHT
 
.TAGS rds upd
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
#>


<#
.SYNOPSIS
Finds a (not necessarily stuck) UPD and disconnects it from the Host.
 
.DESCRIPTION
Sometimes a user profile disk needs to be force-disconnected. This script automates the proces to a degree.
 
- either (preferably) SessionCollection for which the UPD is stuck
- or (less preferably) the file server where the UPD is located
 
and will try to identify the RD Session Host that's got it locked.
 
The File server will be detected automatically from the session collection. It needs to be specified by name there and be a member of the domain. If the UPDs are stored on a DFS share or on a non-windows server, we can't do very much.
 
Written by Evgenij Smirnov (es@it-pro-berlin.de) in October 2016.
 
Contains work of Jeffrey Patton (@jeffpatton) from way back (open files detection by querying LANMANSERVER).
 
This is Version 0.99 of 04.10.2016.
 
.PARAMETER User
Specifies the user name whose UPD has to be unlocked.
 
.PARAMETER SessionCollection
Specifies the session collection. If this parameter is not specified, UPDFileServer needs to be specified. If there is a registered user session, the corresponding SessionCollection will be determined at runtime.
 
.PARAMETER ConnectionBroker
Specifies the RD connection broker to connect to. If this parameter is not specified, the script will assume it is running on the connection broker.
 
.PARAMETER UPDFileServer
If session collection is not specified, the script will need to know which file server to connect to. It will try to detect the correct Session Collection later. If SessionCollection is specified, this parameter is ignored and overwritten by the file server specified in the session collection.
 
.PARAMETER Force
Forces disconnecting the disk even if there is a session for this user active on the host. The script will try to force-log off the user.
 
.EXAMPLE
Disconnect-RDUserProfileDisk.ps1 -User kenmyer -SessionCollection SC01
#>


[CmdletBinding()]
Param(
    [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$false)][string]$User,
    [Parameter(Mandatory=$false, Position=2, ValueFromPipeline=$false)][string]$SessionCollection,
    [Parameter(Mandatory=$false, Position=3, ValueFromPipeline=$false)][string]$ConnectionBroker,
    [Parameter(Mandatory=$false, Position=4, ValueFromPipeline=$false)][string]$UPDFileServer,
    [Parameter(Mandatory=$false)][switch]$Force
)
#region init
# check for AD module and RD Module
$config_ok = $true
$config_problems = @()
if (!(Get-Command -Module ActiveDirectory)) {
    $config_problems += "AD module not present"
    $config_ok = $false
} else {
    try {
        $adu = Get-ADUser $User
        $san = $adu.SAMAccountName
        $sid = $adu.SID.Value
    } catch {
        $config_problems += "The specified user $User could not be found in AD"
        $config_ok = $false
    }
}
if ($SessionCollection) {
    if (!(Get-Command -Module RemoteDesktop)) {
        $config_problems += "RemoteDesktop module not present but a session collection has been specified"
        $config_ok = $false
    } else {
        if (!$ConnectionBroker) { $ConnectionBroker = "$((Get-WmiObject win32_computersystem).DNSHostName).$((Get-WmiObject win32_computersystem).Domain)" }
        if ((Get-Service RDMS -ComputerName $ConnectionBroker -EA SilentlyContinue).Status -like "Running") {
            $sc = Get-RDSessionCollection -CollectionName $SessionCollection -ConnectionBroker $ConnectionBroker -EA SilentlyContinue
            if ($sc.Count -gt 0) {
                $upd = Get-RDSessionCollectionConfiguration -CollectionName $SessionCollection -ConnectionBroker $ConnectionBroker -UserProfileDisk
                if ($upd.EnableUserProfileDisk) {
                    $UPDFileServer = ($upd.DiskPath.Substring(2) -split "\\")[0]
                } else {
                    $config_problems += "UPD is disabled on Session Collection $SessionCollection"
                    $config_ok = $false
                }
            } else {
                $config_problems += "Session Collection not found on broker $ConnectionBroker"
                $config_ok = $false
            }
        } else {
            $config_problems += "ConnectionBroker $ConnectionBroker is not running"
            $config_ok = $false
        }
    }
} else {
    if (!($UPDFileServer)) {
        $config_problems += "Neither SessionCollection nor UPDFileServer have been specified"
        $config_ok = $false
    }
}
# check file server
if ($config_ok) {
    try {
        $updfs = ($UPDFileServer -split "\.")[0]
        $fsca = Get-ADComputer $updfs
    } catch {
        $config_problems += "UPDFileServer $updfs is not an AD member"
        $config_ok = $false
    }
}


if (!$config_ok) {
    Write-Error "There are $($config_problems.Count) problems with the arguments: `n- $($config_problems -join "`n- ")" -Category InvalidArgument 
    exit
}
#endregion
#region detect open file
Write-Host "Configuration OK, detecting open files on UPD files server $updfs for SID $sid..."
$rdsh_ok = $false
$fs = [adsi]"WinNT://$updfs/LanmanServer"
$fsres = $fs.PSBase.Invoke("Resources")
foreach ($file in $fsres) {
    try {
        $path = $file.GetType().InvokeMember("Path","GetProperty",$null,$file,$null)
        if ($path -like "*$($sid).VHDX") {
            try {
                $rdsh = $file.GetType().InvokeMember("User","GetProperty",$null,$file,$null)
                break
            } catch {}
        }
    } catch {}
}

if ($rdsh) {
    Write-Host "An open file $path was reported by the server, locked by $rdsh"
    if ($rdsh -like "*$") {
        $rdsh = $rdsh.Substring(0, $rdsh.Length - 1)
        $rdsh_dns = (Get-ADComputer $rdsh).DNSHostName
        if ((Get-RDServer -Role RDS-RD-SERVER -ConnectionBroker $ConnectionBroker).Server -contains $rdsh_dns) {
            $rdsh_ok = $true
            if ($SessionCollection) {
                if ((Get-RDSessionCollectionConfiguration -CollectionName $SessionCollection -ConnectionBroker $ConnectionBroker -LoadBalancing).SessionHost -notcontains $rdsh_dns) {
                    Write-Host "$rdsh_dns is not part of the session collection $SessionCollection"
                    $rdsh_ok = $false
                }
            } else {
                $SessionCollection = (Get-RDUserSession -ConnectionBroker $ConnectionBroker | where {($_.HostServer -like "$rdsh_dns") -and ($_.UserName -like $User)}).CollectionName
                if ($SessionCollection) {
                    Write-Host "Found session for user $User in session collection $SessionCollection"
                } else {
                    Write-Warning "Unable to determine session collection"
                }
            }
        } else {
            Write-Host "$rdsh_dns is not a RD Session Host registered on $ConnectionBroker"
        }
    } else {
        Write-Host "Since $rdsh does not end in $, it is not a computer account..."
    }
} else {
    Write-Host "No open files for this SID were reported by the server."
}
if ($rdsh_ok) {
    Write-Host "$rdsh is qualified for dismounting UPD!"
} else {
    break
}
#endregion
#region check for session and dismount VHD
$do_dismount = $true
if (!($Force.IsPresent)) { $Force = $false }
if (!$Force) {
    $sessions = Get-WmiObject Win32_LoggedOnUser -ComputerName $rdsh | where {$_.Antecedent -like "*Name=`"$san`"*"}
    if ($sessions.Count -gt 0) {
        Write-Warning "User $san is currently logged into server $rdsh!"
        $do_dismount = ((Read-Host "Dismount VHD anyway? (Y/N)") -like "Y")
    }
}
if ($do_dismount) {
    $vol = Get-WMIObject -ComputerName $rdsh -Class Win32_Volume | where { ($_.Label –like "User Disk") –and ($_.Name –like "C:\Users\$san\") }
    $id = $vol.DeviceID
    if ($id) {
        $id = $id.Substring(0,$id.Length -1)
        Write-Host "VHD found at ID $id"
        $sess_id = (Get-RDUserSession -ConnectionBroker $ConnectionBroker -CollectionName $SessionCollection | where {($_.HostServer -like "$rdsh_dns") -and ($_.UserName -like $User)}).UnifiedSessionId
        if ($sess_id) {
        Write-Host "Logging off session $sess_id..."
            Invoke-RDUserLogoff -UnifiedSessionID $sess_id -HostServer $rdsh_dns -Force
        }
        Start-Sleep -Seconds 2
        $vol = Get-WMIObject -ComputerName $rdsh -Class Win32_Volume | where { ($_.Label –like "User Disk") –and ($_.Name –like "C:\Users\$san\") }
        if ($vol) {
            Write-Host "User $User has been force-logged off the session host $rdsh. However, the VHD is still mounted.`nTo dismount the VHD:`n1. log on to the server $rdsh,`n2. Start PowerShell elevated,`n3. Get-DiskImage –DevicePath $id | Dismount-DiskImage" -ForegroundColor Cyan
        } else {
            Write-Host "User $User has been force-logged off the session host $rdsh. VHD has been dismounted in the process." -ForegroundColor Green
        }
    }
}
#endregion