Назначение
Скрипт PowerShell предназначен для диагностики и автоматического исправления ошибок файловой системы на VHDX‑дисках FSLogix. Он выявляет пользователей, у которых активна RDS‑сессия, но профиль не смонтирован (том с меткой Profile-ИмяПользователя отсутствует), затем находит соответствующий VHDX‑файл в заданных сетевых хранилищах, монтирует его, проверяет с помощью chkdsk /f и при необходимости восстанавливает целостность NTFS.
Основные возможности
-
🔎 Автоматический поиск проблемных сессий через RD Connection Broker.
-
📁 Поддержка нескольких хранилищ VHDX (UNC‑пути).
-
🧪 Безопасное монтирование образов без присвоения буквы (буква назначается временно только для проверки).
-
🛠 Исправление ошибок NTFS с помощью
chkdsk /f(режим можно отключить). -
📄 Подробное логирование и CSV‑отчёт о состоянии каждого проверенного VHDX.
-
⚡ Ограничение на количество проверяемых дисков – защита от перегрузки при большом числе проблем.
Как это работает
-
Скрипт получает список активных сессий из указанной коллекции RDS.
-
Для каждого RDS‑хоста проверяет, какие пользователи имеют смонтированный том FSLogix.
-
Формирует список «проблемных» пользователей (сессия есть – тома нет).
-
Для каждого такого пользователя перебирает заданные UNC‑пути в поисках его VHDX‑файла.
-
Монтирует VHDX (без автоматической буквы), определяет том, временно назначает свободную букву.
-
Запускает
chkdsk /fдля исправления ошибок файловой системы. -
Отмонтирует диск, удаляет временную букву, сохраняет результат.
-
Формирует итоговый лог и CSV‑отчёт.
Результаты работы
-
Лог‑файл:
%TEMP%\FSLogixVHDXCheck\FSLogix_VHDX_Check_<дата_время>.log
Содержит все действия скрипта, ошибки и результатыchkdsk. -
CSV‑отчёт:
%TEMP%\FSLogixVHDXCheck\VHDX_Report_<дата_время>.csv
Включает: пользователя, путь к VHDX, статус (OK/REPAIRED/CORRUPT/NTFS_ERRORS), результат восстановления, детали ошибки.
Важные замечания
-
Скрипт не изменяет и не удаляет профили пользователей – только файловая система VHDX.
-
Работает только с NTFS‑томами. Если том определён как
RAW– восстановление невозможно (требуется ручное вмешательство). -
Для корректной работы необходимы права администратора на RDS‑хостах и доступ к брокеру соединений.
-
Режим восстановления (
$EnableRepair) можно отключить – тогда скрипт будет только диагностировать проблемы.
Пример использования
-
Скачайте скрипт, при необходимости отредактируйте параметры в начале файла:
-
$collectionName– имя коллекции RDS. -
$connectionBroker– FQDN брокера. -
$fslogixRootUncList– массив путей к хранилищам VHDX. -
$EnableRepair– установите$trueдля автоматического исправления.
-
-
Запустите от имени администратора:
.\FSLogix_VHDX_Check.ps1
-
После завершения проверьте лог и CSV‑отчёт в папке
%TEMP%\FSLogixVHDXCheck.
Требования к окружению
-
Windows Server 2016 / 2019 / 2022 (RDS‑роль).
-
Модуль
Hyper-V(дляMount-DiskImage/Dismount-DiskImage). -
Командлеты
Get-RDUserSession(доступны на сервере с ролью RDS‑брокера). -
Сетевой доступ к UNC‑хранилищам VHDX.
# === НАСТРОЙКИ ===
$collectionName = "Farm "
$connectionBroker = "RDCB01.adminbd.ru"
$fslogixRootUncList = @(
"\\UPD02\DisksfsFSLogix$"
"\\UPD03\DisksfsFSLogix$"
)
$MaxDisksToCheck = 1500
$EnableRepair = $true
# === ЛОГИРОВАНИЕ ===
$LogDir = "$env:TEMP\FSLogixVHDXCheck"
if (-not (Test-Path $LogDir)) { New-Item -Path $LogDir -ItemType Directory -Force | Out-Null }
$LogPath = "$LogDir\FSLogix_VHDX_Check_$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
Write-Host $logEntry
Add-Content -Path $LogPath -Value $logEntry
}
Write-Log "=== НАЧАЛО ПРОВЕРКИ И ВОССТАНОВЛЕНИЯ VHDX (NTFS) ===" "INFO"
Write-Log "Коллекция: $collectionName | Брокер: $connectionBroker" "INFO"
Write-Log "Пути к дискам: $($fslogixRootUncList -join ', ')" "INFO"
Write-Log "Лимит проверки: первые $MaxDisksToCheck дисков" "INFO"
Write-Log "Режим восстановления: $(if ($EnableRepair) { 'ВКЛЮЧЕН (chkdsk /f)' } else { 'ВЫКЛЮЧЕН (только диагностика)' })" "INFO"
# === 1. ПОЛУЧАЕМ ВСЕ СЕССИИ ===
Write-Log "Получение сессий из коллекции '$collectionName' через брокер '$connectionBroker'..." "INFO"
try {
$sessions = Get-RDUserSession -CollectionName $collectionName -ConnectionBroker $connectionBroker -ErrorAction Stop
} catch {
Write-Log "❌ Ошибка при получении сессий: $($_.Exception.Message)" "ERROR"
exit 1
}
if (-not $sessions) {
Write-Log "ℹ️ Нет сессий в коллекции." "INFO"
exit 0
}
$validSessions = $sessions | Where-Object { $_.HostServer -and $_.HostServer.Trim() -ne '' }
Write-Log "Всего сессий: $($sessions.Count). Валидных (с HostServer): $($validSessions.Count)" "INFO"
if (-not $validSessions) {
Write-Log "❌ Нет валидных сессий для проверки." "WARN"
exit 0
}
# === 2. НАХОДИМ ПОЛЬЗОВАТЕЛЕЙ БЕЗ СМОНТИРОВАННОГО ПРОФИЛЯ ===
$sessionsByHost = $validSessions | Group-Object HostServer
$problemUsers = @()
Write-Log "Поиск пользователей с активной сессией, но без смонтированного профиля..." "INFO"
foreach ($group in $sessionsByHost) {
$hostName = $group.Name.Trim()
$hostSessions = $group.Group
try {
$userNamesToCheck = $hostSessions.UserName | ForEach-Object { ($_ -split '\\')[-1] }
$mountedUsers = Invoke-Command -ComputerName $hostName -ScriptBlock {
param($expectedNames)
$mounted = @()
$volumes = Get-Volume -ErrorAction SilentlyContinue |
Where-Object { $_.FileSystemLabel -and $_.FileSystemLabel.StartsWith("Profile-") }
foreach ($vol in $volumes) {
$userNameFromLabel = $vol.FileSystemLabel.Substring("Profile-".Length)
$userNameClean = ($userNameFromLabel -split '\\')[-1].Trim()
foreach ($exp in $expectedNames) {
if ($userNameClean.ToLower() -eq $exp.ToLower()) {
$mounted += $exp
break
}
}
}
return $mounted
} -ArgumentList (,$userNamesToCheck) -ErrorAction Stop
foreach ($session in $hostSessions) {
$userName = ($session.UserName -split '\\')[-1]
if ($userName -notin $mountedUsers) {
$problemUsers += [PSCustomObject]@{
FullUserName = $session.UserName
UserName = $userName
SessionHost = $session.HostServer
SessionId = $session.UnifiedSessionId
SessionState = $session.SessionState
}
Write-Log "⚠️ Обнаружена проблема: пользователь '$userName' на хосте '$hostName' — профиль не смонтирован." "WARN"
}
}
} catch {
$errMsg = "Не удалось проверить хост '$hostName': $($_.Exception.Message)"
Write-Log $errMsg "ERROR"
foreach ($session in $hostSessions) {
$userName = ($session.UserName -split '\\')[-1]
$problemUsers += [PSCustomObject]@{
FullUserName = $session.UserName
UserName = $userName
SessionHost = $session.HostServer
SessionId = $session.UnifiedSessionId
SessionState = $session.SessionState
}
Write-Log "⚠️ Пользователь '$userName' помечен как проблемный из-за ошибки проверки хоста." "WARN"
}
}
}
if (-not $problemUsers) {
Write-Log "✅ Все пользователи с сессией имеют смонтированный профиль. Проверка не требуется." "INFO"
exit 0
}
# ОГРАНИЧИВАЕМ ДО ПЕРВЫХ N
if ($problemUsers.Count -gt $MaxDisksToCheck) {
Write-Log "Найдено $($problemUsers.Count) проблемных пользователей. Проверим только первые $MaxDisksToCheck." "INFO"
$problemUsers = $problemUsers | Select-Object -First $MaxDisksToCheck
} else {
Write-Log "Будет проверено $($problemUsers.Count) VHDX-файлов." "INFO"
}
# === ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ СВОБОДНОЙ БУКВЫ ===
function Get-FreeDriveLetter {
$used = Get-Volume | Where-Object { $_.DriveLetter } | ForEach-Object { $_.DriveLetter + ":" }
for ($i = 68; $i -le 90; $i++) {
$letter = [char]$i + ":"
if ($used -notcontains $letter) {
return $letter
}
}
throw "Нет свободных букв диска (D-Z)"
}
# === 3. ПРОВЕРКА И ВОССТАНОВЛЕНИЕ VHDX ===
$vhdxCheckResults = @()
foreach ($user in $problemUsers) {
Write-Log "`n=== Обработка пользователя: $($user.UserName) ===" "INFO"
foreach ($rootUnc in $fslogixRootUncList) {
$foundVhdx = $false
$vhdxPath = ""
Write-Log "Поиск VHDX для '$($user.UserName)' в хранилище: $rootUnc" "INFO"
try {
$userFolders = Get-ChildItem -Path $rootUnc -Directory -ErrorAction Stop |
Where-Object { $_.Name -like "$($user.UserName)*" }
foreach ($folder in $userFolders) {
$candidate1 = Join-Path $folder.FullName "Profile_$($user.UserName).VHDX"
$candidate2 = Join-Path $folder.FullName "Profile_$($user.UserName).vhdx"
if (Test-Path $candidate1) { $vhdxPath = $candidate1; $foundVhdx = $true; break }
if (Test-Path $candidate2) { $vhdxPath = $candidate2; $foundVhdx = $true; break }
}
} catch {
Write-Log "Ошибка при поиске в $rootUnc : $($_.Exception.Message)" "WARN"
continue
}
if (-not $foundVhdx) {
Write-Log "VHDX для пользователя не найден в $rootUnc" "DEBUG"
continue
}
# === РАБОТА С VHDX ===
$status = "OK"
$fsType = "N/A"
$errorDetail = ""
$repairAttempted = $false
$repairSuccess = $null
$driveLetter = $null
$letterChar = $null
$diskNumber = $null
try {
Write-Log "Монтирование VHDX: $vhdxPath" "INFO"
$mountedImage = Mount-DiskImage -ImagePath $vhdxPath -NoDriveLetter -PassThru -ErrorAction Stop
Start-Sleep -Seconds 5
# === НАДЁЖНОЕ ОПРЕДЕЛЕНИЕ НОМЕРА ДИСКА ===
$disk = $null
# Способ 1: Через Get-DiskImage
try {
$diskImage = Get-DiskImage -ImagePath $vhdxPath -ErrorAction Stop
$disk = $diskImage | Get-Disk -ErrorAction SilentlyContinue
} catch { }
# Способ 2: Поиск по всем дискам (fallback)
if (-not $disk -or -not $disk.Number) {
$disk = Get-Disk | Where-Object {
$_.Location -like "*$([System.IO.Path]::GetFileName($vhdxPath))*" -or
$_.FriendlyName -like "*$($user.UserName)*"
} | Select-Object -First 1
}
if (-not $disk -or -not $disk.Number) {
throw "Не удалось определить номер диска для VHDX. Возможно, диск не инициализировался."
}
$diskNumber = $disk.Number
Write-Log "Диск определён как Disk $diskNumber" "INFO"
$expectedLabel = "Profile-$($user.UserName)"
$volume = Get-Volume -ErrorAction SilentlyContinue |
Where-Object { $_.FileSystemLabel -and $_.FileSystemLabel.Trim() -eq $expectedLabel }
if (-not $volume) {
throw "Том с меткой '$expectedLabel' не найден"
}
$fsType = $volume.FileSystem
Write-Log "Том найден: метка='$expectedLabel', FS=$fsType, Health=$($volume.HealthStatus)" "INFO"
if ($fsType -eq "RAW") {
$status = "CORRUPT"
$errorDetail = "Том в состоянии RAW — восстановление невозможно."
Write-Log $errorDetail "ERROR"
continue
}
# === ПРИСВАИВАЕМ БУКВУ ===
$driveLetter = Get-FreeDriveLetter
$letterChar = $driveLetter.TrimEnd(':')
Write-Log "Назначение временной буквы: $driveLetter" "INFO"
Start-Sleep -Seconds 3
$partition = Get-Partition -DiskNumber $diskNumber |
Where-Object { -not $_.DriveLetter } |
Select-Object -First 1
if (-not $partition) {
$partition = Get-Partition -DiskNumber $diskNumber | Select-Object -First 1
}
if ($partition) {
$partition | Set-Partition -NewDriveLetter $letterChar -ErrorAction Stop
Write-Log "Буква $driveLetter успешно назначена" "INFO"
} else {
throw "Не удалось найти раздел на диске $diskNumber"
}
Start-Sleep -Seconds 3
# === ЗАПУСК CHKDSK ===
if ($EnableRepair) {
Write-Log "Запуск chkdsk $driveLetter /f ..." "INFO"
$repairAttempted = $true
$chkdskOutput = & chkdsk "$driveLetter" /f 2>&1 | Out-String
Write-Log "chkdsk завершён. Вывод (первые 700 символов):`n$($chkdskOutput.Substring(0, [Math]::Min(700, $chkdskOutput.Length)))" "DEBUG"
if ($chkdskOutput -match "(Windows has made corrections|исправлений в файловой системе|corrected)") {
$repairSuccess = $true
$status = "REPAIRED"
$errorDetail = "Ошибки NTFS успешно исправлены."
Write-Log "✅ chkdsk исправил ошибки." "INFO"
}
elseif ($chkdskOutput -match "(did not find any problems|не обнаружил проблем|No further action is required)") {
$repairSuccess = $true
$errorDetail = "Ошибок NTFS не обнаружено."
Write-Log "✅ chkdsk: ошибок не найдено." "INFO"
}
else {
$repairSuccess = $false
$status = "NTFS_ERRORS"
$errorDetail = "chkdsk обнаружил ошибки, но не смог их полностью исправить."
Write-Log "❌ chkdsk не исправил все ошибки." "ERROR"
}
}
else {
$errorDetail = "Восстановление отключено."
Write-Log $errorDetail "WARN"
}
}
catch {
$status = "CORRUPT"
$errorDetail = "Ошибка обработки VHDX: $($_.Exception.Message)"
Write-Log "❌ $errorDetail" "ERROR"
}
finally {
# === ОЧИСТКА ===
if ($driveLetter) {
try {
Write-Log "Удаление буквы $driveLetter ..." "DEBUG"
Get-Volume -DriveLetter $letterChar -ErrorAction SilentlyContinue |
Set-Volume -DriveLetter $null -ErrorAction SilentlyContinue
Get-Partition -DriveLetter $letterChar -ErrorAction SilentlyContinue |
Set-Partition -NewDriveLetter $null -ErrorAction SilentlyContinue
}
catch { Write-Log "Ошибка удаления буквы: $($_.Exception.Message)" "WARN" }
}
try {
Write-Log "Отмонтирование VHDX ..." "DEBUG"
Dismount-DiskImage -ImagePath $vhdxPath -ErrorAction SilentlyContinue
Write-Log "VHDX успешно отмонтирован" "DEBUG"
}
catch { Write-Log "Ошибка отмонтирования: $($_.Exception.Message)" "WARN" }
}
$vhdxCheckResults += [PSCustomObject]@{
UserName = $user.UserName
StoragePath = $rootUnc
VhdxPath = $vhdxPath
Status = $status
FileSystem = $fsType
RepairAttempted = $repairAttempted
RepairSuccess = $repairSuccess
Error = $errorDetail
DiskNumber = $diskNumber
}
}
}
# === 4. ИТОГОВЫЙ ОТЧЁТ ===
Write-Log "`n=== ИТОГОВЫЙ ОТЧЁТ ===" "INFO"
$repaired = $vhdxCheckResults | Where-Object { $_.Status -eq "REPAIRED" }
$failed = $vhdxCheckResults | Where-Object { $_.Status -notin @("OK", "REPAIRED") }
if ($repaired) {
Write-Log "✅ Исправлено: $($repaired.Count)" "INFO"
$repaired | ForEach-Object { Write-Log " - $($_.UserName) ($($_.StoragePath)): исправлены ошибки NTFS" "INFO" }
}
if ($failed) {
Write-Log "❌ Проблемные: $($failed.Count)" "ERROR"
$failed | ForEach-Object { Write-Log " - $($_.UserName) ($($_.StoragePath)): $($_.Error)" "ERROR" }
}
if (($repaired.Count + $failed.Count) -eq 0) {
Write-Log "✅ Все в порядке." "INFO"
}
$csvPath = "$LogDir\VHDX_Report_$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').csv"
$vhdxCheckResults | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Log "📄 Отчёт: $csvPath" "INFO"
Write-Host "`n✅ Готово. Лог: $LogPath" -ForegroundColor Cyan
Similar Posts:
- Как найти битые профили fslogix и найти профили которые не монтировались на сервер
- Как вывести список пользователей у которых не при монтировался диск fslogix и переименовать FriendlyName
- Как отключить (отмонтировать) диск User Profile Disks в RDS Windows Server
- Как посмотреть кто залогинен на серверах в сети powershell
- Как найти на серверах rds rdp ферме профили которые в статусе fslogix WaitingForWriteQueueFlush
