From 5ba52c37086bf7bf81f146a6a3bedd3be414ebbc Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Mon, 29 Jun 2026 10:44:40 +0200 Subject: [PATCH 1/4] fix(event): self-heal SPSWeather event source mapping Add-SPSWeatherEvent returned silently when the source was registered against another log (legacy flat scripts could bind 'SPSWeather' to Application), so no SPSWeather log/events were ever written. Re-point the source to the SPSWeather log and warn (not silent) on failure. Fixes #35 --- CHANGELOG.md | 10 ++++++ RELEASE-NOTES.md | 10 ++++++ .../Public/Add-SPSWeatherEvent.ps1 | 35 ++++++++++++------- .../SPSWeather.Common/SPSWeather.Common.psd1 | 2 +- tests/SPSWeather.Common.Tests.ps1 | 8 ++++- 5 files changed, 51 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c7dd1..0ad91f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.2.5] - 2026-06-29 + +### Fixed + +- `Add-SPSWeatherEvent` no longer returns silently when its source is already + registered against another log (legacy flat scripts could bind 'SPSWeather' + to Application): it now re-points the source to the SPSWeather log and only + warns if the registration/write fails. This is why no SPSWeather log or events + appeared on a fresh install (#35). + ## [2.2.4] - 2026-06-29 ### Fixed diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 9fab8ab..f41c400 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,15 @@ # SPSWeather - Release Notes +## [2.2.5] - 2026-06-29 + +### Fixed + +- The SPSWeather Event Log is now created reliably: Add-SPSWeatherEvent re-points + a source that legacy scripts bound to the Application log, instead of giving up + silently, so install/run/alert events are recorded. + +A full list of changes can be found in the [change log](CHANGELOG.md). + ## [2.2.4] - 2026-06-29 ### Fixed diff --git a/src/Modules/SPSWeather.Common/Public/Add-SPSWeatherEvent.ps1 b/src/Modules/SPSWeather.Common/Public/Add-SPSWeatherEvent.ps1 index 8c8500d..3f49a3b 100644 --- a/src/Modules/SPSWeather.Common/Public/Add-SPSWeatherEvent.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Add-SPSWeatherEvent.ps1 @@ -59,21 +59,32 @@ $LogName = 'SPSWeather' - if ([System.Diagnostics.EventLog]::SourceExists($Source)) { - $sourceLogName = [System.Diagnostics.EventLog]::LogNameFromSourceName($Source, '.') - if ($LogName -ne $sourceLogName) { - Write-Verbose -Message "[ERROR] Specified source {$Source} already exists on log {$sourceLogName}" - return - } - } - else { - if ([System.Diagnostics.EventLog]::Exists($LogName) -eq $false) { - $null = New-EventLog -LogName $LogName -Source $Source + # Ensure the event source exists and is mapped to the SPSWeather log. The + # legacy flat scripts could register some sources under 'Application'; in that + # case re-point them to SPSWeather instead of silently giving up (which is why + # no SPSWeather log/events appeared). Requires admin (the script validates it). + try { + if ([System.Diagnostics.EventLog]::SourceExists($Source)) { + $sourceLogName = [System.Diagnostics.EventLog]::LogNameFromSourceName($Source, '.') + if ($LogName -ne $sourceLogName) { + Write-Verbose -Message "Source '$Source' is mapped to log '$sourceLogName'; re-pointing it to '$LogName'." + [System.Diagnostics.EventLog]::DeleteEventSource($Source) + [System.Diagnostics.EventLog]::CreateEventSource($Source, $LogName) + } } else { - [System.Diagnostics.EventLog]::CreateEventSource($Source, $LogName) + if ([System.Diagnostics.EventLog]::Exists($LogName) -eq $false) { + $null = New-EventLog -LogName $LogName -Source $Source + } + else { + [System.Diagnostics.EventLog]::CreateEventSource($Source, $LogName) + } } } + catch { + Write-Warning -Message "Could not register event source '$Source' on log '$LogName' (need admin?): $($_.Exception.Message)" + return + } $autoVersion = $MyInvocation.MyCommand.Module.Version if ($null -eq $autoVersion) { @@ -92,7 +103,7 @@ ComputerName: $($env:COMPUTERNAME) Write-EventLog -LogName $LogName -Source $Source -EventId $EventID -Message ($headerMessage + "`r`n" + $Message) -EntryType $EntryType } catch { - Write-Error -Message @" + Write-Warning -Message @" SPSWeather Version: $scriptVersion An error occurred while writing to Event Log in Source: $Source User: $userName diff --git a/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 b/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 index af8e525..e7d4050 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.2.4' + ModuleVersion = '2.2.5' 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 c34cdbc..3591f33 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.2.3') + (Test-ModuleManifest -Path $modulePath).Version | Should -BeGreaterOrEqual ([version]'2.2.5') } It 'exports exactly the expected public functions' { @@ -178,6 +178,12 @@ Describe 'Public function contracts' { $cmd.Parameters.Keys | Should -Contain 'EventID' } + It 'Add-SPSWeatherEvent self-heals a misrouted source instead of returning silently' { + $src = (Get-Command -Name Add-SPSWeatherEvent -Module SPSWeather.Common).Definition + $src | Should -Match 'DeleteEventSource' + $src | Should -Not -Match '\[ERROR\] Specified source' + } + It 'Get-SPSInstalledProductVersion returns null off a SharePoint server' -Skip:($IsWindows -and (Test-Path 'C:\Program Files\Common Files\microsoft shared\Web Server Extensions')) { Get-SPSInstalledProductVersion | Should -BeNullOrEmpty } From 5872923770cecbe8357a501c114710f224762593 Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Mon, 29 Jun 2026 10:56:45 +0200 Subject: [PATCH 2/4] fix(sys): per-server try/catch so one unreachable node is WARN not a fatal dump Each SYS check wraps the per-server second-hop call: an unreachable server yields a single 'Unreachable' row and the others continue, instead of a raw 0x80090322 double-hop error aborting the whole farm. Also fixes a $nulll typo that always blanked the .NET version. Fixes #33 --- .../Public/Get-SYSDOTNETVersion.ps1 | 54 +++++++------ .../Public/Get-SYSDiskUsageStatus.ps1 | 49 +++++++----- .../Public/Get-SYSEvtAppErrors.ps1 | 9 ++- .../Public/Get-SYSIISAppPoolStatus.ps1 | 73 ++++++++++-------- .../Public/Get-SYSIISSiteCertStatus.ps1 | 76 +++++++++++-------- .../Public/Get-SYSIISW3WPEXEStatus.ps1 | 33 +++++--- .../Public/Get-SYSLastRebootStatus.ps1 | 29 ++++--- 7 files changed, 199 insertions(+), 124 deletions(-) diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 index 69bcf1b..2624c79 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 @@ -33,32 +33,42 @@ } $tbSYSDOTNETVersion = New-Object -TypeName System.Collections.ArrayList foreach ($spServer in $params.Servers) { - [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $resultReg = Invoke-Command -ComputerName $remoteServer -ScriptBlock { - try { - $getItemProperty = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" + try { + [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName + $resultReg = Invoke-Command -ComputerName $remoteServer -ScriptBlock { + try { + $getItemProperty = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" + } + catch { + $null = $getItemProperty + } + return $getItemProperty } - catch { - $null = $getItemProperty + if ($null -ne $resultReg) { + [System.Boolean]$NetRequiredBool = ($resultReg.Release -ge 528040) + [void]$tbSYSDOTNETVersion.Add([SYSDOTNETVersion]@{ + Farm = $params.Farm; + Server = $spServer; + NetVersion = "$($resultReg.Version)"; + NetRequiredVersion = $NetRequiredBool; + }) + } + else { + [void]$tbSYSDOTNETVersion.Add([SYSDOTNETVersion]@{ + Farm = $params.Farm; + Server = $spServer; + NetVersion = $null; + NetRequiredVersion = $null; + }) } - return $getItemProperty - } - if ($nulll -ne $resultReg){ - [System.Boolean]$NetRequiredBool = ($resultReg.Release -ge 528040) - [void]$tbSYSDOTNETVersion.Add([SYSDOTNETVersion]@{ - Farm = $params.Farm; - Server = $spServer; - NetVersion = "$($resultReg.Version)"; - NetRequiredVersion = $NetRequiredBool; - }) } - else { + catch { [void]$tbSYSDOTNETVersion.Add([SYSDOTNETVersion]@{ - Farm = $params.Farm; - Server = $spServer; - NetVersion = $null; - NetRequiredVersion = $null; - }) + Farm = $params.Farm; + Server = $spServer; + NetVersion = 'Unreachable'; + NetRequiredVersion = $false; + }) } } return $tbSYSDOTNETVersion diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 index 9b2b60b..8e0b0fb 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 @@ -40,28 +40,41 @@ } $tbSYSDiskUsageStatus = New-Object -TypeName System.Collections.ArrayList foreach ($spServer in $params.Servers) { - [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $getDrives = Invoke-Command -ComputerName $remoteServer { Get-Volume | Where-Object -FilterScript { $_.DriveType -eq 'Fixed' -and $null -ne $_.DriveLetter } } - foreach ($getDrive in $getDrives) { - $driveSize = [math]::Round($($getDrive.Size) / 1073741824, 2) - $driveFree = [math]::Round($($getDrive.SizeRemaining) / 1073741824, 2) - $perFreeSpace = ($getDrive.SizeRemaining / $getDrive.Size) * 100 - if ($perFreeSpace -gt $params.WarningPercentage) { - $freeSpaceStatus = 'OK' - $isMailInfo = $true - } - else { - $freeSpaceStatus = "Less than $WarningPercentage %" - $isMailInfo = $false + try { + [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName + $getDrives = Invoke-Command -ComputerName $remoteServer { Get-Volume | Where-Object -FilterScript { $_.DriveType -eq 'Fixed' -and $null -ne $_.DriveLetter } } + foreach ($getDrive in $getDrives) { + $driveSize = [math]::Round($($getDrive.Size) / 1073741824, 2) + $driveFree = [math]::Round($($getDrive.SizeRemaining) / 1073741824, 2) + $perFreeSpace = ($getDrive.SizeRemaining / $getDrive.Size) * 100 + if ($perFreeSpace -gt $params.WarningPercentage) { + $freeSpaceStatus = 'OK' + $isMailInfo = $true + } + else { + $freeSpaceStatus = "Less than $($params.WarningPercentage) %" + $isMailInfo = $false + } + [void]$tbSYSDiskUsageStatus.Add([SYSDiskUsageStatus]@{ + Farm = $params.Farm; + Server = $spServer; + DriveLetter = $getDrive.DriveLetter; + Size = $driveSize; + FreeSpace = $driveFree; + Status = $freeSpaceStatus; + IsInfo = $isMailInfo + }) } + } + catch { [void]$tbSYSDiskUsageStatus.Add([SYSDiskUsageStatus]@{ Farm = $params.Farm; Server = $spServer; - DriveLetter = $getDrive.DriveLetter; - Size = $driveSize; - FreeSpace = $driveFree; - Status = $freeSpaceStatus; - IsInfo = $isMailInfo + DriveLetter = ''; + Size = ''; + FreeSpace = ''; + Status = 'Unreachable'; + IsInfo = $false }) } } diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 index da51603..19ff69c 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 @@ -67,7 +67,14 @@ } } catch { - return $_ + [void]$tbSYSEventViewerAppErrors.Add([SYSEventViewerAppError]@{ + Farm = $params.Farm + Server = $pSserver; + ID = 'Non Applicable'; + Severity = 'Warning'; + Name = 'Unreachable'; + Count = '0'; + }) } } return $tbSYSEventViewerAppErrors diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 index f6735ee..846ac71 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 @@ -34,42 +34,53 @@ } $tbIISApplicationPoolStatus = New-Object -TypeName System.Collections.ArrayList foreach ($spServer in $params.Servers) { - [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $spWebAppStatus = Invoke-Command -ComputerName $remoteServer -ScriptBlock { - Import-Module WebAdministration; Get-WebAppPoolState - } - $spWebAppStatusList = $spWebAppStatus.SyncRoot | Select-Object -property Value, ITemXPath - if ($spWebAppStatusList) { - foreach ($spWebAppPool in $spWebAppStatusList) { - $isMailInfo = $true - $webAppSiteStatus = $spWebAppPool.Value - $webAppSiteName = $spWebAppPool.ItemXPath.Split("'")[1] - $isSPAPPPool = Get-SPServiceApplicationPool | Where-Object -FilterScript { $_.ID -eq $WebAppSiteName } | Select-Object -ExpandProperty Name - if ($isSPAPPPool) { - $spAppPoolName = $isSPAPPPool - } - else { - $spAppPoolName = $webAppSiteName - } - if ($spAppPoolName -eq 'SharePoint Web Services Root') { - if ($webAppSiteStatus -ne 'Stopped') { - $isMailInfo = $false + try { + [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName + $spWebAppStatus = Invoke-Command -ComputerName $remoteServer -ScriptBlock { + Import-Module WebAdministration; Get-WebAppPoolState + } + $spWebAppStatusList = $spWebAppStatus.SyncRoot | Select-Object -property Value, ITemXPath + if ($spWebAppStatusList) { + foreach ($spWebAppPool in $spWebAppStatusList) { + $isMailInfo = $true + $webAppSiteStatus = $spWebAppPool.Value + $webAppSiteName = $spWebAppPool.ItemXPath.Split("'")[1] + $isSPAPPPool = Get-SPServiceApplicationPool | Where-Object -FilterScript { $_.ID -eq $WebAppSiteName } | Select-Object -ExpandProperty Name + if ($isSPAPPPool) { + $spAppPoolName = $isSPAPPPool } - } - else { - if ($webAppSiteStatus -ne 'Started') { - $isMailInfo = $false + else { + $spAppPoolName = $webAppSiteName + } + if ($spAppPoolName -eq 'SharePoint Web Services Root') { + if ($webAppSiteStatus -ne 'Stopped') { + $isMailInfo = $false + } } + else { + if ($webAppSiteStatus -ne 'Started') { + $isMailInfo = $false + } + } + [void]$tbIISApplicationPoolStatus.Add([IISApplicationPoolStatus]@{ + Farm = $params.Farm + Server = $spServer; + ApplicationPool = $spAppPoolName; + Status = $webAppSiteStatus; + IsInfo = $isMailInfo; + }) } - [void]$tbIISApplicationPoolStatus.Add([IISApplicationPoolStatus]@{ - Farm = $params.Farm - Server = $spServer; - ApplicationPool = $spAppPoolName; - Status = $webAppSiteStatus; - IsInfo = $isMailInfo; - }) } } + catch { + [void]$tbIISApplicationPoolStatus.Add([IISApplicationPoolStatus]@{ + Farm = $params.Farm + Server = $spServer; + ApplicationPool = 'Unreachable'; + Status = ''; + IsInfo = $false; + }) + } } return $tbIISApplicationPoolStatus } diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 index 814add3..b4f4d53 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 @@ -41,44 +41,56 @@ $tbIISSiteCertStatus = New-Object -TypeName System.Collections.ArrayList $expirationDate = (Get-Date).AddDays($params.Expiration) foreach ($spServer in $params.Servers) { - [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $spSvcInstanceIIS = Get-SPServiceInstance -Server $spServer | Where-Object -FilterScript { - $_.Status -eq 'Online' -and $_.GetType().Name -eq 'SPWebServiceInstance' - } - - if ($null -ne $spSvcInstanceIIS) { - $getWebBindings = Invoke-Command -ComputerName $remoteServer { Get-WebBinding } - $getSSLBindings = $getWebBindings | Where-Object -FilterScript { - $_.protocol -eq 'https' -and $_.bindingInformation -like '*443*' + try { + [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName + $spSvcInstanceIIS = Get-SPServiceInstance -Server $spServer | Where-Object -FilterScript { + $_.Status -eq 'Online' -and $_.GetType().Name -eq 'SPWebServiceInstance' } - if ($getSSLBindings) { - foreach ($binding in $getSSLBindings) { - $iisSiteName = (($binding.ItemXPath -split ([RegEx]::Escape("[@name='")))[1]).split("'")[0] - $getCertMyStore = Invoke-Command -ComputerName $remoteServer { Get-ChildItem 'Cert:LocalMachine\My' } - $getCertificate = $getCertMyStore | Where-Object -FilterScript { - $_.Thumbprint -eq $binding.certificateHash - } - $certExpiration = $getCertificate.NotAfter - if ($certExpiration -gt $expirationDate) { - $certStatus = 'OK' - $isMailInfo = $true - } - else { - $certStatus = 'Renew cert' - $isMailInfo = $false + if ($null -ne $spSvcInstanceIIS) { + $getWebBindings = Invoke-Command -ComputerName $remoteServer { Get-WebBinding } + $getSSLBindings = $getWebBindings | Where-Object -FilterScript { + $_.protocol -eq 'https' -and $_.bindingInformation -like '*443*' + } + if ($getSSLBindings) { + foreach ($binding in $getSSLBindings) { + $iisSiteName = (($binding.ItemXPath -split ([RegEx]::Escape("[@name='")))[1]).split("'")[0] + + $getCertMyStore = Invoke-Command -ComputerName $remoteServer { Get-ChildItem 'Cert:LocalMachine\My' } + $getCertificate = $getCertMyStore | Where-Object -FilterScript { + $_.Thumbprint -eq $binding.certificateHash + } + $certExpiration = $getCertificate.NotAfter + if ($certExpiration -gt $expirationDate) { + $certStatus = 'OK' + $isMailInfo = $true + } + else { + $certStatus = 'Renew cert' + $isMailInfo = $false + } + [void]$tbIISSiteCertStatus.Add([IISWebSiteCertStatus]@{ + Farm = $params.Farm; + Server = $spServer; + WebSiteName = $iisSiteName; + ExpirationDate = $certExpiration; + Status = $certStatus; + IsInfo = $isMailInfo; + }) } - [void]$tbIISSiteCertStatus.Add([IISWebSiteCertStatus]@{ - Farm = $params.Farm; - Server = $spServer; - WebSiteName = $iisSiteName; - ExpirationDate = $certExpiration; - Status = $certStatus; - IsInfo = $isMailInfo; - }) } } } + catch { + [void]$tbIISSiteCertStatus.Add([IISWebSiteCertStatus]@{ + Farm = $params.Farm; + Server = $spServer; + WebSiteName = ''; + ExpirationDate = ''; + Status = 'Unreachable'; + IsInfo = $false; + }) + } } return $tbIISSiteCertStatus } diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 index 7c7c6be..ec32834 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 @@ -34,21 +34,32 @@ } $tbIISWorkerProcessStatus = New-Object -TypeName System.Collections.ArrayList foreach ($spServer in $params.Servers) { - [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $w3wpProcess = Invoke-Command -ComputerName $remoteServer -ScriptBlock { - Get-CimInstance Win32_Process -Filter "name = 'w3wp.exe'" | Sort-Object CommandLine + try { + [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName + $w3wpProcess = Invoke-Command -ComputerName $remoteServer -ScriptBlock { + Get-CimInstance Win32_Process -Filter "name = 'w3wp.exe'" | Sort-Object CommandLine + } + foreach ($w3wpProc in $w3wpProcess) { + $w3wpProcCmdLine = $w3wpProc.CommandLine.Replace('c:\windows\system32\inetsrv\w3wp.exe -ap "', '') + $pos = $w3wpProcCmdLine.IndexOf('"') + $appPoolName = $w3wpProcCmdLine.Substring(0, $pos) + $w3wpMemoryUsage = [Math]::Round($w3wpProc.WorkingSetSize / 1MB) + [void]$tbIISWorkerProcessStatus.Add([IISWorkerProcessStatus]@{ + Farm = $params.Farm + Server = $spServer; + CreationDate = $w3wpProc.CreationDate; + Memory = $w3wpMemoryUsage; + ApplicationPool = $appPoolName; + }) + } } - foreach ($w3wpProc in $w3wpProcess) { - $w3wpProcCmdLine = $w3wpProc.CommandLine.Replace('c:\windows\system32\inetsrv\w3wp.exe -ap "', '') - $pos = $w3wpProcCmdLine.IndexOf('"') - $appPoolName = $w3wpProcCmdLine.Substring(0, $pos) - $w3wpMemoryUsage = [Math]::Round($w3wpProc.WorkingSetSize / 1MB) + catch { [void]$tbIISWorkerProcessStatus.Add([IISWorkerProcessStatus]@{ Farm = $params.Farm Server = $spServer; - CreationDate = $w3wpProc.CreationDate; - Memory = $w3wpMemoryUsage; - ApplicationPool = $appPoolName; + CreationDate = ''; + Memory = ''; + ApplicationPool = 'Unreachable'; }) } } diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 index 2241c0c..8c347b5 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 @@ -34,15 +34,26 @@ } $tbSYSLastRebootStatus = New-Object -TypeName System.Collections.ArrayList foreach ($spServer in $params.Servers) { - [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $cimWin32_OS = Get-CimInstance -ComputerName $remoteServer -ClassName win32_operatingsystem - [void]$tbSYSLastRebootStatus.Add([SYSLastRebootStatus]@{ - Farm = $params.Farm; - Server = $spServer; - OSName = $cimWin32_OS.Caption; - OSVersion = $cimWin32_OS.Version; - LastRebootTime = $cimWin32_OS.LastBootUpTime; - }) + try { + [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName + $cimWin32_OS = Get-CimInstance -ComputerName $remoteServer -ClassName win32_operatingsystem + [void]$tbSYSLastRebootStatus.Add([SYSLastRebootStatus]@{ + Farm = $params.Farm; + Server = $spServer; + OSName = $cimWin32_OS.Caption; + OSVersion = $cimWin32_OS.Version; + LastRebootTime = $cimWin32_OS.LastBootUpTime; + }) + } + catch { + [void]$tbSYSLastRebootStatus.Add([SYSLastRebootStatus]@{ + Farm = $params.Farm; + Server = $spServer; + OSName = 'Unreachable'; + OSVersion = ''; + LastRebootTime = ''; + }) + } } return $tbSYSLastRebootStatus } From 0dee3b518d60139764c74f329c9c1c240859d044 Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Mon, 29 Jun 2026 10:56:45 +0200 Subject: [PATCH 3/4] fix(search): WARN row on Search 503 instead of raw stack trace Crawl status/logs/topology wrap the Search admin calls; a down topology host yields one 'Search unavailable' row instead of dumping a 503 WebException. Fixes #34 --- CHANGELOG.md | 8 +++++++ RELEASE-NOTES.md | 3 +++ .../Public/Get-SPSSearchEntCrawlLogs.ps1 | 19 +++++++++++---- .../Public/Get-SPSSearchEntCrawlStatus.ps1 | 23 ++++++++++++++----- .../Public/Get-SPSSearchEntTopology.ps1 | 18 +++++++++++---- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad91f9..6d84949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to Application): it now re-points the source to the SPSWeather log and only warns if the registration/write fails. This is why no SPSWeather log or events appeared on a fresh install (#35). +- Per-server SYS checks (IIS app pools, W3WP, certs, last reboot, .NET, disks, + event log) wrap each server in try/catch: an unreachable node yields a single + 'Unreachable' row instead of dumping a raw 0x80090322 double-hop error and + aborting the whole farm's collection (#33). Fixed a `$nulll` typo that always + blanked the .NET version. +- Search checks (crawl status, crawl logs, topology) wrap the Search admin calls + in try/catch: a down Search topology host yields one 'Search unavailable' row + instead of a raw 503 stack trace (#34). ## [2.2.4] - 2026-06-29 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f41c400..7b441a6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -7,6 +7,9 @@ - The SPSWeather Event Log is now created reliably: Add-SPSWeatherEvent re-points a source that legacy scripts bound to the Application log, instead of giving up silently, so install/run/alert events are recorded. +- One unreachable farm server no longer wipes the system report: each server is + reported as 'Unreachable' and the rest continue, and Search outages show as a + single 'Search unavailable' row instead of a raw 503/double-hop dump. A full list of changes can be found in the [change log](CHANGELOG.md). diff --git a/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlLogs.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlLogs.ps1 index 9ec9dc6..e8310c4 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlLogs.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlLogs.ps1 @@ -30,11 +30,10 @@ } $spSearchEntSvc = Get-SPEnterpriseSearchServiceApplication -ErrorAction SilentlyContinue if ($null -ne $spSearchEntSvc) { - $spSchContentSources = Get-SPEnterpriseSearchCrawlContentSource -SearchApplication $spSearchEntSvc - if ($null -ne $spSchContentSources) { + $tbSPSSearchEntCrawlLogs = New-Object -TypeName System.Collections.ArrayList + try { + $spSchContentSources = Get-SPEnterpriseSearchCrawlContentSource -SearchApplication $spSearchEntSvc -ErrorAction Stop $spSchCrawlLogPso = New-Object Microsoft.Office.Server.Search.Administration.CrawlLog $spSearchEntSvc - $tbSPSSearchEntCrawlLogs = New-Object -TypeName System.Collections.ArrayList - foreach ($contentSource in $spSchContentSources) { $getSPCrawlErrors = $spSchCrawlLogPso.GetCrawlErrors($contentSource.ID, -1) if ($getSPCrawlErrors.Rows.Count -ne 0) { @@ -50,8 +49,18 @@ } } } - return $tbSPSSearchEntCrawlLogs } + catch { + [void]$tbSPSSearchEntCrawlLogs.Add([SearchContentCrawlLog]@{ + Farm = $params.Farm; + SearchService = $spSearchEntSvc.Name; + ContentSource = 'Search unavailable'; + ErrorID = '503'; + Message = $_.Exception.Message; + Count = '0'; + }) + } + return $tbSPSSearchEntCrawlLogs } } return $result diff --git a/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlStatus.ps1 index 63632f1..eb66d96 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntCrawlStatus.ps1 @@ -32,11 +32,10 @@ } $spSearchEntSvc = Get-SPEnterpriseSearchServiceApplication -ErrorAction SilentlyContinue if ($null -ne $spSearchEntSvc) { - $spSchContentSources = Get-SPEnterpriseSearchCrawlContentSource -SearchApplication $spSearchEntSvc - if ($null -ne $spSchContentSources) { - #Initialize ArrayList variable - $tbSPSSearchEntCrawlStatus = New-Object -TypeName System.Collections.ArrayList - + #Initialize ArrayList variable + $tbSPSSearchEntCrawlStatus = New-Object -TypeName System.Collections.ArrayList + try { + $spSchContentSources = Get-SPEnterpriseSearchCrawlContentSource -SearchApplication $spSearchEntSvc -ErrorAction Stop foreach ($contentSource in $spSchContentSources) { $spCrawlDuration = 'OK' $isMailInfo = $true @@ -57,8 +56,20 @@ IsInfo = $isMailInfo; }) } - return $tbSPSSearchEntCrawlStatus } + catch { + [void]$tbSPSSearchEntCrawlStatus.Add([SearchContentLastCrawl]@{ + Farm = $params.Farm; + SearchService = $spSearchEntSvc.Name; + ContentSource = 'Search unavailable'; + CrawlState = $_.Exception.Message; + Duration = 'Error'; + CrawlStarted = ''; + CrawlCompleted = ''; + IsInfo = $false; + }) + } + return $tbSPSSearchEntCrawlStatus } } return $result diff --git a/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntTopology.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntTopology.ps1 index 122916c..c72a465 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntTopology.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SPSSearchEntTopology.ps1 @@ -30,9 +30,9 @@ } $spSearchEntSvc = Get-SPEnterpriseSearchServiceApplication -ErrorAction SilentlyContinue if ($null -ne $spSearchEntSvc) { - $getSchStatus = Get-SPEnterpriseSearchStatus -SearchApplication $spSearchEntSvc -Detailed - if ($null -ne $getSchStatus) { - $tbSearchTopologyStatus = New-Object -TypeName System.Collections.ArrayList + $tbSearchTopologyStatus = New-Object -TypeName System.Collections.ArrayList + try { + $getSchStatus = Get-SPEnterpriseSearchStatus -SearchApplication $spSearchEntSvc -Detailed -ErrorAction Stop foreach ($compoSch in $getSchStatus) { $isMailInfo = $True if ($compoSch.State -ne 'Active') { @@ -53,8 +53,18 @@ IsInfo = $isMailInfo; }) } - return $tbSearchTopologyStatus } + catch { + [void]$tbSearchTopologyStatus.Add([SearchTopologyStatus]@{ + Farm = $params.Farm; + SearchService = $spSearchEntSvc.Name; + ComponentHost = 'Unreachable'; + ComponentName = 'Search unavailable'; + State = $_.Exception.Message; + IsInfo = $false; + }) + } + return $tbSearchTopologyStatus } } return $result From caa6aa25ae6212b5e2c88067b10e7297300b4046 Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Mon, 29 Jun 2026 11:14:17 +0200 Subject: [PATCH 4/4] fix(sys): force remote calls terminating so unreachable nodes are caught The session-open failure was non-terminating, so the try/catch never fired and the raw 0x80090322 dump still printed. Add -ErrorAction Stop to each remote Invoke-Command/Get-CimInstance so a down node becomes one WARN row. Fixes #33 --- src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 | 2 +- .../SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 | 2 +- src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 | 2 +- .../SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 | 2 +- .../SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 | 4 ++-- .../SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 | 2 +- .../SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 index 2624c79..ac5011d 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSDOTNETVersion.ps1 @@ -35,7 +35,7 @@ foreach ($spServer in $params.Servers) { try { [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $resultReg = Invoke-Command -ComputerName $remoteServer -ScriptBlock { + $resultReg = Invoke-Command -ComputerName $remoteServer -ErrorAction Stop -ScriptBlock { try { $getItemProperty = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" } diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 index 8e0b0fb..fdfdeed 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSDiskUsageStatus.ps1 @@ -42,7 +42,7 @@ foreach ($spServer in $params.Servers) { try { [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $getDrives = Invoke-Command -ComputerName $remoteServer { Get-Volume | Where-Object -FilterScript { $_.DriveType -eq 'Fixed' -and $null -ne $_.DriveLetter } } + $getDrives = Invoke-Command -ComputerName $remoteServer -ErrorAction Stop { Get-Volume | Where-Object -FilterScript { $_.DriveType -eq 'Fixed' -and $null -ne $_.DriveLetter } } foreach ($getDrive in $getDrives) { $driveSize = [math]::Round($($getDrive.Size) / 1073741824, 2) $driveFree = [math]::Round($($getDrive.SizeRemaining) / 1073741824, 2) diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 index 19ff69c..3f6fd0f 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSEvtAppErrors.ps1 @@ -37,7 +37,7 @@ foreach ($pSserver in $params.Servers) { [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($pSserver).HostName try { - $appErrors = Invoke-Command -ComputerName $remoteServer -ScriptBlock { + $appErrors = Invoke-Command -ComputerName $remoteServer -ErrorAction Stop -ScriptBlock { Get-WinEvent -FilterHashTable @{LogName = 'Application'; Level = 2; StartTime = ((Get-Date) - (New-TimeSpan -Days 1)) } ` -ErrorAction SilentlyContinue } diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 index 846ac71..5e2b679 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSIISAppPoolStatus.ps1 @@ -36,7 +36,7 @@ foreach ($spServer in $params.Servers) { try { [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $spWebAppStatus = Invoke-Command -ComputerName $remoteServer -ScriptBlock { + $spWebAppStatus = Invoke-Command -ComputerName $remoteServer -ErrorAction Stop -ScriptBlock { Import-Module WebAdministration; Get-WebAppPoolState } $spWebAppStatusList = $spWebAppStatus.SyncRoot | Select-Object -property Value, ITemXPath diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 index b4f4d53..19dcf6b 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSIISSiteCertStatus.ps1 @@ -48,7 +48,7 @@ } if ($null -ne $spSvcInstanceIIS) { - $getWebBindings = Invoke-Command -ComputerName $remoteServer { Get-WebBinding } + $getWebBindings = Invoke-Command -ComputerName $remoteServer -ErrorAction Stop { Get-WebBinding } $getSSLBindings = $getWebBindings | Where-Object -FilterScript { $_.protocol -eq 'https' -and $_.bindingInformation -like '*443*' } @@ -56,7 +56,7 @@ foreach ($binding in $getSSLBindings) { $iisSiteName = (($binding.ItemXPath -split ([RegEx]::Escape("[@name='")))[1]).split("'")[0] - $getCertMyStore = Invoke-Command -ComputerName $remoteServer { Get-ChildItem 'Cert:LocalMachine\My' } + $getCertMyStore = Invoke-Command -ComputerName $remoteServer -ErrorAction Stop { Get-ChildItem 'Cert:LocalMachine\My' } $getCertificate = $getCertMyStore | Where-Object -FilterScript { $_.Thumbprint -eq $binding.certificateHash } diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 index ec32834..7bf48ed 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSIISW3WPEXEStatus.ps1 @@ -36,7 +36,7 @@ foreach ($spServer in $params.Servers) { try { [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $w3wpProcess = Invoke-Command -ComputerName $remoteServer -ScriptBlock { + $w3wpProcess = Invoke-Command -ComputerName $remoteServer -ErrorAction Stop -ScriptBlock { Get-CimInstance Win32_Process -Filter "name = 'w3wp.exe'" | Sort-Object CommandLine } foreach ($w3wpProc in $w3wpProcess) { diff --git a/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 index 8c347b5..da9589c 100644 --- a/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Get-SYSLastRebootStatus.ps1 @@ -36,7 +36,7 @@ foreach ($spServer in $params.Servers) { try { [System.String]$remoteServer = [System.Net.Dns]::GetHostByName($spServer).HostName - $cimWin32_OS = Get-CimInstance -ComputerName $remoteServer -ClassName win32_operatingsystem + $cimWin32_OS = Get-CimInstance -ComputerName $remoteServer -ClassName win32_operatingsystem -ErrorAction Stop [void]$tbSYSLastRebootStatus.Add([SYSLastRebootStatus]@{ Farm = $params.Farm; Server = $spServer;