Private/Situation.ps1
|
# The Situation: the facts about the Target Machine that a Hint's wording may depend on. # # A machine at a cable was told three times in one Report to "connect by cable to test", # because the Hint was one sentence for every machine, and because three Kinds would each # have had to find out the connection for themselves to do better. The Situation is found # out once, before the first Check, stated in the Report, and handed to whoever asks. What # is not in it, a Hint does not assume. # # Five facts, each one of a few answers or "Unknown". Reading them from Windows is a # Gatherer and is untested like every Gatherer; turning what was read into the five facts # is pure (ADR-0002). $script:SituationValues = [ordered]@{ Connection = @('Wired', 'Wireless', 'Vpn') Form = @('Notebook', 'Desktop', 'Virtual') Docked = @('Yes', 'No') Power = @('Battery', 'Mains') Domain = @('Joined', 'NotJoined') } # The enclosure types of the SMBIOS specification (DMTF DSP0134, System Enclosure or # Chassis Types), as Win32_SystemEnclosure.ChassisTypes reports them. 12 is a docking # station and is reported by some notebooks while docked. $script:SituationNotebookChassis = @(8, 9, 10, 11, 12, 14, 30, 31, 32) $script:SituationDesktopChassis = @(3, 4, 5, 6, 7, 13, 15, 16, 35, 36) function Get-SituationReading { <# .SYNOPSIS Asks Windows what the Situation is made from. Reads, and decides nothing. .DESCRIPTION Every question is asked on its own and may go unanswered: a property that could not be read is left $null, which ConvertTo-Situation reads as "not known". #> [CmdletBinding()] [OutputType([psobject])] param() $reading = [ordered]@{ Adapter = $null; ChassisTypes = $null; PcSystemType = $null; Manufacturer = $null; Model = $null PowerOnline = $null; HasBattery = $null; BatteryStatus = $null DockingState = $null; DockDevicePresent = $null; PartOfDomain = $null; DomainName = $null } # The adapter the default route leaves by: the one the machine reaches everything # else through. try { if (Get-Command -Name Get-NetRoute -ErrorAction SilentlyContinue) { $route = @(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop | Sort-Object { [int]$_.RouteMetric + [int]$_.InterfaceMetric })[0] if ($route) { $adapter = Get-NetAdapter -InterfaceIndex $route.InterfaceIndex -ErrorAction Stop $reading.Adapter = [pscustomobject]@{ PhysicalMediaType = "$($adapter.PhysicalMediaType)" NdisPhysicalMedium = $adapter.NdisPhysicalMedium Virtual = [bool]$adapter.Virtual InterfaceDescription = "$($adapter.InterfaceDescription)" } } } } catch { } try { $reading.ChassisTypes = @((Get-CimInstance Win32_SystemEnclosure -ErrorAction Stop | ForEach-Object { $_.ChassisTypes }) | Where-Object { $null -ne $_ }) } catch { } try { $system = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop $reading.PcSystemType = $system.PCSystemType $reading.Manufacturer = "$($system.Manufacturer)" $reading.Model = "$($system.Model)" $reading.PartOfDomain = [bool]$system.PartOfDomain $reading.DomainName = "$($system.Domain)" } catch { } try { $batteries = @(Get-CimInstance Win32_Battery -ErrorAction Stop) $reading.HasBattery = $batteries.Count -gt 0 if ($batteries.Count) { $reading.BatteryStatus = $batteries[0].BatteryStatus } } catch { } # Says in one word what Win32_Battery says in eleven states. Not on every machine. try { $status = @(Get-CimInstance -Namespace 'root\wmi' -ClassName BatteryStatus -ErrorAction Stop) if ($status.Count) { $reading.PowerOnline = [bool]$status[0].PowerOnline } } catch { } # 1 undocked, 2 docked, 0 "cannot be docked or not known". Written by docking # stations that announce themselves to Windows, which a USB-C hub does not. try { $dock = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\IDConfigDB\CurrentDockInfo' -Name DockingState -ErrorAction Stop $reading.DockingState = $dock.DockingState } catch { } try { $reading.DockDevicePresent = @(Get-CimInstance Win32_PnPEntity -Filter "Name LIKE '%Dock%'" -ErrorAction Stop | Where-Object { $_.Present -and $_.ConfigManagerErrorCode -eq 0 }).Count -gt 0 } catch { } [pscustomobject]$reading } function ConvertTo-Situation { <# .SYNOPSIS What was read from Windows, as the five facts. Pure. .DESCRIPTION "Unknown" is an answer like the others, and the one given whenever the reading does not settle the question: a Hint that takes a guess for a measurement sends a Technician the wrong way with confidence. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Reading) # How the machine is connected, by the adapter of its default route. # An adapter that is neither a cable nor a radio is a VPN or a virtual switch, and is # called Vpn: what carries it underneath is not asked. NdisPhysicalMedium: 9 is native # 802.11, 14 is 802.3. $connection = 'Unknown' $adapterName = '' $adapter = Get-DataProperty $Reading 'Adapter' if ($adapter) { $media = "$(Get-DataProperty $adapter 'PhysicalMediaType')" $medium = ConvertTo-Number (Get-DataProperty $adapter 'NdisPhysicalMedium') $connection = if ($media -match '802\.11|Wireless' -or $medium -eq 9) { 'Wireless' } elseif (-not (Get-DataProperty $adapter 'Virtual') -and ($media -match '802\.3' -or $medium -eq 14)) { 'Wired' } else { 'Vpn' } $adapterName = "$(Get-DataProperty $adapter 'InterfaceDescription')" } # A virtual machine claims whatever enclosure its host gave it. $form = 'Unknown' $maker = '{0} {1}' -f (Get-DataProperty $Reading 'Manufacturer'), (Get-DataProperty $Reading 'Model') $chassis = @((Get-DataCollection $Reading 'ChassisTypes') | ForEach-Object { ConvertTo-Number $_ } | Where-Object { $null -ne $_ }) $pcType = ConvertTo-Number (Get-DataProperty $Reading 'PcSystemType') if ($maker -match '(?i)\bVirtual Machine\b|VMware|VirtualBox|\bQEMU\b|\bKVM\b|\bXen\b|Parallels') { $form = 'Virtual' } elseif (@($chassis | Where-Object { $_ -in $script:SituationNotebookChassis }).Count) { $form = 'Notebook' } elseif (@($chassis | Where-Object { $_ -in $script:SituationDesktopChassis }).Count) { $form = 'Desktop' } # Win32_ComputerSystem.PCSystemType: 1 desktop, 2 mobile, 3 workstation. elseif ($pcType -eq 2) { $form = 'Notebook' } elseif ($pcType -in 1, 3) { $form = 'Desktop' } # Win32_Battery.BatteryStatus: 1, 4 and 5 are a battery being drained; 2 is "has # access to AC", 3 fully charged, 6 to 9 charging. $power = 'Unknown' $online = Get-DataProperty $Reading 'PowerOnline' $status = ConvertTo-Number (Get-DataProperty $Reading 'BatteryStatus') if ($online -is [bool]) { $power = $(if ($online) { 'Mains' } else { 'Battery' }) } elseif ((Get-DataProperty $Reading 'HasBattery') -eq $false) { $power = 'Mains' } elseif ($status -in 1, 4, 5) { $power = 'Battery' } elseif ($status -in 2, 3, 6, 7, 8, 9) { $power = 'Mains' } # Only a notebook is docked. "Not docked" takes Windows saying so: nothing seen is # what a docking station that is a plain USB hub looks like as well. $docked = 'Unknown' $state = ConvertTo-Number (Get-DataProperty $Reading 'DockingState') $seen = Get-DataProperty $Reading 'DockDevicePresent' if ($form -in 'Desktop', 'Virtual') { $docked = 'No' } elseif ($state -eq 2 -or $seen -eq $true) { $docked = 'Yes' } elseif ($state -eq 1 -and $seen -eq $false) { $docked = 'No' } $domain = 'Unknown' $joined = Get-DataProperty $Reading 'PartOfDomain' if ($joined -is [bool]) { $domain = $(if ($joined) { 'Joined' } else { 'NotJoined' }) } [pscustomobject]@{ PSTypeName = 'Gutcheck.Situation' Connection = $connection ConnectionAdapter = $adapterName Form = $form Docked = $docked Power = $power Domain = $domain DomainName = $(if ($domain -eq 'Joined') { "$(Get-DataProperty $Reading 'DomainName')" } else { '' }) } } function Get-SituationRow { <# .SYNOPSIS The Situation as rows of the box at the top of the Report: Group, Name, Value. Pure. .DESCRIPTION All five, always: what could not be determined says so. A Technician reading a Hint has to be able to see what it assumed, and that it assumed nothing. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Situation) if ($null -eq $Situation) { return } $group = Get-Text 'System.Info.Group.Situation' $unknown = Get-Text 'Situation.Unknown' $detail = { param($text, $more) if ("$more".Trim()) { (Get-Text 'Situation.WithDetail') -f $text, "$more".Trim() } else { $text } } $connection = switch ("$(Get-DataProperty $Situation 'Connection')") { 'Wired' { & $detail (Get-Text 'Situation.Connection.Wired') (Get-DataProperty $Situation 'ConnectionAdapter') } 'Wireless' { & $detail (Get-Text 'Situation.Connection.Wireless') (Get-DataProperty $Situation 'ConnectionAdapter') } 'Vpn' { & $detail (Get-Text 'Situation.Connection.Vpn') (Get-DataProperty $Situation 'ConnectionAdapter') } default { $unknown } } $form = switch ("$(Get-DataProperty $Situation 'Form')") { 'Notebook' { Get-Text 'Situation.Form.Notebook' } 'Desktop' { Get-Text 'Situation.Form.Desktop' } 'Virtual' { Get-Text 'Situation.Form.Virtual' } default { $unknown } } $docked = switch ("$(Get-DataProperty $Situation 'Docked')") { 'Yes' { Get-Text 'Situation.Docked.Yes' } 'No' { Get-Text 'Situation.Docked.No' } default { $unknown } } $power = switch ("$(Get-DataProperty $Situation 'Power')") { 'Battery' { Get-Text 'Situation.Power.Battery' } 'Mains' { Get-Text 'Situation.Power.Mains' } default { $unknown } } $domain = switch ("$(Get-DataProperty $Situation 'Domain')") { 'Joined' { & $detail (Get-Text 'Situation.Domain.Joined') (Get-DataProperty $Situation 'DomainName') } 'NotJoined' { Get-Text 'Situation.Domain.NotJoined' } default { $unknown } } [pscustomobject]@{ Group = $group; Name = (Get-Text 'Situation.Connection'); Value = $connection } [pscustomobject]@{ Group = $group; Name = (Get-Text 'Situation.Form'); Value = $form } [pscustomobject]@{ Group = $group; Name = (Get-Text 'Situation.Docked'); Value = $docked } [pscustomobject]@{ Group = $group; Name = (Get-Text 'Situation.Power'); Value = $power } [pscustomobject]@{ Group = $group; Name = (Get-Text 'Situation.Domain'); Value = $domain } } function ConvertTo-SituationDocument { <# .SYNOPSIS The Situation as the text the Elevated Part is handed it in. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Situation) $document = [ordered]@{} foreach ($fact in @($script:SituationValues.Keys) + 'ConnectionAdapter', 'DomainName') { $document[$fact] = "$(Get-DataProperty $Situation $fact)" } $document | ConvertTo-Json } function ConvertFrom-SituationDocument { <# .SYNOPSIS Reads the Situation the Main Part handed over. Pure. .DESCRIPTION The Elevated Part does not determine its own: it runs in another session, behind another logon, and both Parts' Findings are about one machine and have to be worded for the same one. What arrives is text from another process and is held to the permitted answers; anything else is "Unknown", and nothing that is not one of the facts is taken along. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyString()][string]$Text) $read = $null try { if ("$Text".Trim()) { $read = $Text | ConvertFrom-Json -ErrorAction Stop } } catch { $read = $null } $situation = [ordered]@{ PSTypeName = 'Gutcheck.Situation' } foreach ($fact in $script:SituationValues.Keys) { $value = "$(Get-DataProperty $read $fact)" $situation[$fact] = $(if ($value -in $script:SituationValues[$fact]) { @($script:SituationValues[$fact] | Where-Object { $_ -eq $value })[0] } else { 'Unknown' }) } $situation['ConnectionAdapter'] = "$(Get-DataProperty $read 'ConnectionAdapter')" $situation['DomainName'] = "$(Get-DataProperty $read 'DomainName')" [pscustomobject]$situation } function Select-SituationText { <# .SYNOPSIS The wording for what the Situation says of one fact. Pure. .DESCRIPTION The one way a Kind varies a text by the Situation. It takes a wording for each answer the Kind has something particular to say about and insists on one for "Unknown", which is also what is said where the Situation gives an answer the text has no wording for: a Hint that has to guess says what fits every case. #> [CmdletBinding()] [OutputType([string])] param( [AllowNull()]$Situation, [Parameter(Mandatory)][ValidateSet('Connection', 'Form', 'Docked', 'Power', 'Domain')][string]$Fact, [Parameter(Mandatory)][hashtable]$Text ) if (-not $Text.ContainsKey('Unknown')) { throw ("A text that varies by the Situation needs a wording for 'Unknown' ({0})." -f $Fact) } $answer = "$(Get-DataProperty $Situation $Fact)" if ($answer -and $Text.ContainsKey($answer)) { return "$($Text[$answer])" } "$($Text['Unknown'])" } function Get-SituationAdapterName { <# .SYNOPSIS The adapter the machine is connected through, to name in a Hint. Empty where it is not known. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Situation) "$(Get-DataProperty $Situation 'ConnectionAdapter')" } function Get-SituationLinkText { <# .SYNOPSIS What there is to check on this machine's side of a connection, as the middle of a sentence. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Situation) $wired = Select-SituationText -Situation $Situation -Fact Docked -Text @{ Yes = Get-Text 'Situation.Link.WiredDocked' No = Get-Text 'Situation.Link.Wired' Unknown = Get-Text 'Situation.Link.WiredMaybeDocked' } Select-SituationText -Situation $Situation -Fact Connection -Text @{ Wired = $wired Wireless = Get-Text 'Situation.Link.Wireless' Vpn = Get-Text 'Situation.Link.Vpn' Unknown = Get-Text 'Situation.Link.Unknown' } } function Add-SituationPowerNote { <# .SYNOPSIS Says on a measurement that is not in order that it was taken on battery. Pure. .DESCRIPTION Windows holds a machine back on battery, and a slow result then says something about the power plan and nothing about the machine. Said after what the Finding says already, and only where it is WARN or FAIL: there is nothing to qualify about a value that is in order. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(ValueFromPipeline)]$Finding, [AllowNull()]$Situation ) begin { $note = Select-SituationText -Situation $Situation -Fact Power -Text @{ Battery = Get-Text 'Hint.Situation.OnBattery'; Mains = ''; Unknown = '' } } process { if ($null -eq $Finding) { return } if ($note -and "$($Finding.Severity)" -in 'WARN', 'FAIL') { $Finding.Hint = @(@("$($Finding.Hint)", $note) | Where-Object { $_ }) -join ' | ' } $Finding } } function Add-SituationSection { <# .SYNOPSIS Makes sure a Report states the Situation: adds a box for it where no Section at the top carries it already. Pure. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyCollection()]$Section, [AllowNull()]$Situation) $sections = @($Section | Where-Object { $_ }) $group = Get-Text 'System.Info.Group.Situation' $stated = @($sections | Where-Object { "$(Get-DataProperty $_ 'Placement')" -eq 'Top' } | Where-Object { @((Get-DataCollection $_ 'Row') | Where-Object { "$(Get-DataProperty $_ 'Group')" -eq $group }).Count }) if ($stated.Count -or $null -eq $Situation) { return $sections } @(New-Section -Title (Get-Text 'Title.System.Information') -Placement Top -Row @(Get-SituationRow -Situation $Situation)) + $sections } |