SharePoint / Scripts / SharePoint Server

Get-SPPatchStatus

Verifies that every server in the farm is on the same SharePoint build after patching.

Runs in
SharePoint Management Shell on a farm server
Safety
Read-only

Download Get-SPPatchStatus.ps1

What it does

For every SharePoint server in the farm, reports the file version of the core SharePoint DLL (Microsoft.SharePoint.dll), whether the server still needs upgrade, and the state of the SharePoint Timer and IIS services. Lists every database that still needs upgrading and gives an overall verdict.

Read-only. Run from the SharePoint Management Shell (Windows PowerShell 5.1) as a farm administrator who is also a local administrator on the servers, because the DLL is read over the admin share (\\server\C$) and services are queried remotely.

Tip: run it before patching and keep the output, then run it again afterwards.

Parameters

-OutputFilestringCSV file for the per-server results.Default: Timestamped file in the current folder

Examples

.\Get-SPPatchStatus.ps1

Notes

The farm build number is set by the highest update installed and configured. The DLL version normally matches it after a full update, but an individual update does not always change that DLL, so a small difference is flagged for checking rather than as a failure. Central Administration > Upgrade and Migration > Check product and patch installation status gives the authoritative per-server patch list.

Full script

<#
.SYNOPSIS
    Verifies that every server in the farm is on the same SharePoint build after patching.

.DESCRIPTION
    For every SharePoint server in the farm, reports the file version of the core
    SharePoint DLL (Microsoft.SharePoint.dll), whether the server still needs upgrade,
    and the state of the SharePoint Timer and IIS services. Lists every database that
    still needs upgrading and gives an overall verdict.

    Read-only. Run from the SharePoint Management Shell (Windows PowerShell 5.1) as a
    farm administrator who is also a local administrator on the servers, because the
    DLL is read over the admin share (\\server\C$) and services are queried remotely.

    Tip: run it before patching and keep the output, then run it again afterwards.

.PARAMETER OutputFile
    CSV file for the per-server results.

.EXAMPLE
    .\Get-SPPatchStatus.ps1

.NOTES
    The farm build number is set by the highest update installed and configured. The
    DLL version normally matches it after a full update, but an individual update does
    not always change that DLL, so a small difference is flagged for checking rather
    than as a failure. Central Administration > Upgrade and Migration > Check product
    and patch installation status gives the authoritative per-server patch list.
#>
[CmdletBinding()]
param(
    [string]$OutputFile = (Join-Path -Path (Get-Location) -ChildPath ('PatchStatus_{0:yyyyMMdd_HHmm}.csv' -f (Get-Date)))
)

if (-not (Get-Command Get-SPFarm -ErrorAction SilentlyContinue)) {
    Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
}
if (-not (Get-Command Get-SPFarm -ErrorAction SilentlyContinue)) {
    throw 'SharePoint cmdlets are not available. Run this on a farm server from the SharePoint Management Shell.'
}

$farm      = Get-SPFarm
$farmBuild = $farm.BuildVersion
$hive      = $farmBuild.Major          # 14 = 2010, 15 = 2013, 16 = 2016/2019/SE
$dllPath   = "Program Files\Common Files\microsoft shared\Web Server Extensions\$hive\ISAPI\Microsoft.SharePoint.dll"

# SharePoint servers are the ones running the Timer service (this excludes SQL and mail servers)
$servers = Get-SPServer | Where-Object {
    $_.ServiceInstances | Where-Object { $_.GetType().Name -eq 'SPTimerServiceInstance' }
}

$rows = foreach ($server in $servers) {
    $name = $server.Address
    $isLocal = $name -ieq $env:COMPUTERNAME
    $file = if ($isLocal) { "C:\$dllPath" } else { "\\$name\C$\$dllPath" }

    $dllVersion = $null
    try { $dllVersion = [version](Get-Item -Path $file -ErrorAction Stop).VersionInfo.FileVersion }
    catch { $dllVersion = $null }

    $svcState = @{}
    foreach ($svc in 'SPTimerV4', 'W3SVC') {
        try {
            $s = if ($isLocal) { Get-Service -Name $svc -ErrorAction Stop } else { Get-Service -Name $svc -ComputerName $name -ErrorAction Stop }
            $svcState[$svc] = [string]$s.Status
        }
        catch { $svcState[$svc] = 'Unknown' }
    }

    $buildCheck = if (-not $dllVersion) { 'Could not read DLL' }
                  elseif ($dllVersion -eq $farmBuild) { 'Match' }
                  elseif ($dllVersion.Build -eq $farmBuild.Build) { 'Match (build)' }
                  else { 'Check' }

    [pscustomobject]@{
        Server       = $name
        Role         = $server.Role
        DllVersion   = if ($dllVersion) { $dllVersion.ToString() } else { '' }
        FarmBuild    = $farmBuild.ToString()
        BuildCheck   = $buildCheck
        NeedsUpgrade = $server.NeedsUpgrade
        TimerService = $svcState['SPTimerV4']
        IIS          = $svcState['W3SVC']
    }
}

$dbsBehind = @(Get-SPDatabase | Where-Object { $_.NeedsUpgrade } |
    Select-Object Name, @{n='Type';e={$_.TypeName}}, @{n='Server';e={$_.NormalizedDataSource}})

Write-Host "Farm build: $farmBuild   Farm needs upgrade: $($farm.NeedsUpgrade)" -ForegroundColor Cyan
$rows | Format-Table -AutoSize | Out-Host

if ($dbsBehind.Count) {
    Write-Host 'Databases that still need upgrade:' -ForegroundColor Yellow
    $dbsBehind | Format-Table -AutoSize | Out-Host
}

$rows | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8

$problems = @($rows | Where-Object { $_.NeedsUpgrade -or $_.BuildCheck -notlike 'Match*' -or $_.TimerService -ne 'Running' })
if (-not $farm.NeedsUpgrade -and $problems.Count -eq 0 -and $dbsBehind.Count -eq 0) {
    Write-Host 'Verdict: all servers and databases are on the farm build. Patching is complete.' -ForegroundColor Green
}
else {
    Write-Host "Verdict: needs attention ($($problems.Count) server(s), $($dbsBehind.Count) database(s)). Run Invoke-SPPostPatchConfig.ps1 on servers still needing upgrade." -ForegroundColor Yellow
}
Write-Host "Report: $OutputFile"