Private/Kinds/StabilityIncident.ps1
|
# Crash incidents: a part of the Stability Kind, and no Kind. # # One program dying writes several events - the .NET runtime's, Windows Error Reporting's, # the Application Error - and a hang its own. Here they become the one incident they were, # which every crash Finding counts. # How many seconds apart the events of one program may be and still report one crash. The # .NET runtime logs its 1026 about a second before Windows logs the 1000 for the same # death; ten seconds leaves room for a slow machine and none for a second start. $script:StabilityIncidentWindowSeconds = 10 function Get-IncidentWindowSetting { <# .SYNOPSIS How many seconds apart two events of one program may be and still be one crash, as the Check Definition says or as the Kind does unasked. Pure. #> [CmdletBinding()] [OutputType([double])] param([AllowNull()][hashtable]$Parameters = @{}) $window = Get-NumberParameter $Parameters 'IncidentWindowSeconds' $script:StabilityIncidentWindowSeconds if ($window -lt 0) { return [double]$script:StabilityIncidentWindowSeconds } $window } # A time that arrives as text is read by ConvertTo-DataTime (Private/Data.ps1), which # began here as this Kind's own and is every Kind's now: never by a free parse. function ConvertTo-CrashIncident { <# .SYNOPSIS Merges the event rows of the Application log into the crashes behind them. Pure. .DESCRIPTION One crash is logged up to three times: by the .NET runtime (1026), by Windows (Application Error 1000) and, when the program stopped answering first, as an Application Hang 1002. Events of one program that lie within the window are one incident; an event with no partner is an incident on its own. The window is measured from the first event of an incident, not from the one before: a program that a service restarts every eight seconds crashes many times, and a chain of events must not make one incident of an afternoon. What an incident says comes from the 1000 where there is one - version, exception code, faulting module, program path - and from the 1026 for the exception types and the method. What no event of the incident said is an empty string, or an empty list, so that whoever reads the row need not ask first whether it is there. Whether a path is on the network is not decided here: the Gatherer asked the machine and hands over the paths that are. An event without a program name or without a time cannot be placed beside any other, and is an incident on its own. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Crash, [AllowNull()][AllowEmptyCollection()]$RuntimeFault, [AllowNull()][AllowEmptyCollection()]$Hang, # Untyped: it may come from a Check Definition, and "x" must not throw here. $WindowSeconds = $script:StabilityIncidentWindowSeconds, [AllowNull()][AllowEmptyCollection()][string[]]$RemotePath = @() ) $WindowSeconds = Get-IncidentWindowSetting -Parameters @{ IncidentWindowSeconds = $WindowSeconds } # Not List[object]: @($x) throws on one of those. See Public/Invoke-Gutcheck.ps1. $groups = [System.Collections.Generic.List[psobject]]::new() $placed = @{} $sources = @( @{ Kind = 'Crash'; Rows = $Crash } @{ Kind = 'Fault'; Rows = $RuntimeFault } @{ Kind = 'Hang'; Rows = $Hang } ) foreach ($source in $sources) { foreach ($row in @(@($source.Rows) | Where-Object { $null -ne $_ })) { $entry = [pscustomobject]@{ Kind = $source.Kind Row = $row Time = ConvertTo-DataTime (Get-DataProperty $row 'Time') App = "$(Get-DataProperty $row 'App')".Trim() } if (-not $entry.App -or $null -eq $entry.Time) { $groups.Add([pscustomobject]@{ Start = $entry.Time; Members = @($entry) }) continue } # Windows writes OUTLOOK.EXE in one event and outlook.exe in the next. $key = $entry.App.ToLowerInvariant() if (-not $placed.ContainsKey($key)) { $placed[$key] = [System.Collections.Generic.List[psobject]]::new() } $placed[$key].Add($entry) } } foreach ($key in @($placed.Keys)) { $current = $null foreach ($entry in @($placed[$key] | Sort-Object Time)) { if ($null -ne $current -and ($entry.Time - $current.Start).TotalSeconds -le $WindowSeconds) { $current.Members = @($current.Members) + $entry continue } $current = [pscustomobject]@{ Start = $entry.Time; Members = @($entry) } $groups.Add($current) } } $incidents = foreach ($group in $groups) { $members = @($group.Members) $crashed = @($members | Where-Object { $_.Kind -eq 'Crash' } | ForEach-Object { $_.Row }) $faulted = @($members | Where-Object { $_.Kind -eq 'Fault' } | ForEach-Object { $_.Row }) $first = $null if ($crashed.Count) { $first = $crashed[0] } $types = [System.Collections.Generic.List[string]]::new() $method = '' foreach ($row in $faulted) { foreach ($type in (Get-DataCollection $row 'ExceptionTypes')) { if ("$type" -and -not $types.Contains("$type")) { $types.Add("$type") } } if (-not $method) { $method = "$(Get-DataProperty $row 'Method')" } } # The name as the Application Error wrote it, where there is one: that is the # event the path and the version come from. $app = $members[0].App if ($first -and "$(Get-DataProperty $first 'App')".Trim()) { $app = "$(Get-DataProperty $first 'App')".Trim() } $path = "$(Get-DataProperty $first 'AppPath')" [pscustomobject]@{ Time = $group.Start App = $app Version = "$(Get-DataProperty $first 'Version')" Exception = "$(Get-DataProperty $first 'Exception')" Module = "$(Get-DataProperty $first 'Module')" ModuleVer = "$(Get-DataProperty $first 'ModuleVer')" AppPath = $path Remote = [bool]($path -and @($RemotePath) -contains $path) ExceptionTypes = [string[]]$types.ToArray() Method = $method Hang = [bool]@($members | Where-Object { $_.Kind -eq 'Hang' }).Count Events = $members.Count # How many of the events say the program died: the 1000s and the 1026s. What # tells a crash from a hang, whatever else the events did or did not say. CrashEvents = $crashed.Count + $faulted.Count } } # Newest first, as the log returns its events. One that has no time comes last. @($incidents | Sort-Object @{ Expression = { if ($null -ne $_.Time) { $_.Time } else { [datetime]::MinValue } } } -Descending) } function Get-StabilityIncident { <# .SYNOPSIS The incidents in gathered data, whether the Gatherer merged them or not. Pure. .DESCRIPTION Data gathered before incidents existed - an older fixture, an Elevated Part running an older module - carries the three lists of event rows and nothing else. The same merge is then done here, from the same rows. What it cannot know is whether a drive letter is mapped: that was never asked. A UNC path says so itself. #> [CmdletBinding()] [OutputType([object[]])] param( [AllowNull()]$Data, # Untyped: it may come from a Check Definition. See ConvertTo-CrashIncident. $WindowSeconds = $script:StabilityIncidentWindowSeconds ) $carried = $false if ($Data -is [hashtable]) { $carried = $Data.ContainsKey('Incidents') } elseif ($null -ne $Data) { $carried = [bool]$Data.PSObject.Properties['Incidents'] } if ($carried) { return , (Get-DataCollection $Data 'Incidents') } $crashes = (Get-DataCollection $Data 'Crashes') $shares = @($crashes | ForEach-Object { "$(Get-DataProperty $_ 'AppPath')" } | Where-Object { $_ -match '^\\\\' } | Select-Object -Unique) , @(ConvertTo-CrashIncident -Crash $crashes -RuntimeFault (Get-DataCollection $Data 'RuntimeFaults') ` -Hang (Get-DataCollection $Data 'Hangs') -WindowSeconds $WindowSeconds -RemotePath $shares) } function Select-CrashIncident { <# .SYNOPSIS The incidents in which a program died, leaving out those in which it only stopped answering. Pure. .DESCRIPTION A hang is the application stopping; a crash is the application dying. An incident that holds nothing but an Application Hang is listed with the others, and is not counted as a crash: Windows logs one each time somebody closes a window that was slow to answer. An incident that holds an Application Error 1000 or a .NET Runtime 1026 is a crash, whatever else it holds and however little those events said: a 1026 whose text named no exception type, beside a hang, is a program that died. The incident says how many such events it holds. One merged before incidents said so is read as it was then: a hang, and nothing a crash event would have left behind. #> [CmdletBinding()] [OutputType([object[]])] param([AllowNull()][AllowEmptyCollection()]$Incident) , @(@($Incident) | Where-Object { $null -ne $_ -and -not (Test-HangOnlyIncident -Incident $_) }) } function Test-HangOnlyIncident { <# .SYNOPSIS Whether an incident is made of hangs alone. Pure. See Select-CrashIncident. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$Incident) if (-not [bool](Get-DataProperty $Incident 'Hang')) { return $false } $crashEvents = ConvertTo-Number (Get-DataProperty $Incident 'CrashEvents') if ($null -ne $crashEvents) { return ($crashEvents -le 0) } -not "$(Get-DataProperty $Incident 'Exception')" -and -not "$(Get-DataProperty $Incident 'Module')" -and -not "$(Get-DataProperty $Incident 'Method')" -and -not (Get-DataCollection $Incident 'ExceptionTypes').Count } |