Private/Kind.ps1
|
# The Kind boundary. # # A Kind is the named behaviour a Check Definition selects. Every Kind implements the same # interface, so adding a Check means writing one Gatherer and one Judge and nothing else: # # Get-<Kind>Data -Parameters <hashtable> -> gathered data, no judgement # ConvertTo-<Kind>Finding -Data <object> -Parameters <hashtable> -> Findings, pure # ConvertTo-<Kind>Section -Data <object> -> Sections, optional # ConvertTo-<Kind>Event -Data <object> -> event rows, optional # # A Gatherer may additionally declare -Observed, and is then handed what the Checks before # it gathered, keyed by Kind. This is the whole of how one Check reaches another: the # Gutcheck script let the stability scan populate script-scoped state that the per-app # crash analysis read back, guarded such that reordering the two made the dependent Check # vanish rather than fail. The dependency was real and remains; what changes is that it is # an argument a Gatherer asks for by name, visible in its signature and impossible to # satisfy by accident. # # The Kind vocabulary is closed. It is built once at import from the Kinds this module # ships, and a Definition selects from it by name; it can never widen it. ADR-0001 rests # on a Definition being incapable of introducing behaviour, and resolving a Definition's # string through Get-Command would not be that: a name this module does not implement # falls through to whatever function of that name happens to exist in the session. # # Check Definitions evolve in the Checks Repo independently of the module on the Gallery, # so a Run will meet Definitions it cannot perform. Every such case fails soft: a Finding # that names what was skipped and why, never an exception and never a silent omission. $script:KindVocabulary = @{} function Initialize-KindVocabulary { <# .SYNOPSIS Builds the closed set of Kinds this module implements. Called once, at import. .DESCRIPTION A Kind is shipped as Private/Kinds/<Kind>.ps1 and is registered only when that file defines the Gatherer and Judge the interface requires. A Kind file that defines neither leaves the Kind unregistered, which the Local Definitions test catches. #> [CmdletBinding()] param() $script:KindVocabulary = @{} foreach ($file in Get-ChildItem -Path (Join-Path $PSScriptRoot 'Kinds') -Filter '*.ps1' -File) { $kind = $file.BaseName $judge = Get-Command -Name ('ConvertTo-{0}Finding' -f $kind) -CommandType Function -ErrorAction SilentlyContinue $gatherer = Get-Command -Name ('Get-{0}Data' -f $kind) -CommandType Function -ErrorAction SilentlyContinue if (-not $judge -or -not $gatherer) { continue } $section = Get-Command -Name ('ConvertTo-{0}Section' -f $kind) -CommandType Function -ErrorAction SilentlyContinue $sectionName = $null if ($section) { $sectionName = $section.Name } # Not $event: that is an automatic variable, and assigning to it inside a module # that also loads event-reading Kinds is a collision waiting for a long evening. $eventCommand = Get-Command -Name ('ConvertTo-{0}Event' -f $kind) -CommandType Function -ErrorAction SilentlyContinue $eventName = $null if ($eventCommand) { $eventName = $eventCommand.Name } $script:KindVocabulary[$kind] = [pscustomobject]@{ PSTypeName = 'Gutcheck.KindImplementation' Kind = $kind Gatherer = $gatherer.Name Judge = $judge.Name SectionBuilder = $sectionName EventBuilder = $eventName # Whether this Kind can only be performed with admin rights, declared by the # Kind file as $script:<Kind>NeedsAdmin. A property of the Kind rather than of # a Definition: which rights a reliability counter needs is a fact about # Windows, not something a Customer has an opinion about, and a Definition that # could claim otherwise would be a Definition deciding where code runs. NeedsAdmin = [bool](Get-KindDeclaration -Kind $kind -Name 'NeedsAdmin') # Whether this Gatherer asked to see what earlier Checks gathered. Read off # its signature rather than declared in a list, so a Kind cannot claim the # dependency without taking the argument. WantsObserved = $gatherer.Parameters.ContainsKey('Observed') } } } function Get-KindDeclaration { <# .SYNOPSIS A named fact a Kind file declares about itself, or $null when it declares none. .DESCRIPTION Read rather than invoked, so building the vocabulary stays what its comment says it is: a lookup table, with nothing examined and nothing run. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Kind, [Parameter(Mandatory)][string]$Name ) $variable = Get-Variable -Name ('{0}{1}' -f $Kind, $Name) -Scope Script -ErrorAction SilentlyContinue if (-not $variable) { return $null } $variable.Value } function Get-KindImplementation { <# .SYNOPSIS The commands implementing a Kind, or $null when this module does not implement it. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][AllowEmptyString()][string]$Kind) if (-not $Kind) { return $null } $script:KindVocabulary[$Kind] } function Invoke-CheckDefinition { <# .SYNOPSIS Performs one Check: gather, then judge, and return what came back. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)]$Definition, [Parameter(Mandatory)][version]$ModuleVersion, # Checks the Technician asked this Run not to perform, by Check Name or by Kind. [AllowNull()][AllowEmptyCollection()][string[]]$Skip = @(), # What the Checks before this one gathered, keyed by Kind. Handed only to a # Gatherer that declared -Observed. [AllowNull()][hashtable]$Observed = @{} ) $name = Get-DataProperty $Definition 'Name' $kind = Get-DataProperty $Definition 'Kind' $parameters = ConvertTo-ParameterHashtable (Get-DataProperty $Definition 'Parameters') if (-not $name) { $name = $kind } # A Check the Technician skipped is still a Check the Report has to account for. The # long Checks - the disk write test, the stress test - are the ones most often skipped # and the ones whose silent absence would most look like a clean machine. if (@($Skip) -contains $name -or (@($Skip) -contains $kind -and $kind)) { return New-CheckOutcome -Finding (New-Finding -Category Gutcheck -Check $name -Severity INFO ` -Value ("Check '{0}' was skipped at the Technician's request" -f $name) ` -Hint (Get-Text 'Hint.Kind.ThisCheckWasNotPerformed')) } $required = Get-DataProperty $Definition 'MinimumModuleVersion' if ($required -and [version]$required -gt $ModuleVersion) { return New-CheckOutcome -Finding (New-Finding -Category Gutcheck -Check $name -Severity INFO ` -Value ("Check '{0}' was skipped: it needs Gutcheck {1} and this machine has {2}" -f $name, $required, $ModuleVersion) ` -Hint (Get-Text 'Hint.Kind.UpdateGutcheckToPerformThis')) } $implementation = Get-KindImplementation -Kind $kind if (-not $implementation) { return New-CheckOutcome -Finding (New-Finding -Category Gutcheck -Check $name -Severity INFO ` -Value ("Check '{0}' names the Kind '{1}', which Gutcheck {2} does not implement" -f $name, $kind, $ModuleVersion) ` -Hint (Get-Text 'Hint.Kind.UpdateGutcheckTheseCheckDefinitions')) } try { $arguments = @{ Parameters = $parameters } if ($implementation.WantsObserved) { $arguments['Observed'] = $Observed } $data = & $implementation.Gatherer @arguments } catch { # A Gatherer that could not run is a Finding, not a lost Run: a Check that failed # must be visible in the Report rather than simply absent from it. return New-CheckOutcome -Finding (New-Finding -Category Gutcheck -Check $name -Severity INFO ` -Value ("Check '{0}' could not be performed: {1}" -f $name, $_.Exception.Message) ` -Hint (Get-Text 'Hint.Kind.TheCheckFoundNothingBecause')) } # Which Check produced this data, stamped on the way out the way Set-FindingPrivilege # stamps Privilege onto Findings: the Gatherer cannot know its own Check's Name, # because the Name lives in the Definition and a Kind may be named by several. A later # Check handed this through -Observed can then say where an answer came from. if ($null -ne $data -and $data -is [psobject] -and $data -isnot [hashtable]) { $data | Add-Member -NotePropertyName 'CheckName' -NotePropertyValue $name -Force } $findings = @(& $implementation.Judge -Data $data -Parameters $parameters) $sections = @() if ($implementation.SectionBuilder) { $sections = @(& $implementation.SectionBuilder -Data $data) } $events = @() if ($implementation.EventBuilder) { $events = @(& $implementation.EventBuilder -Data $data) } New-CheckOutcome -Finding $findings -Section $sections -EventLogEntry $events -Kind $kind -Data $data } function New-CheckOutcome { <# .SYNOPSIS What one Check produced: Findings, Sections, events, and what it gathered. .DESCRIPTION Data is carried so a later Check can be handed it by name. It is the Run that hands it over, never the Check that reaches for it, which is the difference between a dependency and the shared mutable state this replaced. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Finding, [AllowNull()][AllowEmptyCollection()]$Section, # Not -Event: $Event is a PowerShell automatic variable, and a parameter of that # name shadows it inside every function that takes one. Named as Write-Report # already names the same rows. [AllowNull()][AllowEmptyCollection()]$EventLogEntry, [AllowEmptyString()][string]$Kind = '', [AllowNull()]$Data ) [pscustomobject]@{ PSTypeName = 'Gutcheck.CheckOutcome' Kind = $Kind Finding = @($Finding | Where-Object { $_ }) Section = @($Section | Where-Object { $_ }) Event = @($EventLogEntry | Where-Object { $_ }) Data = $Data } } |