Disconnect-RDUserProfileDisk.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
<#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