diff --git a/Resize-OSPartition/Resize-OSPartition.ps1 b/Resize-OSPartition/Resize-OSPartition.ps1 new file mode 100644 index 0000000..1960e46 --- /dev/null +++ b/Resize-OSPartition/Resize-OSPartition.ps1 @@ -0,0 +1,1701 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Safely (re)creates the Recovery Partition (WinRE) at the end of the + system disk and, on request, expands C: into the remaining space. + +.DESCRIPTION + Multi-phase, highly defensive script: + PHASE 1 - DISCOVERY (read-only) + PHASE 2 - VALIDATION (read-only) + PHASE 3 - USER CONFIRMATION + PHASE 4 - DESTRUCTIVE OPERATION (remove old Recovery, if present) + PHASE 5 - REBUILD (create new Recovery FIRST at the TRUE end + of the disk using an explicit -Offset, + then optionally expand C: into whatever + remains, THEN enable WinRE) + PHASE 6 - VALIDATION (final audit) + PHASE 7 - FINAL REPORT + + v2.1.0 CHANGES (bugfix release): + - New-RecoveryPartition now computes an EXPLICIT end-of-disk -Offset + (disk.Size - buffer - RecoverySizeMB) instead of letting + New-Partition auto-place it. Auto-placement (no -Offset) puts a + new partition at the START of the first available free extent - + NOT the end. Since the free extent runs from the end of C: to the + end of the disk, this glued Recovery directly onto C: and pushed + ALL remaining free space past Recovery, to the true end of the + disk - the opposite of the intended design, and it also meant the + gap-to-expand-C-into was always computed as ~0 MB, so the script + never asked to expand C:. + - The interactive "expand C: into remaining space?" question is now + asked BEFORE reagentc /enable, not after. Recovery is created at + the true end of the disk (so there can be a gap between C: and + Recovery at that point); if adjacency between C: and Recovery + really is required for reagentc to accept the partition (per this + script's own prior testing notes), closing that gap has to happen + before WinRE is enabled, not after. + - The old "blocking partition directly after C:" hard-abort guard + (which assumed Recovery must always sit immediately after C:) was + relaxed to an informational check, since Recovery is no longer + required to be placed immediately after C: at creation time. + + An existing Recovery Partition is OPTIONAL on input. If none is found, + the script treats this as a fresh-build scenario (e.g. after a manual + diskpart shrink) instead of failing. + + The script NEVER assumes Disk 0, Partition 4, or any fixed size. Every + disk/partition reference is re-detected dynamically and re-validated + immediately before any destructive action. + +.PARAMETER AuditOnly + Performs full discovery + validation, prints the plan, but makes NO + changes to the system. + +.PARAMETER RecoverySizeMB + Target size (MB) of the new Recovery Partition. Default 2000. NOTE: + 1024 MB is NOT reliably enough on modern Windows 11 - reagentc /enable + silently falls back to storing WinRE inside C: itself (instead of using + the Recovery partition) if the partition is too small for the current + Winre.wim plus its own required working-space buffer, even though the + partition otherwise looks perfectly valid. The script also checks the + actual current Winre.wim size at runtime and warns if this value is + too small for it. + +.PARAMETER DiskEndBufferMB + Small safety buffer (MB) left truly unallocated at the very end of the + disk, to avoid colliding with the GPT backup header / alignment + rounding. Default 1. + +.PARAMETER LogPath + Directory for the log file. Default C:\Temp. + +.PARAMETER SkipBanner + Suppresses the ASCII banner. + +.EXAMPLE + .\WinRE-Resize2.ps1 -AuditOnly + +.EXAMPLE + .\WinRE-Resize2.ps1 -RecoverySizeMB 1024 + +.NOTES + Author : F.Š. | Windows Recovery Partition Tool + Version: 2.1.0 +#> + +[CmdletBinding()] +param( + [switch]$AuditOnly, + [int]$RecoverySizeMB = 2000, + [int]$DiskEndBufferMB = 1, + [string]$LogPath = "C:\Temp", + [switch]$SkipBanner +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# ============================================================================ +# GLOBAL STATE +# ============================================================================ +$Script:ToolVersion = "2.1.0" +$Script:LogFile = $null +$Script:TranscriptFile = $null +$Script:CurrentStep = 0 +$Script:TotalSteps = 14 +$Script:RecoveryGptType = "de94bba4-06d1-4d40-a16a-bfd50179d6ac" +$Script:RecoveryMbrType = 27 +$Script:RecoveryGptAttrs = "0x8000000000000001" +$Script:BitLockerSuspended = $false # true while WE hold BitLocker protection suspended + +# ============================================================================ +# FUNCTION: Write-Log +# ============================================================================ +function Write-Log { + param( + [Parameter(Mandatory)][string]$Message, + [ValidateSet('INFO','WARNING','ERROR','SUCCESS','DEBUG')][string]$Level = 'INFO' + ) + + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $hostName = $env:COMPUTERNAME + $line = "[$timestamp] [$Level] [$hostName] $Message" + + if ($Script:LogFile) { + try { Add-Content -Path $Script:LogFile -Value $line -ErrorAction Stop } + catch { Write-Host "LOG WRITE FAILED: $_" -ForegroundColor Red } + } + + switch ($Level) { + 'WARNING' { Write-Host $line -ForegroundColor Yellow } + 'ERROR' { Write-Host $line -ForegroundColor Red } + 'SUCCESS' { Write-Host $line -ForegroundColor Green } + 'DEBUG' { Write-Host $line -ForegroundColor DarkGray } + default { Write-Host $line } + } +} + +# ============================================================================ +# FUNCTION: Write-Banner +# ============================================================================ +function Write-Banner { + if ($SkipBanner) { return } + + $banner = @" + ███████╗███████╗ + ██╔════╝██╔════╝ + █████╗ ███████╗ + ██╔══╝ ╚════██║ + ██║ ███████║ + ╚═╝ ╚══════╝ + + F.Š. | Windows Recovery Partition Tool +"@ + Write-Host $banner -ForegroundColor Cyan + Write-Host "Version : $Script:ToolVersion" + Write-Host "Computer : $env:COMPUTERNAME" + Write-Host "User : $env:USERDOMAIN\$env:USERNAME" + Write-Host "Started : $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')" + Write-Host "PS Ver : $($PSVersionTable.PSVersion.ToString())" + Write-Host "OS : $((Get-CimInstance Win32_OperatingSystem).Caption)" + if ($AuditOnly) { + Write-Host "MODE : AUDIT ONLY - no changes will be made" -ForegroundColor Yellow + } + Write-Host ("=" * 60) + Write-Host "" +} + +# ============================================================================ +# FUNCTION: Write-Step +# ============================================================================ +function Write-Step { + param( + [Parameter(Mandatory)][string]$Description, + [ValidateSet('START','OK','FAIL','SKIP')][string]$Status = 'START' + ) + + if ($Status -eq 'START') { + $Script:CurrentStep++ + $prefix = "[{0}/{1}]" -f $Script:CurrentStep, $Script:TotalSteps + Write-Host ("{0} {1}" -f $prefix, $Description.PadRight(45, '.')) -NoNewline + Write-Log -Message "STEP START: $Description" -Level 'INFO' + } + else { + $color = switch ($Status) { 'OK' {'Green'} 'FAIL' {'Red'} 'SKIP' {'Yellow'} } + Write-Host " [$Status]" -ForegroundColor $color + Write-Log -Message "STEP ${Status}: $Description" -Level $(if ($Status -eq 'FAIL') {'ERROR'} else {'INFO'}) + } +} + +# ============================================================================ +# FUNCTION: Stop-Safely +# ============================================================================ +function Stop-Safely { + param( + [string]$Reason = "User requested termination", + [int]$ExitCode = 1 + ) + + if ($Script:BitLockerSuspended) { + Write-Log -Message "Aborting with BitLocker still suspended - resuming protection before exit." -Level 'WARNING' + Resume-BitLockerProtection -MountPoint 'C:' + $Script:BitLockerSuspended = $false + } + + Write-Log -Message "SCRIPT TERMINATED: $Reason" -Level 'ERROR' + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "SCRIPT STOPPED" -ForegroundColor Red + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "Reason : $Reason" -ForegroundColor Red + if ($Script:LogFile) { Write-Host "Log : $Script:LogFile" -ForegroundColor Red } + Write-Host ("=" * 60) -ForegroundColor Red + + try { Stop-Transcript -ErrorAction SilentlyContinue | Out-Null } catch {} + + exit $ExitCode +} + +# ============================================================================ +# FUNCTION: Test-IsAdministrator +# ============================================================================ +function Test-IsAdministrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +# ============================================================================ +# FUNCTION: Confirm-YesNo (generic Y/N/Q prompt helper) +# ============================================================================ +function Confirm-YesNo { + param( + [Parameter(Mandatory)][string]$Prompt + ) + + while ($true) { + Write-Host "" + Write-Host $Prompt + Write-Host "[Y] Yes [N] No [Q] Quit" -ForegroundColor Cyan + $answer = Read-Host "Your choice" + switch ($answer.Trim().ToUpper()) { + 'Y' { return $true } + 'N' { return $false } + 'Q' { Stop-Safely -Reason "User aborted at confirmation prompt" } + default { Write-Host "Invalid input, please type Y, N or Q." -ForegroundColor Yellow } + } + } +} + +# ============================================================================ +# FUNCTION: Invoke-NativeCommand +# Runs an external .exe and captures stdout+stderr WITHOUT letting +# $ErrorActionPreference = 'Stop' turn stderr lines into terminating +# errors. +# ============================================================================ +function Invoke-NativeCommand { + param( + [Parameter(Mandatory)][string]$FilePath, + [string[]]$ArgumentList = @() + ) + + $previousEAP = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + $global:LASTEXITCODE = 0 + try { + if ($ArgumentList.Count -gt 0) { + $rawOutput = & $FilePath @ArgumentList 2>&1 + } + else { + $rawOutput = & $FilePath 2>&1 + } + } + finally { + $ErrorActionPreference = $previousEAP + } + + $lines = @($rawOutput | ForEach-Object { $_.ToString() }) + + return [PSCustomObject]@{ + Output = $lines + ExitCode = $LASTEXITCODE + } +} + +# ============================================================================ +# FUNCTION: Get-SystemDisk +# ============================================================================ +function Get-SystemDisk { + Write-Log -Message "Detecting system disk hosting C:\" -Level 'INFO' + + $cPartition = Get-Partition -DriveLetter C -ErrorAction Stop + $disk = Get-Disk -Number $cPartition.DiskNumber -ErrorAction Stop + + Write-Log -Message "System disk detected: Disk $($disk.Number) ($($disk.FriendlyName))" -Level 'SUCCESS' + + Write-Host "" + Write-Host "SYSTEM DISK" -ForegroundColor Cyan + Write-Host "Disk Number : $($disk.Number)" + Write-Host "Model : $($disk.FriendlyName)" + Write-Host "Serial Number : $($disk.SerialNumber)" + Write-Host "Bus Type : $($disk.BusType)" + Write-Host "Partition Style : $($disk.PartitionStyle)" + Write-Host "Size : $([math]::Round($disk.Size/1GB,2)) GB" + Write-Host "Operational Status : $($disk.OperationalStatus)" + Write-Host "Health Status : $($disk.HealthStatus)" + + return [PSCustomObject]@{ + Disk = $disk + CPartition = $cPartition + } +} + +# ============================================================================ +# FUNCTION: Get-DiskLayout +# ============================================================================ +function Get-DiskLayout { + param([Parameter(Mandatory)][int]$DiskNumber) + + $partitions = Get-Partition -DiskNumber $DiskNumber | Sort-Object Offset + + Write-Host "" + Write-Host "CURRENT PARTITION LAYOUT (Disk $DiskNumber)" -ForegroundColor Cyan + Write-Host ("{0,-6} {1,-12} {2,-10} {3,-16} {4,-8} {5}" -f "Part#","Type","SizeMB","Offset","Drive","GptType") + foreach ($p in $partitions) { + $gptType = $null + try { $gptType = $p.GptType } catch { $gptType = "n/a" } + Write-Host ("{0,-6} {1,-12} {2,-10} {3,-16} {4,-8} {5}" -f ` + $p.PartitionNumber, $p.Type, [math]::Round($p.Size/1MB,0), $p.Offset, ` + ($(if ($p.DriveLetter) { $p.DriveLetter } else { '-' })), $gptType) + } + + return $partitions +} + +# ============================================================================ +# FUNCTION: Get-TrailingFreeSpaceMB +# ============================================================================ +function Get-TrailingFreeSpaceMB { + param([Parameter(Mandatory)][int]$DiskNumber) + + $disk = Get-Disk -Number $DiskNumber + $freeBytes = $disk.Size - $disk.AllocatedSize + if ($freeBytes -lt 0) { $freeBytes = 0 } + return [math]::Floor($freeBytes / 1MB) +} + +# ============================================================================ +# FUNCTION: Get-WinREStatus +# ============================================================================ +function Get-WinREStatus { + Write-Log -Message "Querying reagentc /info" -Level 'INFO' + + $result = Invoke-NativeCommand -FilePath 'reagentc.exe' -ArgumentList @('/info') + $raw = $result.Output + Write-Log -Message "reagentc /info output (exit $($result.ExitCode)): `n$($raw -join "`n")" -Level 'DEBUG' + + $statusLine = $raw | Where-Object { $_ -match 'Windows RE status' } + $locationLine = $raw | Where-Object { $_ -match 'Windows RE location' } + + $status = if ($statusLine -match ':\s*(.+)$') { $Matches[1].Trim() } else { 'Unknown' } + $location = if ($locationLine -match 'harddisk(\d+)\\partition(\d+)') { + [PSCustomObject]@{ DiskNumber = [int]$Matches[1]; PartitionNumber = [int]$Matches[2] } + } else { $null } + + return [PSCustomObject]@{ + Status = $status + Location = $location + RawOutput = $raw + } +} + +# ============================================================================ +# FUNCTION: Get-RecoveryPartition +# ============================================================================ +function Get-RecoveryPartition { + param( + [Parameter(Mandatory)]$Disk, + [Parameter(Mandatory)]$Partitions, + [Parameter(Mandatory)]$CPartition, + [Parameter(Mandatory)]$WinREStatus + ) + + Write-Log -Message "Detecting Recovery Partition candidates" -Level 'INFO' + + $candidates = @() + + foreach ($p in $Partitions) { + if ($p.PartitionNumber -eq $CPartition.PartitionNumber) { continue } + + $isCandidate = $false + $typeMatches = $false + $reasons = @() + + if ($Disk.PartitionStyle -eq 'GPT') { + try { + if ($p.GptType -and ($p.GptType.Trim('{}').ToLower() -eq $Script:RecoveryGptType)) { + $typeMatches = $true + } + } catch {} + } + elseif ($Disk.PartitionStyle -eq 'MBR') { + try { + if ($p.MbrType -eq $Script:RecoveryMbrType) { + $typeMatches = $true + } + } catch {} + } + + if ($typeMatches) { + $isCandidate = $true + $reasons += if ($Disk.PartitionStyle -eq 'GPT') { "GptType matches Recovery GUID" } else { "MbrType = 27" } + if ($p.DriveLetter) { + $reasons += "WARNING: has drive letter $($p.DriveLetter): - unusual for a genuine Recovery partition, verify manually" + } else { + $reasons += "No drive letter" + } + } + + if ($p.Offset -gt $CPartition.Offset) { $reasons += "Located after C:" } + + if ($WinREStatus.Location -and $WinREStatus.Location.PartitionNumber -eq $p.PartitionNumber) { + $isCandidate = $true + $reasons += "Matches reagentc /info WinRE location" + } + + if (-not $isCandidate -and -not $p.DriveLetter -and $p.Offset -ge ($CPartition.Offset + $CPartition.Size) -and ($p.Offset - ($CPartition.Offset + $CPartition.Size)) -lt 1MB -and $p.Size -le 4096MB) { + $isCandidate = $true + $reasons += "UNRECOGNIZED TYPE but immediately after C: with no gap and no drive letter - likely an orphaned Recovery-ish partition" + } + + if ($isCandidate) { + $candidates += [PSCustomObject]@{ + Partition = $p + Reasons = $reasons + } + } + } + + if ($candidates.Count -eq 0) { + Write-Log -Message "No existing Recovery Partition detected on this disk." -Level 'INFO' + return $null + } + + if ($candidates.Count -gt 1) { + Write-Log -Message "Multiple Recovery Partition candidates found ($($candidates.Count)) - cannot auto-select" -Level 'ERROR' + Write-Host "" + Write-Host "AMBIGUOUS: multiple possible Recovery partitions detected:" -ForegroundColor Red + foreach ($c in $candidates) { + Write-Host " - Partition $($c.Partition.PartitionNumber): $($c.Reasons -join '; ')" + } + return $null + } + + $winner = $candidates[0] + Write-Log -Message "Recovery Partition identified: Partition $($winner.Partition.PartitionNumber) [$($winner.Reasons -join '; ')]" -Level 'SUCCESS' + + return $winner.Partition +} + +# ============================================================================ +# FUNCTION: Test-DiskLayout +# ============================================================================ +function Test-DiskLayout { + param( + [Parameter(Mandatory)]$Partitions, + [Parameter(Mandatory)]$CPartition, + [Parameter(Mandatory)]$RecoveryPartition + ) + + $sorted = $Partitions | Sort-Object Offset + $cIndex = [array]::IndexOf($sorted.PartitionNumber, $CPartition.PartitionNumber) + $rIndex = [array]::IndexOf($sorted.PartitionNumber, $RecoveryPartition.PartitionNumber) + + if ($rIndex -ne ($cIndex + 1)) { + Write-Log -Message "Recovery partition is NOT directly behind C: (other partitions in between)" -Level 'ERROR' + return $false + } + + Write-Log -Message "Disk layout validated: Recovery partition is immediately behind C:" -Level 'SUCCESS' + return $true +} + +# ============================================================================ +# FUNCTION: Confirm-RecoveryPartition +# ============================================================================ +function Confirm-RecoveryPartition { + param( + [Parameter(Mandatory)][int]$DiskNumber, + [Parameter(Mandatory)]$RecoveryPartition, + [Parameter(Mandatory)]$CPartition + ) + + $gptType = $null + try { $gptType = $RecoveryPartition.GptType } catch { $gptType = "n/a" } + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "RECOVERY PARTITION DETECTION" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "The script believes the following partition is WinRE:" + Write-Host "" + Write-Host "Disk Number : $DiskNumber" + Write-Host "Partition Number : $($RecoveryPartition.PartitionNumber)" + Write-Host "Size : $([math]::Round($RecoveryPartition.Size/1MB,0)) MB" + Write-Host "Offset : $($RecoveryPartition.Offset)" + Write-Host "Type : $($RecoveryPartition.Type)" + Write-Host "GPT Type : $gptType" + Write-Host "Drive Letter : $(if ($RecoveryPartition.DriveLetter) { $RecoveryPartition.DriveLetter } else { 'None' })" + Write-Host "Position : $(if ($RecoveryPartition.Offset -gt $CPartition.Offset) { 'After C:' } else { 'Before C:' })" + + return Confirm-YesNo -Prompt "Is this the CORRECT Recovery Partition (to be deleted and recreated)?" +} + +# ============================================================================ +# FUNCTION: Confirm-Snapshot +# ============================================================================ +function Confirm-Snapshot { + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Yellow + Write-Host "IMPORTANT - VM SNAPSHOT / CHECKPOINT" -ForegroundColor Yellow + Write-Host ("=" * 60) -ForegroundColor Yellow + Write-Host @" +Before continuing, verify that this VM does NOT have an +active snapshot (VMware) or checkpoint (Hyper-V). + +This script cannot guarantee that VMware snapshots or +Hyper-V checkpoints are correctly detected from inside +Windows. + +Recommended: +- Verify VMware snapshot / Hyper-V checkpoint state manually. +- Verify current backup exists and is valid. +"@ + Write-Host ("=" * 60) -ForegroundColor Yellow + + return Confirm-YesNo -Prompt "Have you verified that the VM has no active snapshot/checkpoint (VMware or Hyper-V)?" +} + +# ============================================================================ +# FUNCTION: Confirm-Backup +# ============================================================================ +function Confirm-Backup { + return Confirm-YesNo -Prompt "Have you verified that a recent successful backup exists?" +} + +# ============================================================================ +# FUNCTION: Get-BitLockerInfo +# ============================================================================ +function Get-BitLockerInfo { + param([string]$MountPoint = 'C:') + + try { + return Get-BitLockerVolume -MountPoint $MountPoint -ErrorAction Stop + } + catch { + Write-Log -Message "Could not query BitLocker status for ${MountPoint}: $_" -Level 'DEBUG' + return $null + } +} + +# ============================================================================ +# FUNCTION: Suspend-BitLockerProtection +# ============================================================================ +function Suspend-BitLockerProtection { + param([string]$MountPoint = 'C:') + + $info = Get-BitLockerInfo -MountPoint $MountPoint + if (-not $info -or $info.VolumeStatus -eq 'FullyDecrypted') { + return $false + } + if ($info.ProtectionStatus -ne 'On') { + Write-Log -Message "BitLocker on $MountPoint is encrypted but protection is already Off - nothing to suspend." -Level 'INFO' + return $false + } + + Write-Log -Message "Suspending BitLocker protection on $MountPoint for 1 restart (required before reagentc/diskpart touch the boot config)." -Level 'WARNING' + try { + Suspend-BitLocker -MountPoint $MountPoint -RebootCount 1 -ErrorAction Stop | Out-Null + } + catch { + Write-Log -Message "Failed to suspend BitLocker on ${MountPoint}: $_" -Level 'ERROR' + return $false + } + + Write-Log -Message "BitLocker protection suspended on $MountPoint." -Level 'SUCCESS' + return $true +} + +# ============================================================================ +# FUNCTION: Resume-BitLockerProtection +# ============================================================================ +function Resume-BitLockerProtection { + param([string]$MountPoint = 'C:') + + Write-Log -Message "Resuming BitLocker protection on $MountPoint." -Level 'INFO' + try { + Resume-BitLocker -MountPoint $MountPoint -ErrorAction Stop | Out-Null + Write-Log -Message "BitLocker protection resumed on $MountPoint." -Level 'SUCCESS' + } + catch { + Write-Log -Message "FAILED to resume BitLocker protection on ${MountPoint}: $_. Resume it manually: Resume-BitLocker -MountPoint $MountPoint (or 'manage-bde -protectors -enable $MountPoint')." -Level 'ERROR' + } +} + +# ============================================================================ +# FUNCTION: Wait-BitLockerDecryption +# ============================================================================ +function Wait-BitLockerDecryption { + param([string]$MountPoint = 'C:') + + $bitlocker = $null + while ($true) { + try { + $bitlocker = Get-BitLockerVolume -MountPoint $MountPoint -ErrorAction Stop + } + catch { + Write-Log -Message "Could not query BitLocker status for ${MountPoint} while waiting for decryption: $_" -Level 'ERROR' + break + } + + Clear-Host + Write-Host "BitLocker decryption progress" -ForegroundColor Cyan + Write-Host "--------------------------------" + Write-Host "Volume : $($bitlocker.MountPoint)" + Write-Host "Status : $($bitlocker.VolumeStatus)" + Write-Host "Progress : $($bitlocker.EncryptionPercentage)%" + Write-Host "Protection : $($bitlocker.ProtectionStatus)" + Write-Host "" + Write-Host "Last check : $(Get-Date)" + + if ($bitlocker.VolumeStatus -ne 'DecryptionInProgress') { + Write-Host "" + Write-Host "Decryption is no longer in progress." -ForegroundColor Green + break + } + + Start-Sleep -Seconds 5 + } + + return $bitlocker +} + +# ============================================================================ +# FUNCTION: Invoke-BitLockerDecryptWorkaround +# ============================================================================ +function Invoke-BitLockerDecryptWorkaround { + param([string]$MountPoint = 'C:') + + $before = Get-BitLockerInfo -MountPoint $MountPoint + if (-not $before -or $before.VolumeStatus -eq 'FullyDecrypted') { + return [PSCustomObject]@{ Success = $true; ProtectorTypes = @() } + } + + $protectorTypes = @($before.KeyProtector | ForEach-Object { $_.KeyProtectorType }) + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "BITLOCKER FULL DECRYPT WORKAROUND" -ForegroundColor Red + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host @" +reagentc.exe refuses to install WinRE while $MountPoint carries +ANY BitLocker encryption - confirmed via ReAgent.log: it falls +back to targeting the OS partition itself and then rejects that +purely because of BitLocker, even with protection suspended/off. + +The only known workaround is to FULLY decrypt $MountPoint, enable +WinRE, then re-encrypt it again. The disk will be UNENCRYPTED for +the entire decrypt + re-encrypt duration. + +Protectors currently on ${MountPoint}: $($protectorTypes -join ', ') +This script can only restore a plain TPM and/or recovery-password +protector afterwards - NOT any PIN/startup-key/AD-Entra escrow +setup you may have. You are responsible for reconfiguring that +and re-escrowing any new recovery key per your organization policy. +"@ + Write-Host ("=" * 60) -ForegroundColor Red + + if (-not (Confirm-YesNo -Prompt "Fully decrypt $MountPoint now so WinRE can be enabled, then re-encrypt it afterwards?")) { + return [PSCustomObject]@{ Success = $false; ProtectorTypes = $protectorTypes } + } + + Write-Log -Message "Disabling BitLocker (full decrypt) on $MountPoint. Protectors before: $($protectorTypes -join ', ')" -Level 'WARNING' + try { + Disable-BitLocker -MountPoint $MountPoint -ErrorAction Stop | Out-Null + } + catch { + Write-Log -Message "Disable-BitLocker failed: $_" -Level 'ERROR' + return [PSCustomObject]@{ Success = $false; ProtectorTypes = $protectorTypes } + } + + $final = Wait-BitLockerDecryption -MountPoint $MountPoint + + if (-not $final -or $final.VolumeStatus -ne 'FullyDecrypted') { + $endStatus = if ($final) { $final.VolumeStatus } else { 'Unknown' } + Write-Log -Message "$MountPoint did not reach FullyDecrypted (ended at $endStatus) - aborting workaround." -Level 'ERROR' + return [PSCustomObject]@{ Success = $false; ProtectorTypes = $protectorTypes } + } + + Write-Log -Message "$MountPoint is now fully decrypted." -Level 'SUCCESS' + return [PSCustomObject]@{ Success = $true; ProtectorTypes = $protectorTypes } +} + +# ============================================================================ +# FUNCTION: Invoke-BitLockerReencrypt +# ============================================================================ +function Invoke-BitLockerReencrypt { + param( + [string]$MountPoint = 'C:', + [string[]]$OriginalProtectorTypes = @() + ) + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Yellow + Write-Host "RE-ENCRYPTING $MountPoint" -ForegroundColor Yellow + Write-Host ("=" * 60) -ForegroundColor Yellow + + try { + if ($OriginalProtectorTypes -contains 'Tpm') { + Write-Log -Message "Re-enabling BitLocker on $MountPoint with a TPM protector (matches what was there before)." -Level 'INFO' + Enable-BitLocker -MountPoint $MountPoint -TpmProtector -SkipHardwareTest -ErrorAction Stop | Out-Null + } + else { + Write-Log -Message "Re-enabling BitLocker on $MountPoint with a recovery-password protector (no TPM protector was present before)." -Level 'INFO' + Enable-BitLocker -MountPoint $MountPoint -RecoveryPasswordProtector -SkipHardwareTest -ErrorAction Stop | Out-Null + } + + $info = Get-BitLockerInfo -MountPoint $MountPoint + if (-not ($info.KeyProtector | Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' })) { + Add-BitLockerKeyProtector -MountPoint $MountPoint -RecoveryPasswordProtector -ErrorAction Stop | Out-Null + $info = Get-BitLockerInfo -MountPoint $MountPoint + } + } + catch { + Write-Log -Message "Re-enabling BitLocker on $MountPoint failed: $_. You MUST re-encrypt $MountPoint manually." -Level 'ERROR' + return $false + } + + $rp = $info.KeyProtector | Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' } | Select-Object -First 1 + if ($rp) { + Write-Host "" + Write-Host "!!! NEW BITLOCKER RECOVERY PASSWORD - RECORD THIS NOW !!!" -ForegroundColor Red + Write-Host "Key protector ID : $($rp.KeyProtectorId)" -ForegroundColor Red + Write-Host "Recovery password : $($rp.RecoveryPassword)" -ForegroundColor Red + Write-Log -Message "New BitLocker recovery password on ${MountPoint}: ID $($rp.KeyProtectorId) = $($rp.RecoveryPassword)" -Level 'WARNING' + } + + Write-Host "" + Write-Host "Encryption has (re)started in the background on $MountPoint." -ForegroundColor Yellow + if ($OriginalProtectorTypes -and ($OriginalProtectorTypes | Where-Object { $_ -ne 'Tpm' -and $_ -ne 'RecoveryPassword' })) { + Write-Host "Original protectors also included: $($OriginalProtectorTypes -join ', ') - reconfigure any PIN/startup-key/AD-Entra escrow manually." -ForegroundColor Yellow + } + Write-Log -Message "Re-encryption of $MountPoint started (runs in the background)." -Level 'SUCCESS' + return $true +} + +# ============================================================================ +# FUNCTION: Show-PreflightChecklist +# ============================================================================ +function Show-PreflightChecklist { + param( + [Parameter(Mandatory)][int]$DiskNumber, + [Parameter(Mandatory)][string]$WinREStatusText, + [Parameter(Mandatory)][bool]$HasExistingRecovery, + [Parameter(Mandatory)][int]$AvailableMB, + [Parameter(Mandatory)][int]$RecoverySizeMB, + [Parameter()]$BitLockerInfo + ) + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "PRE-FLIGHT CHECKLIST (already verified automatically)" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "[OK] Running as Administrator, required cmdlets/tools present" + Write-Host "[OK] System disk detected: Disk $DiskNumber" + Write-Host "[OK] WinRE status queried: $WinREStatusText" + if ($HasExistingRecovery) { + Write-Host "[OK] Existing Recovery partition detected (identity confirmed next)" + } else { + Write-Host "[--] No existing Recovery partition found - fresh build" + } + Write-Host "[OK] Free space check: $AvailableMB MB available / $RecoverySizeMB MB required" + if ($BitLockerInfo) { + if ($BitLockerInfo.ProtectionStatus -eq 'On') { + Write-Host "[!!] BitLocker on C: is $($BitLockerInfo.VolumeStatus), protection ON - will be suspended automatically for the destructive steps and resumed afterwards" -ForegroundColor Yellow + } else { + Write-Host "[OK] BitLocker on C: is $($BitLockerInfo.VolumeStatus), protection $($BitLockerInfo.ProtectionStatus) - no suspend needed" + } + } else { + Write-Host "[--] BitLocker not present/not queryable on C:" + } + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "" + Write-Host "Still needs YOUR manual confirmation:" -ForegroundColor Yellow + if ($HasExistingRecovery) { + Write-Host " - Identity of the detected Recovery partition" + } + Write-Host " - VM snapshot (VMware) / checkpoint (Hyper-V) state" + Write-Host " - Existence of a recent, successful backup" + Write-Host "" +} + +# ============================================================================ +# FUNCTION: Get-ReAgentLogTail +# ============================================================================ +function Get-ReAgentLogTail { + param([int]$Lines = 40) + + $logPath = Join-Path -Path $env:WINDIR -ChildPath 'Logs\ReAgent\ReAgent.log' + if (-not (Test-Path -Path $logPath)) { return $null } + + try { + return Get-Content -Path $logPath -Tail $Lines -ErrorAction Stop + } + catch { + Write-Log -Message "Could not read ${logPath}: $_" -Level 'DEBUG' + return $null + } +} + +# ============================================================================ +# FUNCTION: Get-WinREWimSizeMB +# ============================================================================ +function Get-WinREWimSizeMB { + $candidatePaths = @( + (Join-Path -Path $env:WINDIR -ChildPath 'System32\Recovery\Winre.wim') + ) + foreach ($path in $candidatePaths) { + if (Test-Path -Path $path) { + try { + $bytes = (Get-Item -Path $path -Force -ErrorAction Stop).Length + return [math]::Ceiling($bytes / 1MB) + } + catch { + Write-Log -Message "Could not read size of ${path}: $_" -Level 'DEBUG' + } + } + } + return $null +} + +# ============================================================================ +# FUNCTION: Get-RecommendedRecoverySizeMB +# ============================================================================ +function Get-RecommendedRecoverySizeMB { + param([int]$FallbackMB = 2000) + + $wimSizeMB = Get-WinREWimSizeMB + if (-not $wimSizeMB) { return $FallbackMB } + + $recommended = $wimSizeMB + 400 + 100 + return [math]::Max($recommended, $FallbackMB) +} + +# ============================================================================ +# FUNCTION: Disable-WinRE +# ============================================================================ +function Disable-WinRE { + Write-Log -Message "Disabling WinRE via reagentc /disable" -Level 'INFO' + + $result = Invoke-NativeCommand -FilePath 'reagentc.exe' -ArgumentList @('/disable') + Write-Log -Message "reagentc /disable output (exit $($result.ExitCode)): $($result.Output -join ' | ')" -Level 'DEBUG' + + Start-Sleep -Seconds 2 + $status = Get-WinREStatus + + if ($status.Status -notmatch 'Disabled') { + Write-Log -Message "WinRE could not be disabled. Current status: $($status.Status)" -Level 'ERROR' + return $false + } + + Write-Log -Message "WinRE successfully disabled" -Level 'SUCCESS' + return $true +} + +# ============================================================================ +# FUNCTION: Enable-WinRE +# ============================================================================ +function Enable-WinRE { + Write-Log -Message "Enabling WinRE via reagentc /enable" -Level 'INFO' + + $result = Invoke-NativeCommand -FilePath 'reagentc.exe' -ArgumentList @('/enable') + Write-Log -Message "reagentc /enable output (exit $($result.ExitCode)): $($result.Output -join ' | ')" -Level 'DEBUG' + + Start-Sleep -Seconds 2 + $status = Get-WinREStatus + + if ($status.Status -notmatch 'Enabled') { + Write-Log -Message "WinRE could not be enabled. Current status: $($status.Status)" -Level 'ERROR' + Write-Log -Message "Full reagentc output: `n$($status.RawOutput -join "`n")" -Level 'ERROR' + + $reagentLog = Get-ReAgentLogTail -Lines 40 + if ($reagentLog) { + Write-Log -Message "Tail of $env:WINDIR\Logs\ReAgent\ReAgent.log (often shows the REAL cause behind a misleading console message):`n$($reagentLog -join "`n")" -Level 'ERROR' + } + + if (($result.Output -join ' ') -match 'BitLocker') { + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "REAGENTC REFUSED - CITES BITLOCKER" -ForegroundColor Red + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host @" +reagentc.exe refused to enable WinRE citing BitLocker Drive +Encryption on this volume - even with BitLocker protection +suspended/off. This is a documented reagentc limitation on some +builds: it can block whenever the volume carries ANY BitLocker +encryption at all, regardless of protector/suspend state, so +suspending protectors (what this script already tried) is not +always enough. + +Known workaround - NOT automated here on purpose, because it +means fully decrypting and later re-encrypting C:, and only you +know which protectors / recovery-key escrow policy (TPM, PIN, +AD/Entra/MBAM) need restoring afterwards: + 1. manage-bde -off C: (full decrypt - can take a while) + 2. reagentc /enable + 3. Re-enable BitLocker with your normal protector setup. + +IMPORTANT: this exact console message is also known to be +misleading - the REAL cause can instead be an undersized +Recovery partition for winre.wim, or the new partition's ID/ +attributes not yet recognized by reagentc, OR (v2.1) a gap +between C: and the Recovery partition if adjacency turns out to +matter. Check the ReAgent.log excerpt above (also in the log +file) before assuming it really is BitLocker. +"@ + Write-Host ("=" * 60) -ForegroundColor Red + } + + return $false + } + + Write-Log -Message "WinRE successfully enabled" -Level 'SUCCESS' + return $true +} + +# ============================================================================ +# FUNCTION: Invoke-DiskpartScript +# ============================================================================ +function Invoke-DiskpartScript { + param([Parameter(Mandatory)][string[]]$Commands) + + $tempScript = Join-Path -Path $env:TEMP -ChildPath "diskpart_$([guid]::NewGuid().ToString('N')).txt" + $Commands | Set-Content -Path $tempScript -Encoding ASCII + + Write-Log -Message "Running diskpart script: `n$($Commands -join "`n")" -Level 'DEBUG' + + $result = Invoke-NativeCommand -FilePath 'diskpart.exe' -ArgumentList @('/s', $tempScript) + Write-Log -Message "diskpart output (exit $($result.ExitCode)): `n$($result.Output -join "`n")" -Level 'DEBUG' + + Remove-Item -Path $tempScript -Force -ErrorAction SilentlyContinue + + return $result.Output +} + +# ============================================================================ +# FUNCTION: Remove-RecoveryPartition +# ============================================================================ +function Remove-RecoveryPartition { + param( + [Parameter(Mandatory)][int]$DiskNumber, + [Parameter(Mandatory)][int]$PartitionNumber + ) + + Write-Log -Message "Removing Recovery Partition: Disk $DiskNumber / Partition $PartitionNumber" -Level 'WARNING' + + $commands = @( + "select disk $DiskNumber" + "select partition $PartitionNumber" + "delete partition override" + "exit" + ) + + Invoke-DiskpartScript -Commands $commands | Out-Null + + Start-Sleep -Seconds 2 + $stillExists = Get-Partition -DiskNumber $DiskNumber -ErrorAction SilentlyContinue | + Where-Object { $_.PartitionNumber -eq $PartitionNumber } + + if ($stillExists) { + Write-Log -Message "Recovery Partition removal FAILED - partition still present" -Level 'ERROR' + return $false + } + + Write-Log -Message "Recovery Partition removed successfully" -Level 'SUCCESS' + return $true +} + +# ============================================================================ +# FUNCTION: New-RecoveryPartition +# v2.1 FIX: creates the Recovery partition at the TRUE END of the disk +# using an EXPLICIT -Offset computed from disk.Size, instead of letting +# New-Partition auto-place it. Auto-placement (no -Offset) puts a new +# partition at the START of the first available free extent - which, +# right after removing the old Recovery, is the space starting +# immediately after C: - NOT the end of the disk. That silently glued +# Recovery onto C: and pushed all remaining free space past it, to the +# true end of the disk, which is the opposite of this script's design +# intent (Recovery at the true end, C: expandable into the gap). +# +# No longer checks/enforces adjacency to C: at creation time - Recovery +# is intentionally allowed to have a gap in front of it here. Adjacency +# (if it matters to reagentc) is established afterwards, optionally, by +# expanding C: into that gap - see the "Expand C:?" step in MAIN, which +# now runs BEFORE Enable-WinRE. +# ============================================================================ +function New-RecoveryPartition { + param( + [Parameter(Mandatory)][int]$DiskNumber, + [Parameter(Mandatory)][int]$RecoverySizeMB, + [Parameter(Mandatory)][string]$PartitionStyle, + [int]$EndBufferMB = 1 + ) + + $sizeBytes = [uint64]$RecoverySizeMB * 1MB + $bufferBytes = [uint64]$EndBufferMB * 1MB + + $disk = Get-Disk -Number $DiskNumber + $rawOffset = $disk.Size - $bufferBytes - $sizeBytes + if ($rawOffset -le 0) { + Write-Log -Message "Computed Recovery offset ($rawOffset) is not positive - disk is too small for a $RecoverySizeMB MB Recovery partition plus $EndBufferMB MB buffer." -Level 'ERROR' + return $null + } + $alignedOffset = [uint64]([math]::Floor($rawOffset / 1MB) * 1MB) + + Write-Log -Message "Creating new Recovery Partition ($RecoverySizeMB MB) on Disk $DiskNumber at explicit end-of-disk offset $alignedOffset (disk size $($disk.Size) bytes, buffer $EndBufferMB MB)" -Level 'INFO' + + try { + $newPartition = New-Partition -DiskNumber $DiskNumber -Size $sizeBytes -Offset $alignedOffset -AssignDriveLetter:$false -ErrorAction Stop + } + catch { + Write-Log -Message "New-Partition at offset $alignedOffset failed: $_" -Level 'ERROR' + Write-Log -Message "This usually means the trailing free space no longer matches what the free-space check saw - re-run the script from Step 1 so it re-reads the current disk layout instead of continuing from a stale state." -Level 'ERROR' + return $null + } + + # Sanity check: it must actually have landed at (or very near, after + # alignment rounding) the true end of the disk. + $diskEndSlack = 2MB + $partitionEndOffset = $newPartition.Offset + $newPartition.Size + if (($disk.Size - $partitionEndOffset) -gt ($bufferBytes + $diskEndSlack)) { + Write-Log -Message "New Recovery partition ends at $partitionEndOffset, but disk size is $($disk.Size) bytes - it does not appear to be at the true end of the disk. Removing it and aborting rather than leaving a stray partition behind." -Level 'ERROR' + try { + Remove-Partition -DiskNumber $DiskNumber -PartitionNumber $newPartition.PartitionNumber -Confirm:$false -ErrorAction Stop + Write-Log -Message "Removed the misplaced partition (was Partition $($newPartition.PartitionNumber))." -Level 'INFO' + } + catch { + Write-Log -Message "Could not remove the misplaced partition automatically: $_. Remove Partition $($newPartition.PartitionNumber) on Disk $DiskNumber manually before re-running." -Level 'ERROR' + } + return $null + } + + try { + Format-Volume -Partition $newPartition -FileSystem NTFS -NewFileSystemLabel "Recovery" -Confirm:$false -Force -ErrorAction Stop | Out-Null + } + catch { + Write-Log -Message "Format-Volume failed: $_" -Level 'ERROR' + return $null + } + + Write-Log -Message "New Recovery Partition created: Partition $($newPartition.PartitionNumber) at offset $($newPartition.Offset)" -Level 'SUCCESS' + + $allParts = Get-Partition -DiskNumber $DiskNumber | Sort-Object Offset + if ($allParts[-1].PartitionNumber -ne $newPartition.PartitionNumber) { + Write-Log -Message "WARNING: newly created Recovery partition is NOT the last partition on the disk. Layout may be unexpected." -Level 'WARNING' + } + + $commands = @("select disk $DiskNumber", "select partition $($newPartition.PartitionNumber)") + + if ($PartitionStyle -eq 'GPT') { + $commands += "set id=$Script:RecoveryGptType" + $commands += "gpt attributes=$Script:RecoveryGptAttrs" + } + else { + $commands += "set id=$Script:RecoveryMbrType" + } + $commands += "exit" + + Invoke-DiskpartScript -Commands $commands | Out-Null + + Start-Sleep -Seconds 2 + $verifyPartition = Get-Partition -DiskNumber $DiskNumber -PartitionNumber $newPartition.PartitionNumber + + Write-Log -Message "Recovery Partition type configured (Style: $PartitionStyle)" -Level 'SUCCESS' + + return $verifyPartition +} + +# ============================================================================ +# FUNCTION: Resize-SystemPartition +# Expands C: into whatever contiguous free space remains (the gap +# between C: and the already-created, true-end-of-disk Recovery +# partition). Get-PartitionSupportedSize naturally stops right before +# Recovery, so no manual reserve math is needed. +# ============================================================================ +function Resize-SystemPartition { + param([Parameter(Mandatory)][int]$DiskNumber) + + $before = Get-Partition -DiskNumber $DiskNumber | Where-Object { $_.DriveLetter -eq 'C' } + $beforeSizeGB = [math]::Round($before.Size / 1GB, 2) + + $supported = Get-PartitionSupportedSize -DiskNumber $DiskNumber -PartitionNumber $before.PartitionNumber + $targetSize = $supported.SizeMax + + if ($targetSize -le $before.Size) { + Write-Log -Message "C: is already at its maximum possible size - nothing to expand" -Level 'WARNING' + return $true + } + + Write-Log -Message "Resizing C: from $([math]::Round($before.Size/1GB,2)) GB to $([math]::Round($targetSize/1GB,2)) GB" -Level 'INFO' + + try { + Resize-Partition -DiskNumber $DiskNumber -PartitionNumber $before.PartitionNumber -Size $targetSize -ErrorAction Stop + } + catch { + Write-Log -Message "Resize-Partition failed: $_" -Level 'ERROR' + return $false + } + + Start-Sleep -Seconds 2 + $after = Get-Partition -DriveLetter C + $afterSizeGB = [math]::Round($after.Size / 1GB, 2) + + Write-Host "" + Write-Host "C: before: $beforeSizeGB GB" + Write-Host "C: after : $afterSizeGB GB" + Write-Host "Increase : +$([math]::Round($afterSizeGB - $beforeSizeGB, 2)) GB" + + if ($after.Size -le $before.Size) { + Write-Log -Message "C: partition size did not increase as expected" -Level 'ERROR' + return $false + } + + Write-Log -Message "C: successfully expanded to $afterSizeGB GB" -Level 'SUCCESS' + return $true +} + +# ============================================================================ +# FUNCTION: Test-FinalState +# ============================================================================ +function Test-FinalState { + param( + [Parameter(Mandatory)][int]$DiskNumber, + [Parameter(Mandatory)]$RecoveryPartition + ) + + $results = [ordered]@{} + + $disk = Get-Disk -Number $DiskNumber + $results['Disk online/healthy'] = ($disk.OperationalStatus -eq 'Online' -and $disk.HealthStatus -eq 'Healthy') + + $c = Get-Partition -DriveLetter C + $cVolume = Get-Volume -DriveLetter C + $results['C: exists and NTFS'] = ($cVolume.FileSystem -eq 'NTFS') + + $recovery = Get-Partition -DiskNumber $DiskNumber -PartitionNumber $RecoveryPartition.PartitionNumber + $results['Recovery exists'] = [bool]$recovery + $results['Recovery has no drive letter'] = (-not $recovery.DriveLetter) + + $allPartitions = Get-Partition -DiskNumber $DiskNumber | Sort-Object Offset + $results['Recovery is last partition'] = ($allPartitions[-1].PartitionNumber -eq $recovery.PartitionNumber) + + $winre = Get-WinREStatus + $results['WinRE enabled'] = ($winre.Status -match 'Enabled') + $results['WinRE location matches new Recovery'] = ( + $winre.Location -and + $winre.Location.DiskNumber -eq $DiskNumber -and + $winre.Location.PartitionNumber -eq $recovery.PartitionNumber + ) + + return $results +} + +# ============================================================================ +# FUNCTION: Show-FinalSummary +# ============================================================================ +function Show-FinalSummary { + param( + [Parameter(Mandatory)][hashtable]$Results, + [Parameter(Mandatory)][double]$CSizeBeforeGB, + [Parameter(Mandatory)][double]$CSizeAfterGB, + [Parameter(Mandatory)][int]$RecoverySizeMB, + [Parameter(Mandatory)][bool]$CExpanded, + [Parameter(Mandatory)][int]$RemainingFreeMB + ) + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "FINAL VALIDATION" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan + + $allOk = $true + foreach ($key in $Results.Keys) { + $status = if ($Results[$key]) { "[OK]" } else { $allOk = $false; "[FAIL]" } + $color = if ($Results[$key]) { 'Green' } else { 'Red' } + Write-Host ("{0} {1}" -f $status, $key) -ForegroundColor $color + } + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "RESULT" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "C: BEFORE : $CSizeBeforeGB GB" + Write-Host "C: AFTER : $CSizeAfterGB GB" + if ($CExpanded) { + Write-Host "GAIN : +$([math]::Round($CSizeAfterGB - $CSizeBeforeGB, 2)) GB" + } else { + Write-Host "GAIN : C: was NOT expanded (skipped on request)" + Write-Host "REMAINING : ~$RemainingFreeMB MB free between C: and Recovery (unallocated)" + } + Write-Host "" + Write-Host "Recovery Partition:" + Write-Host "Size : $RecoverySizeMB MB" + Write-Host "Position : Last partition" + Write-Host "" + + if ($allOk) { + Write-Host "STATUS: SUCCESS" -ForegroundColor Green + Write-Log -Message "FINAL STATUS: SUCCESS" -Level 'SUCCESS' + } else { + Write-Host "STATUS: FAILED" -ForegroundColor Red + Write-Log -Message "FINAL STATUS: FAILED" -Level 'ERROR' + } + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Yellow + Write-Host "POST-ACTION VM CHECK" -ForegroundColor Yellow + Write-Host ("=" * 60) -ForegroundColor Yellow + Write-Host @" +If you created a VMware snapshot or Hyper-V checkpoint before +this operation, do NOT keep it longer than necessary. + +Verify: +- Windows boots normally +- C: has the expected size +- WinRE is Enabled +- Recovery partition is present +- Backup is healthy + +Only after validation should the temporary snapshot +be removed according to your VMware/backup procedure. +"@ + Write-Host ("=" * 60) -ForegroundColor Yellow +} + +# ============================================================================ +# MAIN EXECUTION +# ============================================================================ +try { + if (-not (Test-Path -Path $LogPath)) { + New-Item -Path $LogPath -ItemType Directory -Force | Out-Null + } + $timestampForFile = Get-Date -Format 'yyyyMMdd-HHmmss' + $Script:LogFile = Join-Path -Path $LogPath -ChildPath "WinRE-Resize-$timestampForFile-$env:COMPUTERNAME.log" + $Script:TranscriptFile = Join-Path -Path $LogPath -ChildPath "WinRE-Resize-$timestampForFile-$env:COMPUTERNAME.transcript.log" + + try { Start-Transcript -Path $Script:TranscriptFile -Force | Out-Null } catch { + Write-Host "WARNING: could not start transcript ($_)" -ForegroundColor Yellow + } + + Write-Banner + Write-Log -Message "=== WinRE-Resize2.ps1 v$Script:ToolVersion started ===" -Level 'INFO' + Write-Log -Message "Parameters: AuditOnly=$AuditOnly RecoverySizeMB=$RecoverySizeMB DiskEndBufferMB=$DiskEndBufferMB LogPath=$LogPath" -Level 'INFO' + + # ------------------------------------------------------------------ + # PHASE 1 - DISCOVERY + # ------------------------------------------------------------------ + + Write-Step -Description "Checking administrator privileges" -Status 'START' + if (-not (Test-IsAdministrator)) { + Write-Step -Description "Checking administrator privileges" -Status 'FAIL' + Stop-Safely -Reason "Script must be run as Administrator. Please re-launch PowerShell elevated." + } + Write-Step -Description "Checking administrator privileges" -Status 'OK' + + foreach ($cmd in @('Get-Disk','Get-Partition','Get-Volume','Resize-Partition','New-Partition','Format-Volume')) { + if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { + Stop-Safely -Reason "Required cmdlet '$cmd' is not available on this system." + } + } + foreach ($exe in @('reagentc.exe','diskpart.exe')) { + if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) { + Stop-Safely -Reason "Required executable '$exe' was not found in PATH." + } + } + + Write-Step -Description "Detecting system disk" -Status 'START' + $sysDiskInfo = Get-SystemDisk + $diskNumber = $sysDiskInfo.Disk.Number + $cPartition = $sysDiskInfo.CPartition + $cSizeBeforeGB = [math]::Round($cPartition.Size / 1GB, 2) + Write-Step -Description "Detecting system disk" -Status 'OK' + + $partitions = Get-DiskLayout -DiskNumber $diskNumber + + Write-Step -Description "Checking WinRE" -Status 'START' + $winREStatus = Get-WinREStatus + Write-Host "" + Write-Host "WinRE Status : $($winREStatus.Status)" + if ($winREStatus.Location) { + Write-Host "WinRE Location : Disk $($winREStatus.Location.DiskNumber) / Partition $($winREStatus.Location.PartitionNumber)" + } + Write-Step -Description "Checking WinRE" -Status 'OK' + + Write-Step -Description "Detecting Recovery partition" -Status 'START' + $recoveryPartition = Get-RecoveryPartition -Disk $sysDiskInfo.Disk -Partitions $partitions -CPartition $cPartition -WinREStatus $winREStatus + $hasExistingRecovery = [bool]$recoveryPartition + if ($hasExistingRecovery) { + Write-Step -Description "Detecting Recovery partition" -Status 'OK' + } else { + Write-Step -Description "Detecting Recovery partition" -Status 'SKIP' + Write-Host "No existing Recovery partition found - a new one will be created from scratch." -ForegroundColor Yellow + } + + Write-Step -Description "Checking BitLocker status" -Status 'START' + $bitlockerInfo = Get-BitLockerInfo -MountPoint 'C:' + if ($bitlockerInfo) { + Write-Host "" + Write-Host "BitLocker on C: : $($bitlockerInfo.VolumeStatus) / Protection $($bitlockerInfo.ProtectionStatus)" + if ($bitlockerInfo.ProtectionStatus -eq 'On') { + Write-Host "Protection will be suspended automatically around the WinRE/partition steps and resumed afterwards." -ForegroundColor Yellow + } + Write-Step -Description "Checking BitLocker status" -Status 'OK' + } else { + Write-Step -Description "Checking BitLocker status" -Status 'SKIP' + Write-Log -Message "BitLocker not present/not queryable on C: - nothing to suspend." -Level 'INFO' + } + + Write-Step -Description "Checking Recovery partition sizing" -Status 'START' + $winreWimSizeMB = Get-WinREWimSizeMB + $recommendedRecoveryMB = Get-RecommendedRecoverySizeMB + if ($winreWimSizeMB) { + Write-Host "" + Write-Host "Current Winre.wim size : ~$winreWimSizeMB MB" + Write-Host "Recommended minimum : $recommendedRecoveryMB MB (includes reagentc's working-space buffer + headroom)" + } + if ($RecoverySizeMB -lt $recommendedRecoveryMB) { + Write-Step -Description "Checking Recovery partition sizing" -Status 'FAIL' + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Yellow + Write-Host "RECOVERY PARTITION SIZE WARNING" -ForegroundColor Yellow + Write-Host ("=" * 60) -ForegroundColor Yellow + Write-Host @" +reagentc /enable does NOT error out when the Recovery partition +is too small - it silently falls back to storing WinRE inside +C: itself instead, even though the partition otherwise looks +perfectly valid (correct type, no drive letter, adjacent to C:). +This has been observed directly on this system. + +Requested Recovery partition size : $RecoverySizeMB MB +Recommended minimum for this system: $recommendedRecoveryMB MB +"@ + Write-Host ("=" * 60) -ForegroundColor Yellow + if (-not (Confirm-YesNo -Prompt "Continue anyway with a $RecoverySizeMB MB Recovery partition (real risk of the same silent fallback)?")) { + Stop-Safely -Reason "Aborted by user - re-run with -RecoverySizeMB $recommendedRecoveryMB (or more)." + } + } else { + Write-Step -Description "Checking Recovery partition sizing" -Status 'OK' + } + + # ------------------------------------------------------------------ + # PHASE 2 - VALIDATION + # ------------------------------------------------------------------ + + Write-Step -Description "Checking free space" -Status 'START' + + if ($hasExistingRecovery) { + if (-not (Test-DiskLayout -Partitions $partitions -CPartition $cPartition -RecoveryPartition $recoveryPartition)) { + Write-Step -Description "Checking free space" -Status 'FAIL' + Write-Log -Message "WARNING: Recovery partition is not directly behind C:. Automatic procedure cannot safely continue." -Level 'ERROR' + Stop-Safely -Reason "Recovery partition is not directly behind C:. Aborting for safety." + } + } + + $trailingFreeMB = Get-TrailingFreeSpaceMB -DiskNumber $diskNumber + $existingRecoveryMB = if ($hasExistingRecovery) { [math]::Round($recoveryPartition.Size / 1MB, 0) } else { 0 } + $availableMB = $trailingFreeMB + $existingRecoveryMB + + Write-Host "" + Write-Host "FREE SPACE CHECK" -ForegroundColor Cyan + Write-Host "Existing Recovery size : $existingRecoveryMB MB" + Write-Host "Trailing unallocated space: $trailingFreeMB MB" + Write-Host "Total available : $availableMB MB" + Write-Host "Required for new Recovery : $RecoverySizeMB MB" + + if ($availableMB -lt $RecoverySizeMB) { + Write-Step -Description "Checking free space" -Status 'FAIL' + Stop-Safely -Reason "Insufficient space: only $availableMB MB available (existing Recovery + trailing free space), need at least $RecoverySizeMB MB. Free up space manually (e.g. shrink C: further) and re-run." + } + Write-Step -Description "Checking free space" -Status 'OK' + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "CURRENT SYSTEM STATE" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "Computer : $env:COMPUTERNAME" + Write-Host "System Disk : $diskNumber" + Write-Host "Partition Style : $($sysDiskInfo.Disk.PartitionStyle)" + Write-Host "C: Size : $cSizeBeforeGB GB" + Write-Host "WinRE Status : $($winREStatus.Status)" + if ($hasExistingRecovery) { + Write-Host "Recovery Part# : $($recoveryPartition.PartitionNumber)" + Write-Host "Recovery Size : $existingRecoveryMB MB" + } else { + Write-Host "Recovery Part# : (none - will be created fresh)" + } + + if ($AuditOnly) { + Write-Host "" + Write-Host "AUDIT ONLY" -ForegroundColor Yellow + Write-Host "" + Write-Host "Planned actions (NOT executed):" + $n = 1 + if ($hasExistingRecovery) { + Write-Host " $n. Disable WinRE"; $n++ + Write-Host " $n. Delete old Recovery Partition $($recoveryPartition.PartitionNumber) on Disk $diskNumber"; $n++ + } + Write-Host " $n. Create new Recovery Partition ($RecoverySizeMB MB) at the true end of the disk"; $n++ + Write-Host " $n. Ask whether to expand C: into the remaining free space (interactive, optional)"; $n++ + Write-Host " $n. Configure Recovery Partition type ($($sysDiskInfo.Disk.PartitionStyle)) and re-enable WinRE" + Write-Host "" + Write-Host "No changes have been made." + Write-Log -Message "Audit-only run completed. No changes made." -Level 'SUCCESS' + Stop-Transcript -ErrorAction SilentlyContinue | Out-Null + exit 0 + } + + # ------------------------------------------------------------------ + # PHASE 3 - USER CONFIRMATION + # ------------------------------------------------------------------ + + Show-PreflightChecklist -DiskNumber $diskNumber -WinREStatusText $winREStatus.Status ` + -HasExistingRecovery $hasExistingRecovery -AvailableMB $availableMB -RecoverySizeMB $RecoverySizeMB ` + -BitLockerInfo $bitlockerInfo + + Write-Step -Description "User confirmation" -Status 'START' + + if ($hasExistingRecovery) { + if (-not (Confirm-RecoveryPartition -DiskNumber $diskNumber -RecoveryPartition $recoveryPartition -CPartition $cPartition)) { + Write-Step -Description "User confirmation" -Status 'FAIL' + Stop-Safely -Reason "User did not confirm the detected Recovery partition." + } + } + + if (-not (Confirm-Snapshot)) { + Write-Step -Description "User confirmation" -Status 'FAIL' + Stop-Safely -Reason "User did not confirm VM snapshot/checkpoint verification." + } + + if (-not (Confirm-Backup)) { + Write-Step -Description "User confirmation" -Status 'FAIL' + Stop-Safely -Reason "User did not confirm existence of a recent successful backup." + } + + Write-Step -Description "User confirmation" -Status 'OK' + + # ------------------------------------------------------------------ + # PHASE 4 - DESTRUCTIVE OPERATION (only if an old Recovery exists) + # ------------------------------------------------------------------ + + if ($bitlockerInfo -and $bitlockerInfo.ProtectionStatus -eq 'On') { + Write-Step -Description "Suspending BitLocker" -Status 'START' + if (Suspend-BitLockerProtection -MountPoint 'C:') { + $Script:BitLockerSuspended = $true + Write-Step -Description "Suspending BitLocker" -Status 'OK' + } else { + Write-Step -Description "Suspending BitLocker" -Status 'FAIL' + Stop-Safely -Reason "Could not suspend BitLocker protection on C:. Aborting before touching WinRE/partitions to avoid a BitLocker recovery prompt on next boot." + } + } + + Write-Step -Description "Disabling WinRE" -Status 'START' + if ($winREStatus.Status -match 'Enabled') { + if (-not (Disable-WinRE)) { + Write-Step -Description "Disabling WinRE" -Status 'FAIL' + Stop-Safely -Reason "WinRE could not be disabled. Cannot safely proceed." + } + Write-Step -Description "Disabling WinRE" -Status 'OK' + } else { + Write-Step -Description "Disabling WinRE" -Status 'SKIP' + Write-Log -Message "WinRE already disabled - nothing to do." -Level 'INFO' + } + + Write-Step -Description "Removing old Recovery partition" -Status 'START' + if ($hasExistingRecovery) { + $finalCheckPartitions = Get-Partition -DiskNumber $diskNumber + $finalRecovery = $finalCheckPartitions | Where-Object { $_.PartitionNumber -eq $recoveryPartition.PartitionNumber } + $finalC = $finalCheckPartitions | Where-Object { $_.DriveLetter -eq 'C' } + + if (-not $finalRecovery -or -not $finalC) { + Write-Step -Description "Removing old Recovery partition" -Status 'FAIL' + Stop-Safely -Reason "Partition table changed unexpectedly before destructive step. Aborting." + } + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "FINAL DESTRUCTIVE CHECK" -ForegroundColor Red + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "System Disk : Disk $diskNumber ($($sysDiskInfo.Disk.FriendlyName))" + Write-Host "C: : Disk $diskNumber / Partition $($finalC.PartitionNumber)" + Write-Host "Recovery to DELETE: Disk $diskNumber / Partition $($finalRecovery.PartitionNumber)" + Write-Host "Size : $([math]::Round($finalRecovery.Size/1MB,0)) MB" + Write-Host "" + Write-Host "!!! DESTRUCTIVE OPERATION !!!" -ForegroundColor Red + Write-Host "This operation is irreversible without backup. The script will NOT delete C:." + Write-Host "" + $typed = Read-Host "Type exactly DELETE to continue" + if ($typed -cne 'DELETE') { + Write-Step -Description "Removing old Recovery partition" -Status 'FAIL' + Stop-Safely -Reason "User did not type DELETE exactly. Destructive operation aborted." + } + + if (-not (Remove-RecoveryPartition -DiskNumber $diskNumber -PartitionNumber $finalRecovery.PartitionNumber)) { + Write-Step -Description "Removing old Recovery partition" -Status 'FAIL' + Stop-Safely -Reason "Recovery partition removal failed or could not be verified." + } + Write-Step -Description "Removing old Recovery partition" -Status 'OK' + } else { + Write-Step -Description "Removing old Recovery partition" -Status 'SKIP' + } + + # ------------------------------------------------------------------ + # PHASE 5 - REBUILD (Recovery at true end FIRST, then optional C: + # expansion, THEN enable WinRE) + # ------------------------------------------------------------------ + + $trailingFreeMB = Get-TrailingFreeSpaceMB -DiskNumber $diskNumber + if ($trailingFreeMB -lt ($RecoverySizeMB + $DiskEndBufferMB)) { + Stop-Safely -Reason "After removing the old Recovery partition, only $trailingFreeMB MB is free at the end of the disk - not enough for a $RecoverySizeMB MB Recovery partition plus the $DiskEndBufferMB MB safety buffer." + } + + # Informational only (v2.1): with Recovery now placed at the true end + # of the disk via explicit offset, a partition sitting directly after + # C: no longer blocks Recovery creation - it just means there will be + # no gap for C: to expand into later. We warn instead of aborting. + $cPartitionNow = Get-Partition -DriveLetter C + $cEndOffsetNow = $cPartitionNow.Offset + $cPartitionNow.Size + $alignmentSlackNow = 1MB + $adjacentPartition = Get-Partition -DiskNumber $diskNumber | Where-Object { + $_.PartitionNumber -ne $cPartitionNow.PartitionNumber -and + $_.Offset -ge $cEndOffsetNow -and + ($_.Offset - $cEndOffsetNow) -lt $alignmentSlackNow + } + if ($adjacentPartition) { + Write-Log -Message "NOTE: Partition $($adjacentPartition.PartitionNumber) ($([math]::Round($adjacentPartition.Size/1MB,0)) MB) already sits directly after C:. This will limit or eliminate the space available to later expand C: into." -Level 'WARNING' + } + + Write-Step -Description "Creating Recovery partition" -Status 'START' + $newRecovery = New-RecoveryPartition -DiskNumber $diskNumber -RecoverySizeMB $RecoverySizeMB -PartitionStyle $sysDiskInfo.Disk.PartitionStyle -EndBufferMB $DiskEndBufferMB + if (-not $newRecovery) { + Write-Step -Description "Creating Recovery partition" -Status 'FAIL' + Stop-Safely -Reason "Failed to create the new Recovery partition. WinRE remains disabled - manual intervention required." + } + Write-Step -Description "Creating Recovery partition" -Status 'OK' + + # --- Interactive: expand C: into whatever remains, BEFORE enabling WinRE --- + $cNow = Get-Partition -DriveLetter C + $recoveryNow = Get-Partition -DiskNumber $diskNumber -PartitionNumber $newRecovery.PartitionNumber + $gapBytes = $recoveryNow.Offset - ($cNow.Offset + $cNow.Size) + $gapMB = [math]::Max(0, [math]::Round($gapBytes / 1MB, 0)) + + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "EXPAND C: ?" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "Free space currently sitting between C: and the new Recovery partition: ~$gapMB MB" + + $cExpanded = $false + if ($gapMB -lt 1) { + Write-Host "No meaningful free space to expand into - skipping." -ForegroundColor Yellow + } + elseif (Confirm-YesNo -Prompt "Do you want to expand C: to fill this space now?") { + Write-Step -Description "Expanding C:" -Status 'START' + if (Resize-SystemPartition -DiskNumber $diskNumber) { + $cExpanded = $true + Write-Step -Description "Expanding C:" -Status 'OK' + } else { + Write-Step -Description "Expanding C:" -Status 'FAIL' + Write-Log -Message "C: expansion failed - Recovery partition is unaffected. You can retry expanding C: manually via Disk Management. Proceeding to enable WinRE with the gap still in place." -Level 'WARNING' + } + } + else { + Write-Log -Message "User chose not to expand C: now. Remaining free space: ~$gapMB MB. If reagentc requires Recovery to be strictly adjacent to C:, leaving this gap in place could cause WinRE enable to fail or silently fall back to storing WinRE on C: - watch the next step's result." -Level 'WARNING' + } + + # Step: Configure / enable WinRE (runs AFTER the expand-C: decision, + # so that if the user chose to expand, C: and Recovery are adjacent + # by the time reagentc runs). + Write-Step -Description "Configuring WinRE" -Status 'START' + $winREEnabled = Enable-WinRE + $bitlockerDecryptedByUs = $false + $bitlockerProtectorTypesBeforeDecrypt = @() + + if (-not $winREEnabled) { + $currentBitlockerInfo = Get-BitLockerInfo -MountPoint 'C:' + if ($currentBitlockerInfo -and $currentBitlockerInfo.VolumeStatus -ne 'FullyDecrypted') { + Write-Log -Message "reagentc /enable failed while BitLocker encryption is still present on C: - offering the full-decrypt workaround." -Level 'WARNING' + $workaround = Invoke-BitLockerDecryptWorkaround -MountPoint 'C:' + if ($workaround.Success) { + $bitlockerDecryptedByUs = $true + $bitlockerProtectorTypesBeforeDecrypt = $workaround.ProtectorTypes + $winREEnabled = Enable-WinRE + } + } + } + + if (-not $winREEnabled) { + Write-Step -Description "Configuring WinRE" -Status 'FAIL' + if ($bitlockerDecryptedByUs) { + Write-Log -Message "WinRE still could not be enabled even after fully decrypting C: - re-encrypting before aborting." -Level 'ERROR' + Invoke-BitLockerReencrypt -MountPoint 'C:' -OriginalProtectorTypes $bitlockerProtectorTypesBeforeDecrypt | Out-Null + } + Stop-Safely -Reason "Failed to enable WinRE after creating the new Recovery partition. Manual intervention required." + } + Write-Step -Description "Configuring WinRE" -Status 'OK' + + if ($bitlockerDecryptedByUs) { + Invoke-BitLockerReencrypt -MountPoint 'C:' -OriginalProtectorTypes $bitlockerProtectorTypesBeforeDecrypt | Out-Null + } + + if ($Script:BitLockerSuspended) { + Write-Step -Description "Resuming BitLocker" -Status 'START' + Resume-BitLockerProtection -MountPoint 'C:' + $Script:BitLockerSuspended = $false + Write-Step -Description "Resuming BitLocker" -Status 'OK' + } + + # ------------------------------------------------------------------ + # PHASE 6 - FINAL VALIDATION + # ------------------------------------------------------------------ + + Write-Step -Description "Final validation" -Status 'START' + $results = Test-FinalState -DiskNumber $diskNumber -RecoveryPartition $newRecovery + $cSizeAfterGB = [math]::Round((Get-Partition -DriveLetter C).Size / 1GB, 2) + $cFinal = Get-Partition -DriveLetter C + $recoveryFinal = Get-Partition -DiskNumber $diskNumber -PartitionNumber $newRecovery.PartitionNumber + $remainingGapMB = [math]::Max(0, [math]::Round(($recoveryFinal.Offset - ($cFinal.Offset + $cFinal.Size)) / 1MB, 0)) + + $stepStatus = if ($results.Values -notcontains $false) { 'OK' } else { 'FAIL' } + Write-Step -Description "Final validation" -Status $stepStatus + + if ($results['WinRE enabled'] -and -not $results['WinRE location matches new Recovery']) { + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "WINRE IS ENABLED BUT NOT USING THE RECOVERY PARTITION" -ForegroundColor Red + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host @" +reagentc reported success, but WinRE is actually stored inside +C: itself rather than on the dedicated Recovery partition. This +can happen when reagentc's own partition-size check silently +rejects the Recovery partition and falls back to C: without +raising an error, OR (v2.1) if adjacency to C: turns out to be +required and a gap was left between C: and Recovery. + +Current Winre.wim size : $(if ($winreWimSizeMB) { "~$winreWimSizeMB MB" } else { "unknown" }) +Recovery partition size: $RecoverySizeMB MB +Recommended minimum : $recommendedRecoveryMB MB +Gap between C: and Recovery at final check: ~$remainingGapMB MB + +If the partition size looks adequate above, check the ReAgent.log +excerpt below (also written to the script log) for the real reason. +"@ + $reagentLogNow = Get-ReAgentLogTail -Lines 40 + if ($reagentLogNow) { + Write-Log -Message "Tail of $env:WINDIR\Logs\ReAgent\ReAgent.log at final validation (WinRE enabled but not using the Recovery partition):`n$($reagentLogNow -join "`n")" -Level 'ERROR' + Write-Host ($reagentLogNow -join "`n") + } + Write-Host ("=" * 60) -ForegroundColor Red + Write-Host "If a gap remains, re-run and choose to expand C: this time so C: and Recovery are adjacent before WinRE is enabled." -ForegroundColor Yellow + } + + # ------------------------------------------------------------------ + # PHASE 7 - FINAL REPORT + # ------------------------------------------------------------------ + Show-FinalSummary -Results $results -CSizeBeforeGB $cSizeBeforeGB -CSizeAfterGB $cSizeAfterGB -RecoverySizeMB $RecoverySizeMB -CExpanded $cExpanded -RemainingFreeMB $remainingGapMB + + Write-Log -Message "=== WinRE-Resize2.ps1 finished ===" -Level 'INFO' + Write-Host "" + Write-Host "Log file: $Script:LogFile" + + try { Stop-Transcript -ErrorAction SilentlyContinue | Out-Null } catch {} + + if ($results.Values -contains $false) { exit 1 } else { exit 0 } +} +catch { + if ($Script:BitLockerSuspended) { + Write-Log -Message "Unhandled exception while BitLocker was suspended - attempting to resume protection." -Level 'WARNING' + Resume-BitLockerProtection -MountPoint 'C:' + $Script:BitLockerSuspended = $false + } + + Write-Log -Message "UNHANDLED EXCEPTION: $_" -Level 'ERROR' + Write-Log -Message "Stack trace: $($_.ScriptStackTrace)" -Level 'ERROR' + Write-Host "" + Write-Host "UNSAFE / AMBIGUOUS STATE" -ForegroundColor Red + Write-Host "An unexpected error occurred. No further changes will be made." -ForegroundColor Red + Write-Host "Error: $_" -ForegroundColor Red + if ($Script:LogFile) { Write-Host "Log: $Script:LogFile" -ForegroundColor Red } + try { Stop-Transcript -ErrorAction SilentlyContinue | Out-Null } catch {} + exit 1 +} \ No newline at end of file