Private/Kinds/Service.ps1
|
# The Service Kind: the Windows services an application cannot work without. # # The business software a German office runs on - accounting, payroll, document management, # the DATEV workplace - is rarely one process. It is a client plus a database engine, a # licence service or an update agent running as a Windows service, and "the program will # not start" is very often "its service is stopped". The App Kind cannot see that: it looks # at processes the Technician's session owns, and a service belongs to nobody's session. # # Which services matter is per application, so it is a Check Definition parameter. Names # are matched as patterns against both the service name and its display name, because # vendors rename one or the other between versions and a Definition should survive that. # # Crashes come from the Stability Check, handed over as -Observed, the way the App Kind # receives application crashes. The Service Control Manager logs a crash under the # service's display name, in the machine's language; that is the same display name this # Gatherer reads off the same machine, so they are compared as they are. function Get-ServiceData { <# .PARAMETER Observed What earlier Checks gathered. Only the Stability Check's service failures are read. #> [CmdletBinding()] [OutputType([psobject])] param( [hashtable]$Parameters = @{}, [AllowNull()][hashtable]$Observed = @{} ) $patterns = @(Get-Parameter $Parameters 'Services' @() | Where-Object { "$_".Trim() } | ForEach-Object { "$_".Trim() }) $optional = @(Get-Parameter $Parameters 'OptionalServices' @() | Where-Object { "$_".Trim() } | ForEach-Object { "$_".Trim() }) $installed = @() if ($patterns.Count -or $optional.Count) { $installed = @(Get-CimInstance Win32_Service -ErrorAction SilentlyContinue) } $matched = @(Select-ServiceMatch -Pattern (@($patterns) + @($optional)) -Service $installed) $stability = $null if ($Observed -and $Observed.ContainsKey('Stability')) { $stability = $Observed['Stability'] } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Service' Patterns = $patterns OptionalPatterns = $optional Services = $matched # Absent when the Stability Check did not run, which is a different fact from a # service that never crashed. StabilityObserved = $null -ne $stability StabilityDays = Get-DataProperty $stability 'Days' ObservedFailures = Get-DataCollection $stability 'ServiceFailures' } } function Select-ServiceMatch { <# .SYNOPSIS The services each pattern names, one row per pattern and service. Pure. .DESCRIPTION One row per pair, so the Judge can tell a pattern that matched nothing from one that matched several without matching anything itself. Separate from the Gatherer so what Gutcheck understood from a Check Definition can be tested without a machine, the way the Server Kind parses its entries. A pattern that is not a valid regular expression matches nothing rather than ending the Check, and so surfaces as the Finding for a pattern that matched nothing. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowEmptyCollection()][string[]]$Pattern = @(), [AllowNull()][AllowEmptyCollection()][object[]]$Service = @() ) foreach ($p in $Pattern) { foreach ($s in @($Service | Where-Object { $_ })) { $hit = $false try { $hit = ("$($s.Name)" -match $p) -or ("$($s.DisplayName)" -match $p) } catch { $hit = $false } if (-not $hit) { continue } [pscustomobject]@{ Pattern = $p Name = "$($s.Name)" DisplayName = "$($s.DisplayName)" State = "$($s.State)" StartMode = "$($s.StartMode)" DelayedAutoStart = $s.DelayedAutoStart StartName = "$($s.StartName)" PathName = "$($s.PathName)" ExitCode = $s.ExitCode } } } } function ConvertTo-ServiceFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) $required = Get-DataCollection $Data 'Patterns' $optional = Get-DataCollection $Data 'OptionalPatterns' if (-not $required.Count -and -not $optional.Count) { return New-Finding -Category Apps -Check (Get-Text 'Check.Service.Services') -Severity INFO ` -Value (Get-Text 'Value.Service.NoneConfigured') ` -Hint (Get-Text 'Hint.Service.ThisCheckDefinitionNamesNo') } $services = Get-DataCollection $Data 'Services' foreach ($pattern in @(@($required) + @($optional))) { if (@($services | Where-Object { $_.Pattern -eq $pattern }).Count) { continue } New-Finding -Category Apps -Check ((Get-Text 'Check.Service.Service') -f $pattern) -Severity INFO ` -Value (Get-Text 'Value.Service.NotInstalled') ` -Hint (Get-Text 'Hint.Service.NoServiceMatches') } # A service matched by two patterns is still one service, and judging it twice would # count its crashes twice. It is optional only when nothing required named it. $unique = @($services | Sort-Object Name -Unique) foreach ($service in $unique) { $name = $service.Name $isOptional = -not @($services | Where-Object { $_.Name -eq $name -and $required -contains $_.Pattern }).Count New-ServiceStateFinding -Service $service -Optional:$isOptional } New-ServiceCrashFinding -Data $Data -Service $unique -Parameters $Parameters } function New-ServiceStateFinding { [CmdletBinding()] param( [Parameter(Mandatory)]$Service, # Named by OptionalServices: a service with legitimate reasons to be off on some # machines, such as a component that runs on one machine per site. Reported with # its state, so a second-level Technician can see it, but not judged for being off. [switch]$Optional ) $check = (Get-Text 'Check.Service.Service') -f $Service.DisplayName $mode = ConvertTo-ServiceStartModeText -StartMode $Service.StartMode switch ("$($Service.State)") { 'Running' { return New-Finding -Category Apps -Check $check -Severity OK ` -Value ((Get-Text 'Value.Service.Running') -f $mode) } 'Stopped' { if ($Optional) { return New-Finding -Category Apps -Check $check -Severity INFO ` -Value ((Get-Text 'Value.Service.Stopped') -f $mode) } if ($Service.StartMode -eq 'Auto') { # Meant to be running and is not: whatever depends on it is failing now. return New-Finding -Category Apps -Check $check -Severity FAIL ` -Value ((Get-Text 'Value.Service.StoppedExit') -f $mode, $Service.ExitCode) ` -Hint (Get-Text 'Hint.Service.AutomaticButStopped') } if ($Service.StartMode -eq 'Disabled') { return New-Finding -Category Apps -Check $check -Severity WARN ` -Value ((Get-Text 'Value.Service.Stopped') -f $mode) ` -Hint (Get-Text 'Hint.Service.Disabled') } # Started on demand, by a trigger or by the application itself. Stopped is its # resting state, and a Report that called it a problem would be crying wolf. return New-Finding -Category Apps -Check $check -Severity INFO ` -Value ((Get-Text 'Value.Service.Stopped') -f $mode) } default { # Start Pending, Stop Pending, Paused: a service that stays in one of these for # the length of a Run is hung, and the application waiting on it is too. return New-Finding -Category Apps -Check $check -Severity WARN ` -Value ((Get-Text 'Value.Service.OtherState') -f $Service.State, $mode) ` -Hint (Get-Text 'Hint.Service.Stuck') } } } function ConvertTo-ServiceStartModeText { [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$StartMode) switch ($StartMode) { 'Auto' { return (Get-Text 'Value.Service.StartMode.Auto') } 'Manual' { return (Get-Text 'Value.Service.StartMode.Manual') } 'Disabled' { return (Get-Text 'Value.Service.StartMode.Disabled') } } "$StartMode" } function New-ServiceCrashFinding { [CmdletBinding()] param( [AllowNull()]$Data, [AllowNull()][AllowEmptyCollection()][object[]]$Service = @(), [hashtable]$Parameters ) $warn = Get-Parameter $Parameters 'ServiceCrashWarnAbove' 0 $fail = Get-Parameter $Parameters 'ServiceCrashFailAbove' 3 if (-not @($Service).Count) { return } $check = Get-Text 'Check.Service.Crashes' if (-not (Get-DataProperty $Data 'StabilityObserved')) { return New-Finding -Category Apps -Check $check -Severity INFO ` -Value (Get-Text 'Value.Service.CrashesNotRead') ` -Hint (Get-Text 'Hint.Service.StabilityDidNotRun') } $failures = Get-DataCollection $Data 'ObservedFailures' $days = Get-DataProperty $Data 'StabilityDays' $counted = @(foreach ($s in $Service) { $count = @($failures | Where-Object { "$($_.Service)" -eq $s.DisplayName }).Count if ($count) { [pscustomobject]@{ Name = $s.DisplayName; Count = $count } } }) if (-not $counted.Count) { return New-Finding -Category Apps -Check $check -Severity OK ` -Value ((Get-Text 'Value.Service.NoCrashes') -f $days) } $total = ($counted | Measure-Object Count -Sum).Sum $described = ($counted | ForEach-Object { '{0} {1}x' -f $_.Name, $_.Count }) -join ', ' New-Finding -Category Apps -Check $check -Severity (Get-Severity $total $warn $fail) ` -Value ((Get-Text 'Value.Service.Crashes') -f $described, $days) ` -Hint (Get-Text 'Hint.Service.Crashed') } function ConvertTo-ServiceSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) New-Section -Title (Get-Text 'Title.Service.Services') -Row @( (Get-DataCollection $Data 'Services') | Sort-Object Name -Unique | Select-Object Name, DisplayName, State, StartMode, DelayedAutoStart, StartName, PathName ) } |