Private/Kinds/StabilityNetwork.ps1
|
# The tie between a program's crashes and the network: a part of the Stability Kind, and # no Kind. # # What the network Checks before it gathered - the disruptions, what depends on a Server, # the adapters - is copied onto the Stability data by its Gatherer, and held here against # each crash: which Server dropped around it, whether the program depends on that Server, # and how much weight the coincidence carries. # How many seconds after a crash a network disruption may lie and still be held against it # - and, for a program known to be local, before it as well. The SMB client notices a lost # connection when it next needs it, and a program dies when it next needs a page: neither # happens in the second the cable did. $script:StabilityCorrelationWindowSeconds = 120 # How many seconds before a crash a disruption may lie for a program that is not known to # be local. A program on a share runs on with what it has in memory when the connection # drops, and dies when it next needs a page of itself from the share: on the machine this # was built for, four to seven minutes later. On a second machine every one of eleven # crashes followed a drop, by two to twenty-seven minutes - as long as it took the person # at it to do something the program had not yet in memory - and ten minutes saw seven of # them. Half an hour. $script:StabilityCorrelationBeforeSeconds = 1800 # How many seconds after a crash a reconnect to the program's Server may lie and still be # held against it. The access that killed the program is the one Windows re-establishes # the session on: there, twelve to forty-eight seconds after the crash. $script:StabilityCorrelationReconnectSeconds = 120 function Copy-ObservedNetworkDisruption { <# .SYNOPSIS The network disruptions an earlier Check gathered, as this Kind's own data. Pure, and decides nothing. .DESCRIPTION What comes from the Elevated Part crossed between two processes as CliXML, so every property is asked for and none is assumed. Gathered is false when the Kind is not there at all, which is the only thing that tells a Run that could not look from a network that was quiet. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][hashtable]$Observed) $source = Get-ObservedKindData -Observed $Observed -Kind 'NetworkDisruption' [pscustomobject]@{ Gathered = $null -ne $source Disruptions = @(foreach ($row in (Get-DataCollection $source 'Disruptions')) { [pscustomobject]@{ Time = Get-DataProperty $row 'Time' Server = Get-DataProperty $row 'Server' Kind = Get-DataProperty $row 'Kind' Provider = Get-DataProperty $row 'Provider' Id = Get-DataProperty $row 'Id' } }) UnreadableLogs = @((Get-DataCollection $source 'UnreadableLogs') | ForEach-Object { "$_" }) # Every log that Check queried. $null, and not empty, when it did not say: a # NetworkDisruption Kind older than the list, which is then not held against it. ScannedLogs = Copy-ObservedList -InputObject $source -Name 'ScannedLogs' # How far back it looked, and how far back each log still reaches: a crash from # before either has not been held against anything. Days = Get-DataProperty $source 'Days' Since = Get-DataProperty $source 'Since' LogCoverage = @(foreach ($row in (Get-DataCollection $source 'LogCoverage')) { [pscustomobject]@{ Log = "$(Get-DataProperty $row 'Log')" Oldest = Get-DataProperty $row 'Oldest' } }) } } function Copy-ObservedNetworkDependency { <# .SYNOPSIS The inventory of what depends on which Server, and the drive mapping it was resolved with, as this Kind's own data. Pure, and decides nothing. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][hashtable]$Observed) $source = Get-ObservedKindData -Observed $Observed -Kind 'NetworkDependency' [pscustomobject]@{ Gathered = $null -ne $source Dependencies = @(foreach ($row in (Get-DataCollection $source 'Dependencies')) { [pscustomobject]@{ Application = Get-DataProperty $row 'Application' Path = Get-DataProperty $row 'Path' UncPath = Get-DataProperty $row 'UncPath' Server = Get-DataProperty $row 'Server' Kind = Get-DataProperty $row 'Kind' Source = Get-DataProperty $row 'Source' } }) DriveMappings = @(foreach ($row in (Get-DataCollection $source 'DriveMappings')) { [pscustomobject]@{ LocalPath = Get-DataProperty $row 'LocalPath' RemotePath = Get-DataProperty $row 'RemotePath' } }) # The drive letters that are disks of this machine. $null, and not empty, when # the inventory did not say: a drive letter that is neither mapped nor listed # here is then taken for local, as it was before anybody listed them. LocalDrives = Copy-ObservedList -InputObject $source -Name 'LocalDrives' } } function Copy-ObservedAdapter { <# .SYNOPSIS Which of the machine's adapters are on USB and what their power saving is set to, as this Kind's own data. Pure, and decides nothing. .DESCRIPTION From the Network Check, which lists the adapters that are up. A fact that Check did not gather - a Network Kind older than the adapter facts - stays unknown here and does not become "no". #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][hashtable]$Observed) $source = Get-ObservedKindData -Observed $Observed -Kind 'Network' [pscustomobject]@{ Gathered = $null -ne $source Adapters = @(foreach ($row in (Get-DataCollection $source 'Adapters')) { [pscustomobject]@{ Name = Get-DataProperty $row 'Name' Description = Get-DataProperty $row 'Description' IsUsb = Get-DataProperty $row 'IsUsb' PowerSavingOn = Get-DataProperty $row 'PowerSavingOn' } }) UsbSelectiveSuspend = Get-DataProperty $source 'UsbSelectiveSuspend' } } function Get-CorrelationSetting { <# .SYNOPSIS What a crash is held against: how far before and after it a network disruption may lie, which disruptions count, what counts as a reconnect and how long after, and which logs name the Server. Pure. .DESCRIPTION All of them are Check Definition parameters. Which events are a disruption at all is the NetworkDisruption Check's Definition; which of those can end a program is asked here, and is not the same question: the Wi-Fi leaving a network is context in a list of what the network did, and the end of a program that ran from a share. Three windows. WindowSeconds (CorrelationWindowSeconds) is how long after a crash a disruption counts, and for a program known to be local how long before it as well. BeforeSeconds (CorrelationBeforeSeconds) is how long before a crash it counts for every other program: the connection drops, and the program dies minutes later. ReconnectSeconds (CorrelationReconnectSeconds) is how long after a crash a reconnect counts, which is one of ReconnectKinds. The logs are the SMB client's, which name the Server a connection was lost to. With one of them not read, a crash that matches nothing has not been cleared. A window that is no number, or a list that names nothing, is the Kind's own: a Definition is a document somebody edits, and a slip in it must neither throw nor switch the question off. #> [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $window = Get-NumberParameter $Parameters 'CorrelationWindowSeconds' $script:StabilityCorrelationWindowSeconds if ($window -lt 0) { $window = $script:StabilityCorrelationWindowSeconds } $before = Get-NumberParameter $Parameters 'CorrelationBeforeSeconds' $script:StabilityCorrelationBeforeSeconds if ($before -lt 0) { $before = $script:StabilityCorrelationBeforeSeconds } $reconnect = Get-NumberParameter $Parameters 'CorrelationReconnectSeconds' $script:StabilityCorrelationReconnectSeconds if ($reconnect -lt 0) { $reconnect = $script:StabilityCorrelationReconnectSeconds } # What says that a connection to a Server came back. Context in the list of what the # network did, and evidence when it follows a crash: see Select-IncidentReconnect. $reconnectKinds = Get-ListParameter $Parameters 'ReconnectKinds' @('smb-reconnect') # An empty list is not "nothing counts": with no kind to hold a crash against, every # program would read as cleared. It is a Definition that left the list out. # A refused sign-in or a refused share is not in here: the Server answered, and said # no. On a domain PC one scheduled job asking for a share it may not read logs that # twice a day, and counted, it stood next to every crash that happened at that hour. $kinds = Get-ListParameter $Parameters 'CorrelationKinds' @( 'smb-disconnect', 'smb-timeout', 'smb-connect-failed', 'smb-handle-lost', 'link-down', 'network-disconnected', 'wlan-disconnect', 'adapter-reset') # What can be held against a program that is known to be local: the machine losing # its network, and nothing else. That some Server did not answer says nothing about a # program that asks no Server for anything. $localKinds = Get-ListParameter $Parameters 'LocalCorrelationKinds' @( 'link-down', 'network-disconnected', 'wlan-disconnect', 'adapter-reset') $logs = Get-ListParameter $Parameters 'ServerEventLogs' @( 'Microsoft-Windows-SmbClient/Connectivity', 'Microsoft-Windows-SMBClient/Operational', 'Microsoft-Windows-SmbClient/Security') [pscustomobject]@{ WindowSeconds = $window BeforeSeconds = $before ReconnectSeconds = $reconnect Kinds = @($kinds) LocalKinds = @($localKinds) ReconnectKinds = @($reconnectKinds) ServerLogs = @($logs) } } function Get-ProgramNetwork { <# .SYNOPSIS Where a program runs from, as far as the network is concerned: each of its paths with the share and the Server behind it, and the Servers it depends on. Pure. .DESCRIPTION A drive letter is resolved with the mapping the NetworkDependency Check gathered, in the session of the person whose drives they are; a UNC path names its Server itself. A path the Gatherer found to be on the network and no mapping explains is on the network all the same, on a Server nobody can name. A drive letter that is neither mapped nor one of the machine's disks, where the inventory listed those, is not known to be either: the Main Part started elevated does not see the drives of the person at the desk. Such a path is placed nowhere, and UnknownDrives names its letter. The Servers are those of the program's own paths and of what the inventory lists for it as a program - a module it loads from a share is the program, too. .PARAMETER NetworkDependency This Kind's copy of what the NetworkDependency Check gathered. May be absent. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowEmptyString()][string]$App = '', [AllowNull()][AllowEmptyCollection()]$Incident, [AllowNull()]$NetworkDependency ) $mapping = Get-DataCollection $NetworkDependency 'DriveMappings' $servers = @() # The disks of this machine, where the inventory listed them. Only then can a drive # letter be neither: not mapped, not a disk, and so not known to be either. $disksListed = Test-DataCarried -InputObject $NetworkDependency -Name 'LocalDrives' $disks = @((Get-DataCollection $NetworkDependency 'LocalDrives') | ForEach-Object { if ("$_" -match '^\s*([A-Za-z])') { '{0}:' -f $Matches[1].ToUpperInvariant() } }) $unplaced = @() # A program can have been started from more than one place in a month. $paths = @() $remote = @{} foreach ($row in @($Incident | Where-Object { $null -ne $_ })) { $path = "$(Get-DataProperty $row 'AppPath')".Trim() if (-not $path) { continue } if ($paths -notcontains $path) { $paths += $path; $remote[$path] = $false } if ([bool](Get-DataProperty $row 'Remote')) { $remote[$path] = $true } } $locations = @(foreach ($path in $paths) { $unc = "$(ConvertTo-NetworkDependencyUncPath -Path $path -DriveMapping $mapping)" $share = '' $server = '' if ($unc) { $parts = Split-NetworkDependencyUncPath -UncPath $unc if ($parts) { $share = "$($parts.Share)"; $server = "$($parts.Server)" } } $drive = '' if ($path -match '^([A-Za-z]):') { $drive = '{0}:' -f $Matches[1].ToUpperInvariant() } if ($server -and -not @($servers | Where-Object { Test-ServerNameMatch -Name $_ -Other $server }).Count) { $servers += $server } # Network: a UNC path, a mapped drive, or what the Gatherer found to be remote. # Local: a disk of this machine. Unknown: a drive letter that is neither - the # drive of somebody whose session this Part did not run in, or one no longer # connected. Where nobody listed the disks, a drive letter is local, as it was. $placement = 'Local' if ($remote[$path] -or $unc) { $placement = 'Network' } elseif ($disksListed -and $drive -and $disks -notcontains $drive) { $placement = 'Unknown' if ($unplaced -notcontains $drive) { $unplaced += $drive } } [pscustomobject]@{ Path = $path Remote = [bool]($placement -eq 'Network') Share = $share Server = $server Drive = $drive Placement = $placement } }) $key = ConvertTo-ProgramKey $App $dependencies = @((Get-DataCollection $NetworkDependency 'Dependencies') | Where-Object { $key -and "$(Get-DataProperty $_ 'Kind')" -eq 'program' -and (ConvertTo-ProgramKey (Get-DataProperty $_ 'Application')) -eq $key }) foreach ($dependency in $dependencies) { $server = "$(Get-DataProperty $dependency 'Server')".Trim() if ($server -and -not @($servers | Where-Object { Test-ServerNameMatch -Name $_ -Other $server }).Count) { $servers += $server } } # The network places the program or a module of it lives in, as a Technician names # them: the share, or the path as the event wrote it when nothing says which share. $places = @() foreach ($location in @($locations | Where-Object { $_.Remote })) { $place = $location.Path if ($location.Share) { $place = $location.Share } if ($places -notcontains $place) { $places += $place } } foreach ($dependency in $dependencies) { $place = "$(Get-DataProperty $dependency 'UncPath')".Trim() $parts = Split-NetworkDependencyUncPath -UncPath $place if ($parts) { $place = "$($parts.Share)" } if (-not $place) { $place = "$(Get-DataProperty $dependency 'Server')".Trim() } if ($place -and $places -notcontains $place) { $places += $place } } $inventoried = [bool](Get-DataProperty $NetworkDependency 'Gathered') [pscustomobject]@{ Locations = $locations Dependencies = $dependencies # Whether the program itself runs from the network, which is what decides whether # a disruption to another Server can be left out. OnNetwork = [bool]@($locations | Where-Object { $_.Remote }).Count Servers = [string[]]$servers Places = [string[]]$places # Whether anybody took the inventory. Without it, a program whose own path is # local has not been cleared: nobody looked at what it loads. Inventoried = $inventoried # The drive letters of its paths that are neither mapped nor a disk of this # machine. Where there is one, nothing says whether the program is on the network. UnknownDrives = [string[]]$unplaced # Whether the program is known to be a local one: nothing of it on the network, # no drive that could not be placed, and either a path of its own or an inventory # that would have listed it. What a chance coincidence is measured by - see # Test-CorrelationWeight. KnownLocal = [bool](-not $places.Count -and -not $unplaced.Count -and ($locations.Count -or $inventoried)) } } function Test-ServerNameMatch { <# .SYNOPSIS Whether two names are the same Server. Pure. .DESCRIPTION A drive is mapped to \\FS01 and the SMB client logs fs01.example.internal: one Server. A short name is the first label of a full one. Two full names are the same only when all of each is, and an address is only itself - nothing here can know which name it has. True only where the names say so. That two names are not known to be the same Server does not make them two Servers: see Compare-ServerName. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()][AllowEmptyString()][string]$Name, [AllowNull()][AllowEmptyString()][string]$Other) (Compare-ServerName -Program $Name -Named $Other) -eq 'Same' } function ConvertTo-ServerNameKey { <# .SYNOPSIS A Server's name as something to compare by: no leading backslashes, no brackets around an address, no trailing dot, lower case. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Name) "$Name".Trim().TrimStart('\').Trim('[', ']').TrimEnd('.').ToLowerInvariant() } function Test-ServerAddress { <# .SYNOPSIS Whether a Server is named by its address and not by a name. Pure. .DESCRIPTION IPv4 as four numbers; IPv6 by its colons, or as the name a UNC path writes it by, which may hold none: fe80--1.ipv6-literal.net. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()][AllowEmptyString()][string]$Name) $key = ConvertTo-ServerNameKey $Name [bool]($key -match '^\d{1,3}(\.\d{1,3}){3}$' -or $key -match ':' -or $key -match '\.ipv6-literal\.net$') } function Compare-ServerName { <# .SYNOPSIS Whether the Server an event names is the Server a program runs from: Same, Different, or Unknown. Pure. .DESCRIPTION Three answers, because two names that differ are not two Servers. A drive mapped to \\corp.example\dfs\apps runs from whichever Server the namespace hands out, and the SMB client logs that Server's own name; a drive mapped to an address runs from a Server whose name nothing here can know. Only Different lets a disruption be left out, so only what the names can really tell apart is Different. the program's Server the event's Server answer ------------------------- --------------------------------- --------- any the same, whatever the case Same an address anything else Unknown any an address, and not the same one Unknown one label FS01 one label, another NAS02 Different one label FS01 full, first label FS01 Same one label CORP full, a later label CORP Unknown (fs01.corp.example: CORP may be the short name of the domain, and \\CORP\dfs a namespace in it) one label FS01 full, no label FS01 Different several labels fs01.a.b one label, FS01 Same several labels anything else Unknown (corp.example may be a domain and its namespace, behind which any Server can stand - and nothing tells fs01.corp.example from a domain of that name) Not symmetric, and not meant to be: it is the program's name that may stand for more than one Server. What stays out of reach: a namespace or an alias with a name of one label. A drive mapped to \\DFS01\root that is served by FS01 reads as Different, and nothing in two names says otherwise. .PARAMETER Program The Server as the program's path names it. .PARAMETER Named The Server as the event names it. #> [CmdletBinding()] [OutputType([string])] param( [AllowNull()][AllowEmptyString()][string]$Program, [AllowNull()][AllowEmptyString()][string]$Named ) $mine = ConvertTo-ServerNameKey $Program $theirs = ConvertTo-ServerNameKey $Named if (-not $mine -or -not $theirs) { return 'Unknown' } if ($mine -eq $theirs) { return 'Same' } if ((Test-ServerAddress $mine) -or (Test-ServerAddress $theirs)) { return 'Unknown' } $mineLabels = @($mine -split '\.') $theirsLabels = @($theirs -split '\.') if ($mineLabels.Count -gt 1) { if ($theirsLabels.Count -eq 1 -and $theirsLabels[0] -eq $mineLabels[0]) { return 'Same' } return 'Unknown' } if ($theirsLabels[0] -eq $mine) { return 'Same' } if ($theirsLabels.Count -gt 1 -and $theirsLabels -contains $mine) { return 'Unknown' } 'Different' } function Get-ServerAttribution { <# .SYNOPSIS Whether the Server an event names is one the program runs from: Same, Unknown, Different - or empty where no Server is named on one side or the other. Pure. .DESCRIPTION Same when it is any of the program's Servers, Unknown when it may be one of them, and Different only when Compare-ServerName tells it apart from every one of them. #> [CmdletBinding()] [OutputType([string])] param( [AllowNull()][AllowEmptyCollection()][string[]]$Server = @(), [AllowNull()][AllowEmptyString()][string]$Named ) $servers = @($Server | Where-Object { $_ }) $name = "$Named".Trim() if (-not $name -or -not $servers.Count) { return '' } $answers = @($servers | ForEach-Object { Compare-ServerName -Program $_ -Named $name }) if ($answers -contains 'Same') { return 'Same' } if ($answers -contains 'Unknown') { return 'Unknown' } 'Different' } function Select-IncidentDisruption { <# .SYNOPSIS The network disruptions that are held against one incident. Pure. .DESCRIPTION One of the kinds that count, no further after the incident than the window and no further before it than the window before, and not about somebody else's Server: a disruption that names a Server other than the program's is left out, one that names the program's or none is not. A link that went down names no Server and took every one of them with it. A program that runs from no Server is held against every disruption in the window. Left out is only what Compare-ServerName calls Different. A disruption to a Server that may or may not be the program's - the drive is mapped to a namespace or to an address - is held against the incident, and says so: each row returned is a copy that carries Attribution, which is Same, Unknown, or empty where no Server was named on one side or the other. It carries Offset as well: the seconds it lay from the incident, negative before it. .PARAMETER Server The Servers the program runs from. Empty for a local program, and for one on a Server nobody could name. .PARAMETER WindowSeconds How long after the incident a disruption counts. .PARAMETER BeforeSeconds How long before the incident it counts. Left out, the same as after: one window either side, which is what a program known to be local is held against. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Incident, [AllowNull()][AllowEmptyCollection()]$Disruption, [AllowNull()][AllowEmptyCollection()][string[]]$Server = @(), [double]$WindowSeconds = $script:StabilityCorrelationWindowSeconds, [double]$BeforeSeconds = -1, [AllowNull()][AllowEmptyCollection()][string[]]$Kind = @() ) $time = ConvertTo-DataTime (Get-DataProperty $Incident 'Time') if ($null -eq $time) { return } if ($BeforeSeconds -lt 0) { $BeforeSeconds = $WindowSeconds } foreach ($row in @($Disruption)) { if ($null -eq $row) { continue } if (@($Kind) -notcontains "$(Get-DataProperty $row 'Kind')") { continue } $at = ConvertTo-DataTime (Get-DataProperty $row 'Time') if ($null -eq $at) { continue } $offset = ($at - $time).TotalSeconds if ($offset -gt $WindowSeconds -or $offset -lt -$BeforeSeconds) { continue } $attribution = Get-ServerAttribution -Server $Server -Named "$(Get-DataProperty $row 'Server')" if ($attribution -eq 'Different') { continue } [pscustomobject]@{ Time = Get-DataProperty $row 'Time' Server = Get-DataProperty $row 'Server' Kind = Get-DataProperty $row 'Kind' Provider = Get-DataProperty $row 'Provider' Id = Get-DataProperty $row 'Id' Attribution = $attribution Offset = $offset } } } function Select-IncidentReconnect { <# .SYNOPSIS The reconnects that are held against one incident: a connection to the program's Server that came back shortly after it. Pure. .DESCRIPTION A program on a share whose connection dropped runs on with what it has in memory, and dies when it next needs a page of itself from the share. That access is the one Windows re-establishes the session on, so the reconnect is logged seconds after the crash - and often it is all that is logged near it, the drop lying minutes back. Only after the incident, and no further than the seconds given. A reconnect at or before it is not evidence by itself: it says that an earlier drop was repaired. The Server is matched as Select-IncidentDisruption matches it: the program's, or one that may be the program's, and any Server for a program that runs from none anybody could name. But a reconnect that names no Server is left out, where a disruption that names none is not: a link that went down took every Server with it, and a reconnect to nobody says nothing. Each row returned is a copy that carries Attribution and Offset, as there. .PARAMETER Seconds How long after the incident a reconnect counts. .PARAMETER Kind The kinds that are a reconnect. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Incident, [AllowNull()][AllowEmptyCollection()]$Disruption, [AllowNull()][AllowEmptyCollection()][string[]]$Server = @(), [double]$Seconds = $script:StabilityCorrelationReconnectSeconds, [AllowNull()][AllowEmptyCollection()][string[]]$Kind = @() ) $time = ConvertTo-DataTime (Get-DataProperty $Incident 'Time') if ($null -eq $time) { return } foreach ($row in @($Disruption)) { if ($null -eq $row) { continue } if (@($Kind) -notcontains "$(Get-DataProperty $row 'Kind')") { continue } $named = "$(Get-DataProperty $row 'Server')".Trim() if (-not $named) { continue } $at = ConvertTo-DataTime (Get-DataProperty $row 'Time') if ($null -eq $at) { continue } $offset = ($at - $time).TotalSeconds if ($offset -le 0 -or $offset -gt $Seconds) { continue } $attribution = Get-ServerAttribution -Server $Server -Named $named if ($attribution -eq 'Different') { continue } [pscustomobject]@{ Time = Get-DataProperty $row 'Time' Server = Get-DataProperty $row 'Server' Kind = Get-DataProperty $row 'Kind' Provider = Get-DataProperty $row 'Provider' Id = Get-DataProperty $row 'Id' Attribution = $attribution Offset = $offset } } } function Get-DisruptionCoverage { <# .SYNOPSIS What the gathered disruptions cover: which of the logs that name a Server were not read, and from when on the ones that were read can say anything. Pure. .DESCRIPTION A log is not read when it was refused, and when the NetworkDisruption Check never queried it - the Definition of this Check names a log that Check's Definition does not. Where that Check did not say which logs it queried, older data, only a refusal is known. Start is the later of the time that Check looked back to and the oldest record the newest-starting of those logs still holds: the SMB client's logs are small, and on a machine that uses its shares they reach back days where the Application log reaches back months. Neither is measured from now - a fixture is judged the same next year. $null where the data says neither, and then nothing is known to lie before it. A log that is empty has no oldest record, and sets no Start: whether it is empty because nothing happened or because it was cleared yesterday is not in the data. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$NetworkDisruption, [AllowNull()][AllowEmptyCollection()][string[]]$ServerLog = @() ) $logs = @($ServerLog | Where-Object { $_ }) $unreadable = @((Get-DataCollection $NetworkDisruption 'UnreadableLogs') | ForEach-Object { "$_".Trim() }) $refused = @($logs | Where-Object { $unreadable -contains $_ }) $unscanned = @() if (Test-DataCarried -InputObject $NetworkDisruption -Name 'ScannedLogs') { $scanned = @((Get-DataCollection $NetworkDisruption 'ScannedLogs') | ForEach-Object { "$_".Trim() }) $unscanned = @($logs | Where-Object { $unreadable -notcontains $_ -and $scanned -notcontains $_ }) } $start = ConvertTo-DataTime (Get-DataProperty $NetworkDisruption 'Since') foreach ($row in (Get-DataCollection $NetworkDisruption 'LogCoverage')) { $log = "$(Get-DataProperty $row 'Log')".Trim() if (-not $log -or $logs -notcontains $log -or $refused -contains $log -or $unscanned -contains $log) { continue } $oldest = ConvertTo-DataTime (Get-DataProperty $row 'Oldest') if ($null -ne $oldest -and ($null -eq $start -or $oldest -gt $start)) { $start = $oldest } } [pscustomobject]@{ Refused = [string[]]$refused Unscanned = [string[]]$unscanned Start = $start } } function Test-CorrelationWeight { <# .SYNOPSIS Whether the incidents that coincided with a network disruption are enough to hold against the network. Pure. .DESCRIPTION For a program on the network: as many as the Check Definition asks for, one by default. Such a program dies of a single lost connection. For a program known to be local that is not enough. A service that crashes twenty-eight times in a month will crash once within two minutes of a Wi-Fi disconnect by chance, and that one coincidence is not a reason to send a Technician to the network first. There the incidents that coincided must also be a share of those that could be looked at: half by default, and a Check Definition parameter. #> [CmdletBinding()] [OutputType([bool])] param( [double]$Correlated, [double]$Total, [bool]$KnownLocal, [hashtable]$Parameters = @{} ) $needed = Get-NumberParameter $Parameters 'CorrelatedIncidents' 1 if ($Correlated -le 0 -or $Correlated -lt $needed) { return $false } if (-not $KnownLocal) { return $true } $percent = Get-NumberParameter $Parameters 'LocalCorrelationPercent' 50 ($Correlated * 100) -ge ($percent * $Total) } function Get-ProgramCorrelation { <# .SYNOPSIS How many of a program's incidents coincided with a network disruption. Pure. .DESCRIPTION An incident coincides when a disruption that counts lies in its window, or a reconnect to the program's Server follows it shortly. The window reaches further back than forward for a program that is not known to be local: its connection drops, and it dies minutes later. See Get-CorrelationSetting. A program known to be local is held against the machine losing its network, in one window either side, and against no reconnect: that a connection to a Server came back says nothing about a program that asks no Server for anything. Three answers, and State says which: Correlated some incidents had one NotCorrelated disruptions were gathered, they cover the incidents, and none had one. This clears the network, which is worth as much as the other answer - and is said only where it is true. NotAssessed nobody could look, and Reason says why not: NotGathered no disruptions were gathered LogsUnreadable a log that names a Server was refused, and nothing that could be read matched LogsNotScanned a log that names a Server was never queried, and nothing that was matched NotCovered every incident lies before the logs begin Untimed no incident has a time to hold anything against Not the same as the answer before it, and never to be read as it. One log refused is enough: a disruption to the Server is written to one of them, and nothing says to which. With a log unread and something matching all the same - the link went down, which a Run without admin rights can read - the incidents are counted, and LogsUnread says that more may have been missed. Total is the number of incidents that could be held against anything, and the number every count is "of". An incident whose window the logs do not reach back to the start of, or without a time, is not among them unless something was found for it all the same: Before and Untimed count those, and Incidents counts all. It is the start of the window that must be covered and not the incident: a connection that dropped seven minutes before a crash is otherwise in a part of the log that is gone, and the crash would read as cleared. What was found is counted apart, because the Finding says which it was: DroppedBefore is the incidents with a disruption at or before them, DroppedAfter those with one after them, and Reconnected those with a reconnect after them. An incident can be in all three, and is in Correlated once. Unattributed names the Servers of disruptions and reconnects that were counted though nothing says they are the program's: see Compare-ServerName. Disruptions holds, for each incident in the order handed over, the disruptions held against it, as Found, the reconnects after it, as Reconnects, and whether it could be held against anything, as Covered. .PARAMETER Network What Get-ProgramNetwork returned for the program. .PARAMETER NetworkDisruption This Kind's copy of what the NetworkDisruption Check gathered. May be absent. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Incident, [AllowNull()]$Network, [AllowNull()]$NetworkDisruption, [hashtable]$Parameters = @{} ) $setting = Get-CorrelationSetting -Parameters $Parameters $incidents = @($Incident | Where-Object { $null -ne $_ }) $gathered = [bool](Get-DataProperty $NetworkDisruption 'Gathered') $rows = Get-DataCollection $NetworkDisruption 'Disruptions' $onNetwork = [bool](Get-DataProperty $Network 'OnNetwork') $servers = @() if ($onNetwork) { $servers = (Get-DataCollection $Network 'Servers') } $knownLocal = [bool](Get-DataProperty $Network 'KnownLocal') # Older data carries no LocalKinds; then every kind counts, as it did. $countedKinds = $setting.Kinds $localKinds = (Get-DataCollection $setting 'LocalKinds') if ($knownLocal -and $localKinds.Count) { $countedKinds = $localKinds } # A program that is not known to be local is looked at from further back, and for a # reconnect after the crash. A local one is held against one window either side, and # against no reconnect. $windowAfter = $setting.WindowSeconds $windowBefore = $setting.BeforeSeconds $reconnectSeconds = $setting.ReconnectSeconds $reconnectKinds = $setting.ReconnectKinds if ($knownLocal) { $windowBefore = $windowAfter; $reconnectKinds = @() } $coverage = Get-DisruptionCoverage -NetworkDisruption $NetworkDisruption -ServerLog $setting.ServerLogs $logsUnread = [bool]($coverage.Refused.Count -or $coverage.Unscanned.Count) $correlated = 0 $named = 0 $names = @() $unattributed = @() $before = 0 $untimed = 0 $droppedBefore = 0 $droppedAfter = 0 $droppedOnlyAfter = 0 $reconnected = 0 # How long after the connection dropped each crash came, in seconds, and for how many # of them it was back before the crash. Of a program on the network only. $delays = [System.Collections.Generic.List[double]]::new() $restored = 0 $perIncident = [System.Collections.Generic.List[psobject]]::new() foreach ($row in $incidents) { $found = @() $reconnects = @() if ($gathered) { $found = @(Select-IncidentDisruption -Incident $row -Disruption $rows -Server $servers ` -WindowSeconds $windowAfter -BeforeSeconds $windowBefore -Kind $countedKinds) if (@($reconnectKinds).Count) { $reconnects = @(Select-IncidentReconnect -Incident $row -Disruption $rows -Server $servers ` -Seconds $reconnectSeconds -Kind $reconnectKinds) } } $coincides = [bool]($found.Count -or $reconnects.Count) # An incident nothing was found for has only been cleared if somebody could have # found something: it has a time, and the logs reach back to the start of its # window - not merely to the incident, which says nothing of the minutes before. $covered = $true if ($gathered -and -not $coincides) { $time = ConvertTo-DataTime (Get-DataProperty $row 'Time') if ($null -eq $time) { $covered = $false; $untimed++ } elseif ($null -ne $coverage.Start -and $time.AddSeconds(-$windowBefore) -lt $coverage.Start) { $covered = $false; $before++ } } $perIncident.Add([pscustomobject]@{ Found = $found; Reconnects = $reconnects; Covered = $covered }) if (-not $coincides) { continue } $correlated++ $earlier = @($found | Where-Object { $_.Offset -le 0 }).Count $later = @($found | Where-Object { $_.Offset -gt 0 }).Count if ($earlier) { $droppedBefore++ } if ($later) { $droppedAfter++ } if ($later -and -not $earlier) { $droppedOnlyAfter++ } if ($reconnects.Count) { $reconnected++ } # From the drop nearest before the crash. Whether the connection came back in # between is what a Technician does not expect to read: it did, as a rule, within # a second, and the program died all the same. if ($earlier -and $onNetwork) { $nearest = @($found | Where-Object { $_.Offset -le 0 } | Sort-Object Offset -Descending)[0] $delay = -[double]$nearest.Offset $delays.Add($delay) if (@($setting.ReconnectKinds).Count) { $back = @(Select-IncidentDisruption -Incident $row -Disruption $rows -Server $servers ` -WindowSeconds 0 -BeforeSeconds $delay -Kind $setting.ReconnectKinds | Where-Object { "$($_.Server)".Trim() }) if ($back.Count) { $restored++ } } } $here = @() foreach ($disruption in (@($found) + @($reconnects))) { $name = "$($disruption.Server)".Trim().ToUpperInvariant() if (-not $name) { continue } if ($here -notcontains $name) { $here += $name } if ($disruption.Attribution -eq 'Unknown' -and $unattributed -notcontains $name) { $unattributed += $name } } if ($here.Count) { $named++ } foreach ($name in $here) { if ($names -notcontains $name) { $names += $name } } } if ($names.Count -gt 1) { $names = @($names | Sort-Object) } if ($unattributed.Count -gt 1) { $unattributed = @($unattributed | Sort-Object) } $total = $incidents.Count - $before - $untimed $state = 'Correlated' $reason = '' if (-not $gathered) { $state = 'NotAssessed'; $reason = 'NotGathered' } elseif (-not $correlated) { if ($coverage.Refused.Count) { $state = 'NotAssessed'; $reason = 'LogsUnreadable' } elseif ($coverage.Unscanned.Count) { $state = 'NotAssessed'; $reason = 'LogsNotScanned' } elseif ($incidents.Count -and $total -le 0) { $state = 'NotAssessed' $reason = 'Untimed' if ($before) { $reason = 'NotCovered' } } else { $state = 'NotCorrelated' } } [pscustomobject]@{ State = $state Reason = $reason # The incidents that could be held against anything: what every count is "of". Total = $total Incidents = $incidents.Count Correlated = $correlated # What it was that coincided, each counted in incidents: a disruption at or before # the incident, one after it, one after it and none before, a reconnect after it. DroppedBefore = $droppedBefore DroppedAfter = $droppedAfter DroppedOnlyAfter = $droppedOnlyAfter Reconnected = $reconnected # Of a program on the network: for how many incidents the time from the drop to # the crash is known, the shortest, the longest and the middle one of those times # in seconds, and for how many the connection was back before the crash. Delayed = $delays.Count DelayMinSeconds = $(if ($delays.Count) { ($delays | Measure-Object -Minimum).Minimum } else { $null }) DelayMaxSeconds = $(if ($delays.Count) { ($delays | Measure-Object -Maximum).Maximum } else { $null }) DelayMedianSeconds = $(if ($delays.Count) { @($delays | Sort-Object)[[int][math]::Floor(($delays.Count - 1) / 2)] } else { $null }) RestoredBefore = $restored # How many of the correlated incidents had a disruption or a reconnect that named # a Server. Named = $named Servers = [string[]]$names # The Servers among them that nothing says are the program's, and the program's. Unattributed = [string[]]$unattributed ProgramServers = [string[]]@($servers | ForEach-Object { "$_".Trim().ToUpperInvariant() } | Where-Object { $_ }) OnNetwork = $onNetwork KnownLocal = $knownLocal # Whether what coincided is enough to hold against the network. Weighs = [bool]($gathered -and (Test-CorrelationWeight -Correlated $correlated -Total $total ` -KnownLocal $knownLocal -Parameters $Parameters)) LogsUnread = [bool]($gathered -and $logsUnread) RefusedLogs = [string[]]$coverage.Refused UnscannedLogs = [string[]]$coverage.Unscanned # Incidents nothing was found for and nothing could have been: the logs do not # reach back to the start of their window, and without a time. Before = $before Untimed = $untimed CoverageStart = $coverage.Start # The windows this program was held against: after an incident, before it - the # same for a program known to be local - and for a reconnect after it. WindowSeconds = $windowAfter BeforeSeconds = $windowBefore ReconnectSeconds = $reconnectSeconds Disruptions = @($perIncident) } } function Get-CorrelationWindowText { <# .SYNOPSIS A window as a Technician reads it: "2 Minuten", or "2min" in a column heading. Pure. .DESCRIPTION With -BeforeSeconds, and where that is another number than -Seconds, both windows: "10 Minuten vor bis 2 Minuten nach dem Absturz". Where the two are the same it is one window either side, and is named as one. #> [CmdletBinding()] [OutputType([string])] param([double]$Seconds, [switch]$Short, [double]$BeforeSeconds = -1) if ($BeforeSeconds -ge 0 -and [int][math]::Round($BeforeSeconds) -ne [int][math]::Round($Seconds)) { return (Get-Text 'Value.Stability.Window.Asymmetric' (Get-CorrelationWindowText -Seconds $BeforeSeconds) ( Get-CorrelationWindowText -Seconds $Seconds)) } $whole = [int][math]::Round($Seconds) if ($whole -ge 120 -and $whole % 60 -eq 0) { if ($Short) { return (Get-Text 'Value.Stability.Window.ShortMinutes' ($whole / 60)) } return (Get-Text 'Value.Stability.Window.Minutes' ($whole / 60)) } if ($Short) { return (Get-Text 'Value.Stability.Window.ShortSeconds' $whole) } Get-Text 'Value.Stability.Window.Seconds' $whole } function Get-DisruptionColumnHeading { <# .SYNOPSIS The heading of the Section's column of disruptions, which names the window they were looked for in: one where it is the same either side of a crash, both where it is not. Pure. .DESCRIPTION The windows are those of a program that is not known to be local. One that is known to be local stands in the same table and is held against the narrower of the two on both sides. #> [CmdletBinding()] [OutputType([string])] param([double]$Seconds, [double]$BeforeSeconds) $after = Get-CorrelationWindowText -Seconds $Seconds -Short if ([int][math]::Round($BeforeSeconds) -eq [int][math]::Round($Seconds)) { return (Get-Text 'Column.Stability.Incident.Disruption' $after) } Get-Text 'Column.Stability.Incident.DisruptionAsymmetric' (Get-CorrelationWindowText -Seconds $BeforeSeconds -Short) $after } function Get-ProgramCorrelationLine { <# .SYNOPSIS What a program's Finding says about its crashes and the network. Pure. .DESCRIPTION "2 von 8 Vorfaellen mit SMB-Stoerung zu FS01 innerhalb von 10 Minuten vor bis 2 Minuten nach dem Absturz" for a program that runs from FS01. Of a local program the same is said neutrally, and with the one window it is held against: its crashes coincided with a disruption, and nothing says the disruption reached it. Where a reconnect after the crash is among what was found, the line says what was found and in how many incidents: "6 von 8 Vorfaellen mit Netzstoerung zu FS01: Verbindung bis zu 10 Minuten vor dem Absturz abgebrochen (4), innerhalb von 2 Minuten nach dem Absturz wiederhergestellt (6)". An incident whose connection dropped only after the crash is named as that; one where it dropped before as well is counted under "before", once. One line as a rule. A second and a third say how many incidents are not among those counted, and why: they lie before the logs begin, or have no time. The lines are written to the pipeline one by one: collect them with @( ). #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)]$Correlation) # Nothing about the network of a program the network does not concern. if ([bool](Get-DataProperty $Correlation 'SetAside')) { return } $before = ConvertTo-Number (Get-DataProperty $Correlation 'Before') $untimed = ConvertTo-Number (Get-DataProperty $Correlation 'Untimed') $start = Get-DataProperty $Correlation 'CoverageStart' $unread = @((Get-DataCollection $Correlation 'RefusedLogs')) + @((Get-DataCollection $Correlation 'UnscannedLogs')) if ($Correlation.State -eq 'NotAssessed') { switch ($Correlation.Reason) { 'LogsUnreadable' { $line = Get-Text 'Value.Stability.Program.CorrelationLogsUnreadable' if ($unread.Count) { $line = Get-Text 'Value.Stability.Program.LogsNamed' $line ($unread -join ', ') } } 'LogsNotScanned' { $line = Get-Text 'Value.Stability.Program.CorrelationLogsNotScanned' ($unread -join ', ') } 'NotCovered' { $line = Get-Text 'Value.Stability.Program.CorrelationNotCovered' $before $start; $before = 0 } 'Untimed' { $line = Get-Text 'Value.Stability.Program.CorrelationUntimed' $untimed; $untimed = 0 } default { $line = Get-Text 'Value.Stability.Program.CorrelationNotGathered' } } } elseif ($Correlation.State -eq 'NotCorrelated') { $line = Get-Text 'Value.Stability.Program.NotCorrelated' $Correlation.Total } else { # A correlation from before there were two windows carries one, for either side. $after = ConvertTo-Number $Correlation.WindowSeconds $earlier = ConvertTo-Number (Get-DataProperty $Correlation 'BeforeSeconds') if ($null -eq $earlier) { $earlier = $after } $window = Get-CorrelationWindowText -Seconds $after -BeforeSeconds $earlier $servers = @($Correlation.Servers) -join ', ' $count = $Correlation.Correlated $total = $Correlation.Total $reconnected = ConvertTo-Number (Get-DataProperty $Correlation 'Reconnected') if ($reconnected -gt 0) { # What was found, each with the number of incidents it was found for. A # reconnect names its Server, so there is always one to name. $parts = @() $dropped = ConvertTo-Number (Get-DataProperty $Correlation 'DroppedBefore') $later = ConvertTo-Number (Get-DataProperty $Correlation 'DroppedOnlyAfter') if ($dropped -gt 0) { $parts += Get-Text 'Value.Stability.Program.EvidenceDroppedBefore' (Get-CorrelationWindowText -Seconds $earlier) $dropped } if ($later -gt 0) { $parts += Get-Text 'Value.Stability.Program.EvidenceDroppedAfter' (Get-CorrelationWindowText -Seconds $after) $later } $parts += Get-Text 'Value.Stability.Program.EvidenceReconnected' ( Get-CorrelationWindowText -Seconds (ConvertTo-Number (Get-DataProperty $Correlation 'ReconnectSeconds'))) $reconnected $what = $parts -join ', ' if (-not $Correlation.OnNetwork) { $line = Get-Text 'Value.Stability.Program.EvidenceNeutral' $count $total $servers $what } elseif ($Correlation.Named -eq $count) { $line = Get-Text 'Value.Stability.Program.EvidenceServer' $count $total $servers $what } else { $line = Get-Text 'Value.Stability.Program.EvidenceMixed' $count $total $Correlation.Named $servers $what } } elseif (-not $Correlation.OnNetwork) { if ($servers) { $line = Get-Text 'Value.Stability.Program.CorrelatedNeutralServer' $count $total $window $servers } else { $line = Get-Text 'Value.Stability.Program.CorrelatedNeutral' $count $total $window } } elseif (-not $Correlation.Named) { $line = Get-Text 'Value.Stability.Program.CorrelatedLink' $count $total $window } elseif ($Correlation.Named -eq $count) { $line = Get-Text 'Value.Stability.Program.CorrelatedServer' $count $total $servers $window } else { $line = Get-Text 'Value.Stability.Program.CorrelatedMixed' $count $total $window $Correlation.Named $servers } # Counted, and said to be counted without knowing: the drive is mapped to a # namespace or an address, and the event names a Server. $unattributed = Get-DataCollection $Correlation 'Unattributed' if ($unattributed.Count) { $line = Get-Text 'Value.Stability.Program.ServerUnattributed' $line ($unattributed -join ', ') ( (Get-DataCollection $Correlation 'ProgramServers') -join ', ') } if ($Correlation.LogsUnread) { $line = Get-Text 'Value.Stability.Program.CorrelatedPartial' $line } } $line if ($before -gt 0) { Get-Text 'Value.Stability.Program.IncidentsBeforeCoverage' $before $start } if ($untimed -gt 0) { Get-Text 'Value.Stability.Program.IncidentsUntimed' $untimed } # How long after the drop the crashes came: the sentence that makes "the network was # fine when it crashed" and "it is the network" both true. $delayed = ConvertTo-Number (Get-DataProperty $Correlation 'Delayed') if ($Correlation.State -eq 'Correlated' -and $delayed -gt 0) { $shortest = Get-DelayText -Seconds (ConvertTo-Number (Get-DataProperty $Correlation 'DelayMinSeconds')) $longest = Get-DelayText -Seconds (ConvertTo-Number (Get-DataProperty $Correlation 'DelayMaxSeconds')) if ($delayed -eq 1 -or $shortest -eq $longest) { Get-Text 'Value.Stability.Program.DelayOne' $shortest } else { Get-Text 'Value.Stability.Program.Delay' $shortest $longest ( Get-DelayText -Seconds (ConvertTo-Number (Get-DataProperty $Correlation 'DelayMedianSeconds'))) } $back = ConvertTo-Number (Get-DataProperty $Correlation 'RestoredBefore') if ($back -gt 0) { Get-Text 'Value.Stability.Program.RestoredBefore' $back $delayed } } } function Get-DelayText { <# .SYNOPSIS A length of time as a Technician reads it: "45 Sekunden" below two minutes, whole minutes from there. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Seconds) $number = ConvertTo-Number $Seconds if ($null -eq $number -or $number -lt 0) { $number = 0 } if ($number -lt 120) { return (Get-Text 'Value.Stability.Window.Seconds' ([int][math]::Round($number))) } Get-Text 'Value.Stability.Window.Minutes' ([int][math]::Round($number / 60)) } function Get-ProgramCorrelationHint { <# .SYNOPSIS What to do about what Get-ProgramCorrelationLine says. Nothing when the network is cleared. Pure. .DESCRIPTION To look at the network is said where the coincidences are worth it: of a program known to be local, only when they weigh. See Test-CorrelationWeight. Where a reconnect after the crash is among what was found, a Hint ahead of that one says where to look: in the minutes before the crash and not in its second. Why there is the Meaning. See Get-ProgramCorrelationMeaning. #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)]$Correlation, [AllowNull()]$Situation) if ([bool](Get-DataProperty $Correlation 'SetAside')) { return } $reason = "$(Get-DataProperty $Correlation 'Reason')" # What a repeat Run with admin rights would add, wherever something was refused. # Not gathered at all is not a refused prompt alone: the Check may have been left out # of this Run, and "confirm the prompt" is then advice for something else. if ($reason -eq 'NotGathered') { Get-Text 'Hint.Stability.CorrelationNotGathered' } elseif ($reason -eq 'LogsUnreadable' -or (Get-DataCollection $Correlation 'RefusedLogs').Count) { Get-Text 'Hint.Stability.CorrelationRerunWithAdmin' } # Admin rights do not help where nobody asked for the log, or the log is too short. if ((Get-DataCollection $Correlation 'UnscannedLogs').Count) { Get-Text 'Hint.Stability.CorrelationLogsNotScanned' } if ($reason -eq 'NotCovered') { Get-Text 'Hint.Stability.CorrelationNotCovered' } if (Test-CorrelationWorthALook -Correlation $Correlation) { if ((ConvertTo-Number (Get-DataProperty $Correlation 'Reconnected')) -gt 0) { Get-Text 'Hint.Stability.ProgramReconnectedAfter' } Get-StabilityNetworkHint -Kind Coincided -Situation $Situation # The other way out: a program that does not run from the share does not die of # the share going away for a second. if ((ConvertTo-Number (Get-DataProperty $Correlation 'Delayed')) -gt 0) { Get-Text 'Hint.Stability.ProgramRunLocal' } } } function Get-StabilityLinkText { <# .SYNOPSIS What there is to check on this machine's side of its connection, to say in a Hint - or nothing where it is not known what the machine is connected by. Pure. .DESCRIPTION Nothing, and not "cable, docking station, Wi-Fi and switch": where nothing is known the Hints keep the wording they had, which names them all in a sentence of its own. The wording for a machine that may be docked ends on a comma, for the middle of a sentence, and is taken without it. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Situation) $known = Select-SituationText -Situation $Situation -Fact Connection -Text @{ Wired = 'yes'; Wireless = 'yes'; Vpn = 'yes'; Unknown = '' } if (-not $known) { return '' } (Get-SituationLinkText -Situation $Situation).Trim().TrimEnd(',').Trim() } function Get-StabilityNetworkHint { <# .SYNOPSIS What to do where a program's crashes point at the network: the cause, a suspicion, or a disruption that coincided. Naming what this machine is connected by, where that is known. Pure. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)][ValidateSet('Cause', 'Suspected', 'Coincided')][string]$Kind, [AllowNull()]$Situation ) $link = Get-StabilityLinkText -Situation $Situation switch ($Kind) { 'Cause' { if ($link) { return (Get-Text 'Hint.Stability.VerdictNetworkCause.Link') -f $link } return Get-Text 'Hint.Stability.VerdictNetworkCause' } 'Suspected' { if ($link) { return (Get-Text 'Hint.Stability.VerdictNetworkSuspected.Link') -f $link } return Get-Text 'Hint.Stability.VerdictNetworkSuspected' } } if ($link) { return (Get-Text 'Hint.Stability.ProgramCorrelated.Link') -f $link } Get-Text 'Hint.Stability.ProgramCorrelated' } function Get-StabilityUsbAdapterHint { <# .SYNOPSIS What to do about a network adapter on USB: with the docking station, unless the machine is known not to be in one. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Situation) Select-SituationText -Situation $Situation -Fact Docked -Text @{ No = Get-Text 'Hint.Stability.UsbAdapter.NoDock' Unknown = Get-Text 'Hint.Stability.UsbAdapter' } } function Test-CorrelationWorthALook { <# .SYNOPSIS Whether what coincided with a program's crashes is worth a look at the network: of a program known to be local, only when it weighs. Pure. See Test-CorrelationWeight. #> [CmdletBinding()] [OutputType([bool])] param([Parameter(Mandatory)]$Correlation) "$(Get-DataProperty $Correlation 'State')" -eq 'Correlated' -and ([bool](Get-DataProperty $Correlation 'Weighs') -or -not [bool](Get-DataProperty $Correlation 'KnownLocal')) } function Get-ProgramCorrelationMeaning { <# .SYNOPSIS What Get-ProgramCorrelationLine says means, where the line does not say so itself. Nothing when it does. Pure. .DESCRIPTION Two things are not in the line. Why a log was not held against the crashes when nobody asked for it: two Check Definitions that disagree. And what a reconnect after a crash says: the connection had dropped before, and the program died when it next read from the share. Each is said where Get-ProgramCorrelationHint says what to do about it. #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)]$Correlation) if ([bool](Get-DataProperty $Correlation 'SetAside')) { return } if ((Get-DataCollection $Correlation 'UnscannedLogs').Count) { Get-Text 'Meaning.Stability.CorrelationLogsNotScanned' } if (-not (Test-CorrelationWorthALook -Correlation $Correlation)) { return } # Where the time from the drop to the crash is known, that is the whole story and is # told as one; a reconnect after the crash is then one more sign of it and needs no # sentence of its own. if ((ConvertTo-Number (Get-DataProperty $Correlation 'Delayed')) -gt 0) { Get-Text 'Meaning.Stability.ProgramDiesLater' if ((ConvertTo-Number (Get-DataProperty $Correlation 'RestoredBefore')) -gt 0) { Get-Text 'Meaning.Stability.ProgramRestoredNoHelp' } } elseif ((ConvertTo-Number (Get-DataProperty $Correlation 'Reconnected')) -gt 0) { Get-Text 'Meaning.Stability.ProgramReconnectedAfter' } } function Get-IncidentDisruptionText { <# .SYNOPSIS The disruptions held against one incident, and the reconnects after it, as the cell the Section shows. Pure. .DESCRIPTION The kind as the list of disruptions names it, the Server, and how many seconds before or after the crash. "None" only where somebody looked, and could have seen. In the order they happened, and what was logged more than once in a second once: a connection that drops is logged as the connection, the session and the share. The first four are shown - or the first three and the first reconnect after the crash, where that would otherwise be among the ones left out: it is the reconnect that ties the drop minutes before to the crash. #> [CmdletBinding()] [OutputType([string])] param( [AllowNull()]$Incident, [AllowNull()][AllowEmptyCollection()]$Disruption, [Parameter(Mandatory)]$Correlation, # Whether the incident could be held against anything. See Get-ProgramCorrelation. [bool]$Covered = $true, # The reconnects held against the incident. See Select-IncidentReconnect. [AllowNull()][AllowEmptyCollection()]$Reconnect ) $reconnects = @($Reconnect | Where-Object { $null -ne $_ }) $found = @($Disruption | Where-Object { $null -ne $_ }) + $reconnects if (-not $found.Count) { if ($Correlation.State -eq 'NotAssessed' -or $Correlation.LogsUnread -or -not $Covered) { return (Get-Text 'Value.Stability.Incident.DisruptionNotAssessed') } return (Get-Text 'Value.Stability.Incident.DisruptionNone') } $time = ConvertTo-DataTime (Get-DataProperty $Incident 'Time') $seen = @{} $rows = @(for ($index = 0; $index -lt $found.Count; $index++) { $row = $found[$index] # The token is data; what the Report shows is the German written for it. $label = ('{0} {1}' -f (Get-NetworkDisruptionKindText -Kind "$(Get-DataProperty $row 'Kind')"), "$(Get-DataProperty $row 'Server')".Trim().ToUpperInvariant()).Trim() if ("$(Get-DataProperty $row 'Attribution')" -eq 'Unknown') { $label = Get-Text 'Value.Stability.Incident.DisruptionUnattributed' $label } $offset = [int][math]::Round(((ConvertTo-DataTime (Get-DataProperty $row 'Time')) - $time).TotalSeconds) # Once, what was logged more than once in the same second. $key = '{0}|{1}' -f $offset, $label if ($seen.ContainsKey($key)) { continue } $seen[$key] = $true [pscustomobject]@{ Offset = $offset Label = $label # The reconnects stand behind the disruptions in $found. Reconnect = [bool]($index -ge ($found.Count - $reconnects.Count)) } }) # In the order they happened; two in the same second by what they were. if ($rows.Count -gt 1) { $rows = @($rows | Sort-Object @{ Expression = { $_.Offset } }, @{ Expression = { $_.Label } }) } # The first few: a Server that drops logs a handful of events each time. And the # reconnect after the crash, when the first few are all from the minutes before it. $shown = @($rows | Select-Object -First 4) if ($rows.Count -gt 4 -and -not @($shown | Where-Object { $_.Reconnect }).Count) { $back = @($rows | Where-Object { $_.Reconnect } | Select-Object -First 1) if ($back.Count) { $shown = @($rows | Select-Object -First 3) + $back } } $text = @($shown | ForEach-Object { Get-Text 'Value.Stability.Incident.Disruption' $_.Label $_.Offset }) -join ', ' if ($shown.Count -lt $rows.Count) { $text = Get-Text 'Value.Stability.ScatteredMore' $text ($rows.Count - $shown.Count) } $text } function Get-SectionCorrelationParameter { <# .SYNOPSIS The window and the kinds of disruption the Gatherer carried, as the parameters the Judge was given. Pure. .DESCRIPTION A Section is built from data alone. What the Check Definition said about holding crashes against the network travels in the data for that reason, and where it does not - older data - the defaults apply, as they did to the Finding. #> [CmdletBinding()] [OutputType([hashtable])] param([AllowNull()]$Data) $carried = Get-DataProperty $Data 'Correlation' $parameters = @{} $window = ConvertTo-Number (Get-DataProperty $carried 'WindowSeconds') if ($null -ne $window) { $parameters['CorrelationWindowSeconds'] = $window } # Data from before a crash was looked at from further back, and for a reconnect after # it, carries neither: then the Kind's own apply, as they do to the Finding. $before = ConvertTo-Number (Get-DataProperty $carried 'BeforeSeconds') if ($null -ne $before) { $parameters['CorrelationBeforeSeconds'] = $before } $reconnect = ConvertTo-Number (Get-DataProperty $carried 'ReconnectSeconds') if ($null -ne $reconnect) { $parameters['CorrelationReconnectSeconds'] = $reconnect } $kinds = Get-DataCollection $carried 'Kinds' if ($kinds.Count) { $parameters['CorrelationKinds'] = $kinds } $reconnectKinds = Get-DataCollection $carried 'ReconnectKinds' if ($reconnectKinds.Count) { $parameters['ReconnectKinds'] = $reconnectKinds } $logs = Get-DataCollection $carried 'ServerLogs' if ($logs.Count) { $parameters['ServerEventLogs'] = $logs } $parameters } function Get-IncidentDisruptionColumn { <# .SYNOPSIS For every incident, in the order handed over, what the Section shows of the network disruptions around it. Pure. .DESCRIPTION Held against the network program by program, exactly as the Findings are: a disruption to a Server stands beside the crash of a program that runs from it, and not beside that of one that runs from another. #> [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()][AllowEmptyCollection()]$Incident, [AllowNull()]$Data, [hashtable]$Parameters = @{} ) $incidents = @($Incident | Where-Object { $null -ne $_ }) $dependency = Get-DataProperty $Data 'NetworkDependency' $disruption = Get-DataProperty $Data 'NetworkDisruption' $byProgram = @{} for ($index = 0; $index -lt $incidents.Count; $index++) { $key = ConvertTo-ProgramKey (Get-DataProperty $incidents[$index] 'App') if (-not $byProgram.ContainsKey($key)) { $byProgram[$key] = @() } $byProgram[$key] += $index } $cells = @('') * $incidents.Count foreach ($key in @($byProgram.Keys)) { $indexes = @($byProgram[$key]) $rows = @($indexes | ForEach-Object { $incidents[$_] }) $network = Get-ProgramNetwork -App $key -Incident $rows -NetworkDependency $dependency $correlation = Get-ProgramCorrelation -Incident $rows -Network $network -NetworkDisruption $disruption -Parameters $Parameters for ($position = 0; $position -lt $indexes.Count; $position++) { $cells[$indexes[$position]] = Get-IncidentDisruptionText -Incident $rows[$position] ` -Disruption $correlation.Disruptions[$position].Found -Correlation $correlation ` -Covered ([bool]$correlation.Disruptions[$position].Covered) ` -Reconnect $correlation.Disruptions[$position].Reconnects } } , [string[]]$cells } |