From c0dd0f39e3a864cf18100c04359bdff3256cf266 Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Mon, 29 Jun 2026 13:57:08 +0200 Subject: [PATCH] fix(dc): SE - query Get-SPCacheHostConfig locally on each cache host On SE, Get-SPCacheHostConfig only returns the real Size (in MB) and ports when executed locally on the cache host after Use-SPCacheCluster - calling it remotely from APP1 returns null. Refactor Get-AppFabricStatus in two passes: (1) from the farm entry server, detect SE and list the DC service instances + full server list; (2) open a CredSSP session per cache host and run Use-SPCacheCluster + Get-SPCacheHostConfig -HostName $env:COMPUTERNAME locally. Fallback to the SE cluster tier suffixed ' (tier)' when the local lookup still fails. The 2016/2019 (AppFabric) branch is unchanged. Fixes #51, #53 --- CHANGELOG.md | 24 ++ RELEASE-NOTES.md | 16 +- .../Public/Get-AppFabricStatus.ps1 | 250 ++++++++++-------- .../SPSWeather.Common/SPSWeather.Common.psd1 | 2 +- tests/SPSWeather.Common.Tests.ps1 | 2 +- 5 files changed, 178 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c02d430..18c61a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.3.6] - 2026-06-29 + +### Fixed + +- `Get-AppFabricStatus` (SE branch) now reports the real cache Size in MB + (e.g. '2048 MB') and the actual CachePort. `Get-SPCacheHostConfig` only + returns its full payload when executed locally on the cache host after + `Use-SPCacheCluster`; the function now does exactly that, opening a + CredSSP session per cache host and calling `Get-SPCacheHostConfig + -HostName $env:COMPUTERNAME` from inside. When the local lookup still + fails, the row falls back to the SE cluster tier (Small/Medium/Large) + suffixed ' (tier)'. The 2016/2019 (AppFabric) branch is unchanged + (#51, #53). + +## [2.3.6] - 2026-06-29 + +### Fixed + +- Distributed Cache: the email body header for the cache size column is now + 'Size' (it was 'Size (MB)' but the value was the SE sizing tier - Small / + Medium / Large - when Get-SPCacheHostConfig is null). The fallback value + is now displayed as 'Small (tier)' / 'Medium (tier)' / ... to make clear + it is not a raw MB number (#53). + ## [2.3.5] - 2026-06-29 ### Fixed diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 9305a10..32fcebd 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,14 +1,16 @@ # SPSWeather - Release Notes -## [2.3.5] - 2026-06-29 +## [2.3.6] - 2026-06-29 ### Fixed -- Distributed Cache report on SharePoint Subscription Edition no longer shows - a phantom 'Microsoft' row (a 2.3.4 regression caused by iterating cluster - health HostInfo objects whose ToString starts with - 'Microsoft.SharePoint.Internal.Caching...'). The function iterates the - SPDistributedCacheServiceInstance set again; cluster Size still comes from - Get-SPCacheClusterInfo. +- Distributed Cache report on SharePoint Subscription Edition now shows the + real cache Size in MB (e.g. '2048 MB') and the actual CachePort. The + function opens a CredSSP session per cache host and runs Use-SPCacheCluster + + Get-SPCacheHostConfig -HostName $env:COMPUTERNAME locally - the SE cmdlet + only returns the full payload when executed on the cache host itself. + When the local lookup still fails, the row falls back to the SE cluster + tier (Small/Medium/Large) suffixed ' (tier)'. +- 2016/2019 (AppFabric) branch is unchanged. A full list of changes can be found in the [change log](CHANGELOG.md). diff --git a/src/Modules/SPSWeather.Common/Public/Get-AppFabricStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-AppFabricStatus.ps1 index 7a7b4cf..4771b42 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-AppFabricStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-AppFabricStatus.ps1 @@ -15,11 +15,120 @@ [System.String] $Farm = 'SPS' ) - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { + # Pass 1: from the farm entry server, detect SharePoint version and the + # Distributed Cache topology (which servers host the cache + the full server + # list). On SE we do NOT call Get-SPCacheHostConfig here: the cmdlet only + # returns a useful payload (Size in MB, ports, ...) when executed locally on + # the cache host after Use-SPCacheCluster. The 2016/2019 (AppFabric) branch + # keeps its original logic, unchanged. + $inventory = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $Farm ` + -Server $Server ` + -ScriptBlock { + $localFarm = $args[0] + $pathToSearch = 'C:\Program Files\Common Files\microsoft shared\Web Server Extensions\*\ISAPI\Microsoft.SharePoint.dll' + $fullPath = Get-Item $pathToSearch -ErrorAction SilentlyContinue | Sort-Object { $_.Directory } -Descending | Select-Object -First 1 + if ($null -eq $fullPath) { + throw 'SharePoint path {C:\Program Files\Common Files\microsoft shared\Web Server Extensions} does not exist' + } + $productVersion = (Get-Command $fullPath).FileVersionInfo + $isSE = ($productVersion.FileMajorPart -eq 16 -and $productVersion.FileBuildPart -gt 13000) + + if ($isSE) { + $allSPServers = (Get-SPServer | Where-Object -FilterScript { $_.Role -ne 'Invalid' }).Name + $dcInstances = Get-SPServiceInstance | Where-Object -FilterScript { + $_.GetType().Name -eq 'SPDistributedCacheServiceInstance' + } + $clusterInfo = Get-SPCacheClusterInfo -ErrorAction SilentlyContinue + $hosts = @() + foreach ($dc in $dcInstances) { + $hosts += [PSCustomObject]@{ + Farm = $localFarm + Server = "$($dc.Server.Address)".Split('.')[0] + Status = "$($dc.Status)" + } + } + return [PSCustomObject]@{ + IsSE = $true + AllServers = @($allSPServers) + DcHosts = $hosts + ClusterSize = if ($null -ne $clusterInfo) { "$($clusterInfo.Size)" } else { '' } + } + } + else { + return [PSCustomObject]@{ IsSE = $false } + } + } + + $tbAppFabricStatus = New-Object -TypeName System.Collections.ArrayList + + if ($inventory.IsSE) { + # Pass 2 (SE): query Get-SPCacheHostConfig LOCALLY on each cache host. + # That is where the cmdlet returns the real Size (in MB), ports, etc. + foreach ($dcHost in $inventory.DcHosts) { + $hostConfig = $null + try { + $hostConfig = Invoke-SPSCommand -Credential $InstallAccount ` + -Server $dcHost.Server ` + -ScriptBlock { + Use-SPCacheCluster -ErrorAction SilentlyContinue + $cfg = Get-SPCacheHostConfig -HostName $env:COMPUTERNAME -ErrorAction SilentlyContinue + if ($null -eq $cfg) { return $null } + return [PSCustomObject]@{ + HostName = "$($cfg.HostName)" + CachePort = "$($cfg.CachePort)" + Size = "$($cfg.Size)" + ServiceName = "$($cfg.ServiceName)" + } + } + } + catch { + Write-Verbose -Message "Get-SPCacheHostConfig failed on '$($dcHost.Server)': $($_.Exception.Message)" + } + + $port = if ($null -ne $hostConfig) { $hostConfig.CachePort } else { '22233' } + $size = if ($null -ne $hostConfig) { $hostConfig.Size } + elseif ($inventory.ClusterSize) { "$($inventory.ClusterSize) (tier)" } + else { '' } + $serviceName = if ($null -ne $hostConfig) { $hostConfig.ServiceName } else { 'AppFabricCachingService' } + $cacheStatus = if ($dcHost.Status -eq 'Online') { 'Up' } else { 'Unknown' } + $isMailInfo = ($dcHost.Status -eq 'Online' -and $cacheStatus -eq 'Up') + + [void]$tbAppFabricStatus.Add([PSCustomObject]@{ + Farm = $inventory.DcHosts[0].Farm + Server = $dcHost.Server + Port = $port + ServiceName = $serviceName + Size = $size + CacheStatus = $cacheStatus + SPInstanceStatus = $dcHost.Status + IsInfo = $isMailInfo + }) + } + $reportedServers = @($inventory.DcHosts | ForEach-Object { $_.Server }) + foreach ($srv in $inventory.AllServers) { + if ($reportedServers -notcontains $srv) { + [void]$tbAppFabricStatus.Add([PSCustomObject]@{ + Farm = $Farm + Server = $srv + Port = '' + ServiceName = '' + Size = '' + CacheStatus = '' + SPInstanceStatus = 'Not a cache host' + IsInfo = $true + }) + } + } + return $tbAppFabricStatus + } + + # SharePoint 2016 / 2019 branch: original AppFabric-based logic, unchanged. + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { $params = $args[0] class AppFabricStatus { [System.String]$Farm @@ -32,117 +141,44 @@ [System.Boolean]$IsInfo } $tbAppFabricStatus = New-Object -TypeName System.Collections.ArrayList - $pathToSearch = 'C:\Program Files\Common Files\microsoft shared\Web Server Extensions\*\ISAPI\Microsoft.SharePoint.dll' - $fullPath = Get-Item $pathToSearch -ErrorAction SilentlyContinue | Sort-Object { $_.Directory } -Descending | Select-Object -First 1 - if ($null -eq $fullPath) { - $message = 'SharePoint path {C:\Program Files\Common Files\microsoft shared\Web Server Extensions} does not exist' - throw $message - } - else { - $productVersion = (Get-Command $fullPath).FileVersionInfo - } - if ($productVersion.FileMajorPart -eq 16 -and $productVersion.FileBuildPart -gt 13000) { - Write-Verbose -Message 'Subscription Edition: iterating SPDistributedCacheServiceInstance + Get-SPCacheClusterInfo for Size' - $allSPServers = (Get-SPServer | Where-Object -FilterScript { $_.Role -ne 'Invalid' }).Name - $dcInstances = @(Get-SPServiceInstance | Where-Object -FilterScript { + Use-CacheCluster -ErrorAction SilentlyContinue + $cacheHosts = Get-CacheHost -ErrorAction SilentlyContinue + if ($null -ne $cacheHosts) { + foreach ($cacheHost in $cacheHosts) { + $isMailInfo = $true + $cacheHostConfig = Get-AFCacheHostConfiguration -ComputerName $cacheHost.HostName ` + -CachePort $cacheHost.PortNo ` + -ErrorAction SilentlyContinue + $hostName = $cacheHost.HostName + $cacheserver = $hostName.Split('.')[0] + $spCacheSvc = Get-SPServiceInstance -Server $cacheserver | Where-Object -FilterScript { $_.GetType().Name -eq 'SPDistributedCacheServiceInstance' - }) - # Cluster Size comes from the SE-native cmdlet (string like 'Small'/'Medium'/...). - $clusterInfo = Get-SPCacheClusterInfo -ErrorAction SilentlyContinue - $clusterSize = if ($null -ne $clusterInfo) { "$($clusterInfo.Size)" } else { '' } - $reportedServers = New-Object -TypeName System.Collections.ArrayList - foreach ($dcInst in $dcInstances) { - $cacheserver = "$($dcInst.Server.Address)".Split('.')[0] - $SPInstanceStatus = "$($dcInst.Status)" - # Try Get-SPCacheHostConfig with the FQDN then short name; some SE - # builds return null for both - degrade gracefully to SP DC defaults. - $cacheHostConfig = $null - $cacheHost = $null - foreach ($tryHost in @("$($dcInst.Server.Address)", $cacheserver)) { - if ([string]::IsNullOrEmpty($tryHost)) { continue } - $cacheHostConfig = Get-SPCacheHostConfig -HostName $tryHost -ErrorAction SilentlyContinue - if ($null -ne $cacheHostConfig) { break } } - if ($null -ne $cacheHostConfig) { - $cacheHost = Get-SPCacheHost -HostName $cacheHostConfig.HostName -CachePort $cacheHostConfig.CachePort -ErrorAction SilentlyContinue + if ($cacheHost.Status -ne 'Up') { + $isMailInfo = $false } - $port = if ($null -ne $cacheHostConfig) { "$($cacheHostConfig.CachePort)" } else { '22233' } - $size = if ($null -ne $cacheHostConfig) { "$($cacheHostConfig.Size)" } else { $clusterSize } - $serviceName = if ($null -ne $cacheHost) { "$($cacheHost.ServiceName)" } else { 'AppFabricCachingService' } - $cacheStatus = if ($null -ne $cacheHost) { "$($cacheHost.Status)" } - elseif ($SPInstanceStatus -eq 'Online') { 'Up' } - else { 'Unknown' } - $isMailInfo = ($SPInstanceStatus -eq 'Online' -and $cacheStatus -eq 'Up') - [void]$tbAppFabricStatus.Add([AppFabricStatus]@{ - Farm = $params.Farm - Server = $cacheserver; - Port = $port; - ServiceName = $serviceName; - Size = $size; - CacheStatus = $cacheStatus; - SPInstanceStatus = $SPInstanceStatus; - IsInfo = $isMailInfo; - }) - [void]$reportedServers.Add($cacheserver) - } - # Servers that are not part of the cache cluster: informational row - # (legitimate topology to host Distributed Cache on a subset of servers). - foreach ($srv in $allSPServers) { - if ($reportedServers -notcontains $srv) { - [void]$tbAppFabricStatus.Add([AppFabricStatus]@{ - Farm = $params.Farm - Server = $srv; - Port = ''; - ServiceName = ''; - Size = ''; - CacheStatus = ''; - SPInstanceStatus = 'Not a cache host'; - IsInfo = $true; - }) - } - } - return $tbAppFabricStatus - } - else { - Use-CacheCluster -ErrorAction SilentlyContinue - $cacheHosts = Get-CacheHost -ErrorAction SilentlyContinue - if ($null -ne $cacheHosts) { - foreach ($cacheHost in $cacheHosts) { - $isMailInfo = $true - $cacheHostConfig = Get-AFCacheHostConfiguration -ComputerName $cacheHost.HostName ` - -CachePort $cacheHost.PortNo ` - -ErrorAction SilentlyContinue - $hostName = $cacheHost.HostName - $cacheserver = $hostName.Split('.')[0] - $spCacheSvc = Get-SPServiceInstance -Server $cacheserver | Where-Object -FilterScript { - $_.GetType().Name -eq 'SPDistributedCacheServiceInstance' - } - if ($cacheHost.Status -ne 'Up') { - $isMailInfo = $false - } - if ($null -ne $spCacheSvc) { - $SPInstanceStatus = $spCacheSvc.Status - if ($SPInstanceStatus -ne 'Online') { - $isMailInfo = $false - } - } - else { - $SPInstanceStatus = 'SPService Not Found' + if ($null -ne $spCacheSvc) { + $SPInstanceStatus = $spCacheSvc.Status + if ($SPInstanceStatus -ne 'Online') { $isMailInfo = $false } - [void]$tbAppFabricStatus.Add([AppFabricStatus]@{ + } + else { + $SPInstanceStatus = 'SPService Not Found' + $isMailInfo = $false + } + [void]$tbAppFabricStatus.Add([AppFabricStatus]@{ Farm = $params.Farm - Server = $cacheserver; - Port = $cacheHost.PortNo; - ServiceName = $cacheHost.ServiceName; - Size = $cacheHostConfig.Size; - CacheStatus = $cacheHost.Status; - SPInstanceStatus = $SPInstanceStatus; - IsInfo = $isMailInfo; + Server = $cacheserver + Port = $cacheHost.PortNo + ServiceName = $cacheHost.ServiceName + Size = $cacheHostConfig.Size + CacheStatus = $cacheHost.Status + SPInstanceStatus = $SPInstanceStatus + IsInfo = $isMailInfo }) - } - return $tbAppFabricStatus } + return $tbAppFabricStatus } } return $result diff --git a/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 b/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 index 18a5db4..de7152d 100644 --- a/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 +++ b/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'SPSWeather.Common.psm1' - ModuleVersion = '2.3.5' + ModuleVersion = '2.3.6' GUID = 'c39bd612-8520-4e65-9037-80060894d654' Author = 'Jean-Cyril DROUHIN' CompanyName = 'luigilink' diff --git a/tests/SPSWeather.Common.Tests.ps1 b/tests/SPSWeather.Common.Tests.ps1 index e14893c..c87718f 100644 --- a/tests/SPSWeather.Common.Tests.ps1 +++ b/tests/SPSWeather.Common.Tests.ps1 @@ -36,7 +36,7 @@ Describe 'SPSWeather.Common module' { } It 'manifest version is 2.0.0 or higher' { - (Test-ModuleManifest -Path $modulePath).Version | Should -BeGreaterOrEqual ([version]'2.3.5') + (Test-ModuleManifest -Path $modulePath).Version | Should -BeGreaterOrEqual ([version]'2.3.6') } It 'exports exactly the expected public functions' {