<# .SYNOPSIS Creates a restore point, repairs the Windows component store with DISM, then runs SFC, and sums it all up in one object. .DESCRIPTION The order matters. SFC repairs system files from the component store (WinSxS), so if the store itself is damaged, SFC "fixes" files from broken copies or gives up. This script runs them the right way round: 1. A System Restore point, so there's a way back (skip with -SkipRestorePoint). 2. DISM CheckHealth (quick, read-only) and then RestoreHealth, through Repair-WindowsImage. 3. sfc /scannow, with its output captured, cleaned up and turned into a plain result. 4. A check for a pending restart. -ScanOnly swaps the repairs for DISM ScanHealth and sfc /verifyonly. -WhatIf runs only the read-only CheckHealth and the pending-restart check. .PARAMETER ScanOnly Look but don't fix: DISM ScanHealth and sfc /verifyonly. .PARAMETER Source A repair source for DISM when Windows Update can't supply one, such as a mounted install.wim (wim:E:\sources\install.wim:1) or a folder from the same Windows build. .PARAMETER LimitAccess With -Source, stop DISM from falling back to Windows Update or WSUS. .PARAMETER SkipRestorePoint Don't create a restore point first. .PARAMETER EnableSystemRestore If System Restore is off on the system drive, turn it on so the restore point can be made. .PARAMETER LogFolder Where the SFC output is saved. Default: %ProgramData%\WindowsHealth. .EXAMPLE .\Repair-WindowsHealth.ps1 .EXAMPLE .\Repair-WindowsHealth.ps1 -Source wim:E:\sources\install.wim:1 -LimitAccess #> [CmdletBinding(SupportsShouldProcess)] param( [switch]$ScanOnly, [string]$Source, [switch]$LimitAccess, [switch]$SkipRestorePoint, [switch]$EnableSystemRestore, [string]$LogFolder = (Join-Path $env:ProgramData 'WindowsHealth') ) $identity = [Security.Principal.WindowsIdentity]::GetCurrent() if (-not ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'DISM and SFC need an elevated PowerShell. Right-click, Run as administrator, and try again.' } $LogFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LogFolder) $started = Get-Date $summary = [ordered]@{ ComputerName = $env:COMPUTERNAME RestorePoint = 'Skipped' StoreBefore = $null Dism = 'NotRun' Sfc = 'NotRun' SfcDetail = $null RestartPending = $false Duration = $null SfcLog = $null CbsLog = Join-Path $env:SystemRoot 'Logs\CBS\CBS.log' DismLog = Join-Path $env:SystemRoot 'Logs\DISM\dism.log' } # 1. Restore point. Checkpoint-Computer only exists in Windows PowerShell 5.1, so go straight to the WMI class. if (-not $SkipRestorePoint -and -not $ScanOnly -and $PSCmdlet.ShouldProcess($env:SystemDrive, 'Create a System Restore point')) { try { $drive = "$env:SystemDrive\" if ($EnableSystemRestore) { $on = Invoke-CimMethod -Namespace root/default -ClassName SystemRestore -MethodName Enable -Arguments @{ Drive = $drive } -ErrorAction Stop if ($on.ReturnValue -ne 0) { Write-Warning "Enabling System Restore returned $($on.ReturnValue)." } } $before = @(Get-CimInstance -Namespace root/default -ClassName SystemRestore -ErrorAction SilentlyContinue).Count $rp = Invoke-CimMethod -Namespace root/default -ClassName SystemRestore -MethodName CreateRestorePoint -ErrorAction Stop -Arguments @{ Description = "Before DISM and SFC repair $(Get-Date -Format 'yyyy-MM-dd HH:mm')" RestorePointType = [uint32]12 # MODIFY_SETTINGS EventType = [uint32]100 # BEGIN_SYSTEM_CHANGE } $after = @(Get-CimInstance -Namespace root/default -ClassName SystemRestore -ErrorAction SilentlyContinue).Count $summary.RestorePoint = switch ($true) { ($rp.ReturnValue -ne 0) { "Failed: error $($rp.ReturnValue). Is System Restore on? Try -EnableSystemRestore"; break } ($after -gt $before) { 'Created'; break } default { 'NotCreated: Windows only allows one every 24 hours by default' } } } catch { $summary.RestorePoint = "Failed: $($_.Exception.Message)" } if ($summary.RestorePoint -notin 'Created') { Write-Warning "Restore point: $($summary.RestorePoint). Carrying on." } } # 2. DISM. CheckHealth is read-only and quick, so it runs even under -WhatIf. try { $summary.StoreBefore = [string](Repair-WindowsImage -Online -CheckHealth -ErrorAction Stop).ImageHealthState } catch { $summary.StoreBefore = "Error: $($_.Exception.Message)" } $dismParams = @{ Online = $true; ErrorAction = 'Stop' } if ($ScanOnly) { $dismParams.ScanHealth = $true; $dismAction = 'DISM ScanHealth (read-only, 5-15 minutes)' } else { $dismParams.RestoreHealth = $true; $dismParams.NoRestart = $true; $dismAction = 'DISM RestoreHealth (10-30 minutes)' if ($Source) { $dismParams.Source = $Source } if ($LimitAccess) { $dismParams.LimitAccess = $true } } if ($PSCmdlet.ShouldProcess('Windows component store', $dismAction)) { try { Write-Verbose "Running $dismAction." $dism = Repair-WindowsImage @dismParams $summary.Dism = [string]$dism.ImageHealthState # Healthy, Repairable or NonRepairable if ($dism.RestartNeeded) { $summary.RestartPending = $true } } catch { $msg = $_.Exception.Message $summary.Dism = if ($msg -match '0x800f081f|source files could not be found') { 'SourceNotFound: give it -Source with a matching install.wim' } else { "Failed: $msg" } } } # 3. SFC. Its output is UTF-16, which PowerShell reads as text with a NUL between every letter; strip those before matching. $sfcArg = if ($ScanOnly) { '/verifyonly' } else { '/scannow' } if ($PSCmdlet.ShouldProcess('Protected system files', "sfc $sfcArg (10-20 minutes)")) { try { if ($summary.Dism -like 'Failed*' -or $summary.Dism -like 'SourceNotFound*' -or $summary.Dism -eq 'NonRepairable') { Write-Warning 'DISM did not finish cleanly, so SFC may not be able to repair everything.' } $null = New-Item -ItemType Directory -Path $LogFolder -Force -ErrorAction Stop $raw = & "$env:SystemRoot\System32\sfc.exe" $sfcArg 2>&1 $text = (($raw | Out-String) -replace "`0", '') -replace '\r?\n\s*\r?\n', "`n" $summary.SfcLog = Join-Path $LogFolder ('sfc-{0:yyyyMMdd-HHmmss}.txt' -f (Get-Date)) Set-Content -LiteralPath $summary.SfcLog -Value $text -Encoding utf8 $summary.Sfc = switch -Regex ($text) { 'did not find any integrity violations' { 'Clean'; break } 'found corrupt files and successfully repaired' { 'Repaired'; break } 'found corrupt files but was unable to fix' { 'NotAllRepaired'; break } 'found integrity violations' { 'ViolationsFound'; break } 'repair pending which requires reboot' { $summary.RestartPending = $true; 'RestartFirst'; break } 'could not perform the requested operation' { 'CouldNotRun'; break } default { "Unrecognized (exit code $LASTEXITCODE)" } } $summary.SfcDetail = ($text -split "`n" | Where-Object { $_ -match 'Windows Resource Protection|There is a system repair' } | ForEach-Object Trim) -join ' ' } catch { $summary.Sfc = "Failed: $($_.Exception.Message)" } } # 4. Anything still waiting on a restart? $pendingKeys = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' ) if ($pendingKeys | Where-Object { Test-Path -LiteralPath $_ }) { $summary.RestartPending = $true } if ((Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name PendingFileRenameOperations -ErrorAction SilentlyContinue).PendingFileRenameOperations) { $summary.RestartPending = $true } $summary.Duration = (Get-Date) - $started [pscustomobject]$summary