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. # # What the Elevated Part gathered is in there too, from the first Check of the Main Part # on. It crossed between two processes as CliXML, so it is read like a fixture and not like # an object this process built: a Kind that is absent was not performed or did not come # back, and every property is asked for with Get-DataProperty and Get-DataCollection. # # One key in there is no Kind: 'AppSelection', the applications chosen for this Run and # performed in it, which the Run puts there before the first Check of the Main Part. See # ConvertTo-AppSelectionObserved. No Kind can take its place: a Kind is a file in # Private/Kinds, and there is none of that name. # # A second is 'RunUser': the account the Run runs as, the one working at the machine, and # whether they differ, which the Run puts there at the same moment. It is how a Check about # the User learns that the Run is not in the User's Session; none compares the two names # itself. See ConvertTo-RunUserObserved. # # 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') # Whether the Judge, and the Section, asked for the Situation. The same rule. JudgeWantsSituation = $judge.Parameters.ContainsKey('Situation') SectionWantsSituation = [bool]($section -and $section.Parameters.ContainsKey('Situation')) } } } 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, under 'AppSelection' what # the Run chose and under 'RunUser' whose Session it is in. Handed only to a # Gatherer that declared -Observed. [AllowNull()][hashtable]$Observed = @{}, # What a Hint may assume about the machine. Handed only to a Judge or a Section # that declared -Situation. [AllowNull()]$Situation ) $name = Get-DataProperty $Definition 'Name' $kind = Get-DataProperty $Definition 'Kind' $parameters = ConvertTo-ParameterHashtable (Get-DataProperty $Definition 'Parameters') if (-not $name) { $name = $kind } # The application an application's Check is about, as its Definition names it. Several # Checks share one: "App: Teams", "App: Teams servers" and "App: Teams cache" are Teams. $app = "$(Get-Parameter $parameters 'App' '')" # 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 (Get-Text 'Value.Kind.SkippedOnRequest' $name) ` -Hint (Get-Text 'Hint.Kind.ThisCheckWasNotPerformed') ` -Unobserved @(Get-SignalOfKind -Kind $kind)) -CheckName $name -App $app } $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 (Get-Text 'Value.Kind.SkippedNeedsVersion' $name $required $ModuleVersion) ` -Hint (Get-Text 'Hint.Kind.UpdateGutcheckToPerformThis') ` -Unobserved @(Get-SignalOfKind -Kind $kind)) -CheckName $name -App $app } $implementation = Get-KindImplementation -Kind $kind if (-not $implementation) { return New-CheckOutcome -Finding (New-Finding -Category Gutcheck -Check $name -Severity INFO ` -Value (Get-Text 'Value.Kind.UnknownKind' $name $kind $ModuleVersion) ` -Hint (Get-Text 'Hint.Kind.UpdateGutcheckTheseCheckDefinitions')) -CheckName $name -App $app } 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 (Get-Text 'Value.Kind.NotPerformed' $name $_.Exception.Message) ` -Hint (Get-Text 'Hint.Kind.TheCheckFoundNothingBecause') ` -Unobserved @(Get-SignalOfKind -Kind $kind)) -CheckName $name -App $app } # 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 } # A Judge should never throw, and one did: on a threshold a Definition gave as text. # That ended the Run, and every Check after it with it. What cannot be judged is one # Finding saying so, like what cannot be gathered - the data is still handed on, and # the Sections and events are each built on their own, so that one failing does not # cost the Report the others. $findings = @() $judgeArguments = @{ Data = $data; Parameters = $parameters } if ($implementation.JudgeWantsSituation) { $judgeArguments['Situation'] = $Situation } try { $findings = @(& $implementation.Judge @judgeArguments) } catch { $findings = @(New-Finding -Category Gutcheck -Check $name -Severity INFO ` -Value (Get-Text 'Value.Kind.NotJudged' $name $_.Exception.Message) ` -Hint (Get-Text 'Hint.Kind.TheCheckFoundNothingBecause') ` -Unobserved @(Get-SignalOfKind -Kind $kind)) } $sections = @() if ($implementation.SectionBuilder) { $sectionArguments = @{ Data = $data } if ($implementation.SectionWantsSituation) { $sectionArguments['Situation'] = $Situation } try { $sections = @(& $implementation.SectionBuilder @sectionArguments) } catch { $sections = @() } } $events = @() if ($implementation.EventBuilder) { try { $events = @(& $implementation.EventBuilder -Data $data) } catch { $events = @() } } New-CheckOutcome -Finding $findings -Section $sections -EventLogEntry $events -Kind $kind -Data $data ` -CheckName $name -App $app } 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, # Which Check this is the outcome of, stamped onto everything it produced. [AllowEmptyString()][string]$CheckName = '', [AllowEmptyString()][string]$App = '' ) [pscustomobject]@{ PSTypeName = 'Gutcheck.CheckOutcome' Kind = $Kind Finding = @($Finding | Where-Object { $_ } | Set-CheckOrigin -CheckName $CheckName -App $App) Section = @($Section | Where-Object { $_ } | Set-CheckOrigin -CheckName $CheckName -App $App -Kind $Kind) Event = @($EventLogEntry | Where-Object { $_ }) Data = $Data } } |