private/Add-Modulelog.ps1
|
function Add-ModuleLog { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('Error', 'Warning', 'Info', 'Performance', 'Signature', 'WMI', 'Detection')] [string]$LogType, [Parameter(Mandatory)] [string]$Message, [Parameter()] [object]$Data, [Parameter()] [array]$Updates ) $logRoot = "$env:ProgramData\Lenovo\lenovo.Client.Update\Logs" # Create log directory if it doesn't exist if (-not (Test-Path $logRoot)) { New-Item -ItemType Directory -Path $logRoot -Force | Out-Null } $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ") switch ($LogType) { 'Error' { $logFile = Join-Path $logRoot "ErrorLog.txt" $logEntry = "[$timestamp] [ERROR] $Message" Add-Content -Path $logFile -Value $logEntry } 'Warning' { $logFile = Join-Path $logRoot "ErrorLog.txt" $logEntry = "[$timestamp] [WARNING] $Message" Add-Content -Path $logFile -Value $logEntry } 'Info' { $logFile = Join-Path $logRoot "InfoLog.txt" $logEntry = "[$timestamp] [INFO] $Message" Add-Content -Path $logFile -Value $logEntry } 'Detection' { $logFile = Join-Path $logRoot "Detection.json" # Build update details array $updateDetails = @() if ($Updates -and $Updates.Count -gt 0) { $updateDetails = foreach ($update in $Updates) { [PSCustomObject]@{ packageId = $update.ID title = $update.Title version = $update.Version type = $update.Type releaseDate = if ($update.ReleaseDate) { $update.ReleaseDate.ToString("yyyy-MM-dd") } else { $null } isInstalled = $update.IsInstalled } } } # Create structured log entry $entry = [PSCustomObject]@{ timestamp = $timestamp level = "INFO" component = "Get-LnvUpdate" message = $Message computer = $Data.ComputerName user = $Data.User executionTimeMs = if ($Data.ExecutionTime) { [int]([TimeSpan]::Parse($Data.ExecutionTime)).TotalMilliseconds } else { 0 } summary = [PSCustomObject]@{ totalFound = $Data.TotalUpdatesFound applicable = $Data.ApplicableUpdates installed = $Data.InstalledUpdates needed = $Data.ApplicableUpdates - $Data.InstalledUpdates } updates = $updateDetails } $existing = if (Test-Path $logFile) { $content = Get-Content $logFile -Raw if ($content) { $parsed = $content | ConvertFrom-Json if ($parsed -is [array]) { $parsed } else { @($parsed) } } else { @() } } else { @() } $existing += $entry if ($existing.Count -gt 100) { $existing = $existing | Select-Object -Last 100 } $existing | ConvertTo-Json -Depth 10 | Set-Content $logFile -Force } 'Performance' { $logFile = Join-Path $logRoot "Performance.json" $entry = [PSCustomObject]@{ timestamp = $timestamp level = "DEBUG" message = $Message data = $Data } $existing = if (Test-Path $logFile) { $content = Get-Content $logFile -Raw if ($content) { $content | ConvertFrom-Json } else { @() } } else { @() } if ($existing -isnot [array]) { $existing = @($existing) } $existing += $entry if ($existing.Count -gt 100) { $existing = $existing | Select-Object -Last 100 } $existing | ConvertTo-Json -Depth 10 | Set-Content $logFile -Force } 'Signature' { $logFile = Join-Path $logRoot "SignatureVerification.json" $entry = [PSCustomObject]@{ timestamp = $timestamp level = "DEBUG" message = $Message data = $Data } $existing = if (Test-Path $logFile) { $content = Get-Content $logFile -Raw if ($content) { $content | ConvertFrom-Json } else { @() } } else { @() } if ($existing -isnot [array]) { $existing = @($existing) } $existing += $entry if ($existing.Count -gt 100) { $existing = $existing | Select-Object -Last 100 } $existing | ConvertTo-Json -Depth 10 | Set-Content $logFile -Force } 'WMI' { $logFile = Join-Path $logRoot "WMIExport.json" $entry = [PSCustomObject]@{ timestamp = $Timestamp level = "INFO" message = $Message data = $Data } $existing = if (Test-Path $logFile) { $content = Get-Content $logFile -Raw if ($content) { $content | ConvertFrom-Json } else { @() } } else { @() } if ($existing -isnot [array]) { $existing = @($existing) } $existing += $entry if ($existing.Count -gt 50) { $existing = $existing | Select-Object -Last 50 } $existing | ConvertTo-Json -Depth 10 | Set-Content $logFile -Force } } Write-Verbose "Logged [$LogType]: $Message" } function Write-LnvLog { <# .SYNOPSIS Writes a diagnostic line into the log file a public cmdlet is currently writing. .DESCRIPTION The log file is appended to directly rather than produced with Start-Transcript, because a transcript only records what is sent to the host: every line that reached the log would also have to scroll past in the terminal. Writing the file here keeps the console quiet while the log file still gets the full detail. The line only goes to the log when a cmdlet has set $script:LnvLogFilePath. While that is $null this function does nothing at all, which is what lets the callers log unconditionally: a run that writes no log, or that writes a transcript instead (Get-LnvUpdate -LogFile), is left exactly as it would have been without these calls. When a log file is active, Info and Detail messages are additionally emitted as verbose output so an interactive user can follow along with -Verbose; Warning and Error levels are not, because the caller has already reported those on their own stream. While $script:LnvLogQuietDetection is $true, Info and Detail lines are kept out of the log file unless they were written with -Always. A log file that cannot be written is ignored on purpose - logging must never be the reason a detection run fails. .PARAMETER Message The text to log. .PARAMETER Level Info (default), Detail, Warning or Error. Detail is used for the indented sub-lines that explain a preceding Info or Warning line. .PARAMETER NoTimestamp Write the message as-is, without the leading timestamp and level tag. Used for the header block and other pre-formatted output. .PARAMETER ToConsole Also write the line to the console. Used for the parts of Debug-LnvUpdate that are meant to be read on screen, so that they end up in its log file as well. .PARAMETER ConsoleOnly Write the line to the console and not to the log file. Used for the few notes that are true only on screen, such as Debug-LnvUpdate reporting that a section was cut short - the log file holds that section in full, so the note would contradict it there. .PARAMETER Always Write the line even while $script:LnvLogQuietDetection is on. Used for the environment header, which states which module version produced the log and on which machine, and so has to be there however little else was written. .EXAMPLE Write-LnvLog "Already installed: n3jet42w" -Level Info #> [CmdletBinding()] param ( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [AllowEmptyString()] [string]$Message, [ValidateSet('Info', 'Detail', 'Warning', 'Error')] [string]$Level = 'Info', [switch]$NoTimestamp, [switch]$ToConsole, [switch]$ConsoleOnly, [switch]$Always ) process { if ($ToConsole -or $ConsoleOnly) { Write-Information $Message -InformationAction Continue } elseif ($script:LnvLogFilePath -and ($Level -eq 'Info' -or $Level -eq 'Detail')) { Write-Verbose $Message } if ($ConsoleOnly -or -not $script:LnvLogFilePath) { return } if ($script:LnvLogQuietDetection -and -not ($Always -or $ToConsole) -and ($Level -eq 'Info' -or $Level -eq 'Detail')) { return } $Line = if ($NoTimestamp) { $Message } else { '[{0}] [{1}] {2}' -f (Get-Date).ToString('HH:mm:ss'), $Level.ToUpperInvariant(), $Message } try { Add-Content -LiteralPath $script:LnvLogFilePath -Value $Line -Encoding UTF8 -ErrorAction Stop } catch { # Deliberately ignored, see the description } } } function Write-LnvWarning { <# .SYNOPSIS Reports a warning to the caller and records it in the log file. .DESCRIPTION Warnings state why a package was skipped (a 404 on the Lenovo server, unsigned files, XML that will not parse), which is exactly what a support case needs, so they have to be in the log file and not only on the console. The warning itself is unchanged: it is still raised with Write-Warning. .PARAMETER Message The warning text. .EXAMPLE Write-LnvWarning "Package $Name returned 404, skipping" #> [CmdletBinding()] param ( [Parameter(Mandatory, Position = 0)] [AllowEmptyString()] [string]$Message ) Write-Warning $Message # Flattened, because some of these messages span several lines Write-LnvLog -Message ($Message -replace '\s*\r?\n\s*', ' ') -Level 'Warning' } function Write-LnvError { <# .SYNOPSIS Reports a non-terminating error to the caller and records it in the log file. .DESCRIPTION The counterpart of Write-LnvWarning for the errors the cmdlets report while carrying on with the next package. The error is still raised with Write-Error, so the caller's $ErrorActionPreference and -ErrorAction continue to apply. .PARAMETER Message The error text. .EXAMPLE Write-LnvError "The package $Name could not be retrieved and will be skipped" #> [CmdletBinding()] param ( [Parameter(Mandatory, Position = 0)] [AllowEmptyString()] [string]$Message ) Write-LnvLog -Message ($Message -replace '\s*\r?\n\s*', ' ') -Level 'Error' Write-Error $Message } function Resolve-LnvLogFilePath { <# .SYNOPSIS Works out which file a cmdlet should write its log to and makes sure the directory exists. .DESCRIPTION Shared by the cmdlets that accept -LogFile / -LogPath so they all resolve a log location the same way: - -LogFile : a timestamped file in $env:ProgramData\Lenovo\Lenovo.Client.Update\Logs - -LogPath <dir> : a timestamped file inside that directory - -LogPath <file> : that file, and a bare file name goes to the default log directory - both : -LogPath wins, with a warning Returns $null when neither was requested, or when the directory could not be created, in which case the caller carries on without a log file. .PARAMETER LogFile Log to the default location. .PARAMETER LogPath Log to this file or directory. .PARAMETER Prefix The leading part of a generated file name, for example 'Detection' or 'Debug'. .EXAMPLE $Path = Resolve-LnvLogFilePath -LogFile:$LogFile -LogPath $LogPath -Prefix 'Detection' #> [CmdletBinding()] [OutputType([string])] param ( [switch]$LogFile, [string]$LogPath, [string]$Prefix = 'Detection' ) $DefaultDirectory = "$env:ProgramData\Lenovo\Lenovo.Client.Update\Logs" $GeneratedName = "$Prefix-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" $Resolved = if ($LogFile -and $LogPath) { Write-Warning "Both -LogFile and -LogPath specified. Using -LogPath value." $LogPath } elseif ($LogPath) { $LogPath } elseif ($LogFile) { Join-Path $DefaultDirectory $GeneratedName } else { $null } if (-not $Resolved) { return $null } if (Test-Path -Path $Resolved -PathType Container) { $Resolved = Join-Path $Resolved $GeneratedName } elseif (-not (Split-Path -Path $Resolved -Parent)) { # A bare file name goes to the default log directory $Resolved = Join-Path $DefaultDirectory $Resolved } $Directory = Split-Path -Path $Resolved -Parent if ($Directory -and -not (Test-Path -Path $Directory)) { try { New-Item -ItemType Directory -Path $Directory -Force -ErrorAction Stop | Out-Null } catch { Write-Warning "Could not create log directory '$Directory': $($_.Exception.Message)" return $null } } return $Resolved } function Get-LnvEnvironmentInfo { <# .SYNOPSIS Collects the OS, PowerShell, module and machine detail that is useful when troubleshooting a log file sent in by a customer. .DESCRIPTION Gathers the information that cannot be derived from the rest of a log but is almost always the first thing asked for during a support case: - the full numeric Windows version (Major.Minor.Build.UBR) plus the marketing release (for example 24H2), edition and installation type, so the exact patch level of the device is known - the version of this module that produced the log - the PowerShell version/edition and whether the session was elevated - the machine identity that packages are evaluated against (model, product name, serial number, BIOS and embedded controller version) The result is cached for the lifetime of the module session because none of it changes while a cmdlet runs. Every lookup is best-effort: anything that cannot be read is returned as $null instead of failing the caller, because this only feeds diagnostics. .PARAMETER Refresh Collect the information again instead of returning the cached result. .EXAMPLE Get-LnvEnvironmentInfo #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [switch]$Refresh ) if ($script:LnvEnvironmentInfo -and -not $Refresh) { return $script:LnvEnvironmentInfo } # Get-WindowsVersion cross-checks cmd.exe, the registry, ntoskrnl.exe, .NET and WMI and # returns the highest, which is the most accurate reading of the actual patch level. [Version]$OSVersion = $null try { $OSVersion = Get-WindowsVersion } catch { Write-Verbose "Get-WindowsVersion failed, falling back to [Environment]::OSVersion: $($_.Exception.Message)" } if (-not $OSVersion) { $OSVersion = [Environment]::OSVersion.Version } # DisplayVersion (24H2, 23H2, ...) exists since Windows 10 20H2, ReleaseId before that. # ProductName in this key still reads "Windows 10" on Windows 11, so OSName comes from CIM. $CurrentVersion = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue $CimOperatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue -Verbose:$false $CimComputerSystem = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue -Verbose:$false $CimComputerProduct = Get-CimInstance -ClassName Win32_ComputerSystemProduct -ErrorAction SilentlyContinue -Verbose:$false $CimBios = Get-CimInstance -ClassName Win32_BIOS -ErrorAction SilentlyContinue -Verbose:$false [bool]$IsElevated = $false try { $IsElevated = Test-RunningAsAdmin } catch { Write-Verbose "Could not determine whether the session is elevated: $($_.Exception.Message)" } $Info = [PSCustomObject][ordered]@{ # Module ModuleName = 'Lenovo.Client.Update' ModuleVersion = $script:LnvModuleVersion ModuleRoot = $script:LnvModuleRoot # PowerShell / process PSVersion = $PSVersionTable.PSVersion.ToString() PSEdition = if ($PSVersionTable.Contains('PSEdition')) { $PSVersionTable.PSEdition } else { 'Desktop' } Is64BitProcess = [Environment]::Is64BitProcess IsElevated = $IsElevated UserName = "${env:USERDOMAIN}\${env:USERNAME}" ComputerName = $env:COMPUTERNAME # Operating system OSName = if ($CimOperatingSystem) { $CimOperatingSystem.Caption } else { $CurrentVersion.ProductName } OSVersion = $OSVersion.ToString() OSBuild = $OSVersion.Build OSUpdateBuildRevision = $OSVersion.Revision OSDisplayVersion = if ($CurrentVersion.DisplayVersion) { $CurrentVersion.DisplayVersion } else { $CurrentVersion.ReleaseId } OSEdition = $CurrentVersion.EditionID OSInstallationType = $CurrentVersion.InstallationType OSArchitecture = if ($CimOperatingSystem) { $CimOperatingSystem.OSArchitecture } else { $env:PROCESSOR_ARCHITECTURE } OSLanguage = [CultureInfo]::CurrentUICulture.Name OSLanguageCode = [CultureInfo]::CurrentUICulture.ThreeLetterWindowsLanguageName # The value that _OS dependency tests are evaluated against OSFamily = if ($OSVersion -ge [Version]::new(10, 0, 22000, 0)) { '11' } else { '10' } OSInstallDate = if ($CimOperatingSystem) { $CimOperatingSystem.InstallDate } else { $null } LastBootUpTime = if ($CimOperatingSystem) { $CimOperatingSystem.LastBootUpTime } else { $null } # Machine Manufacturer = if ($CimComputerSystem) { $CimComputerSystem.Manufacturer } else { $null } Model = if ($CimComputerSystem) { $CimComputerSystem.Model } else { $null } ProductName = if ($CimComputerProduct) { $CimComputerProduct.Version } else { $null } SerialNumber = if ($CimBios) { $CimBios.SerialNumber } else { $null } BiosVersion = if ($CimBios) { $CimBios.SMBIOSBIOSVersion } else { $null } BiosReleaseDate = if ($CimBios) { $CimBios.ReleaseDate } else { $null } EmbeddedControllerVersion = if ($CimBios) { @($CimBios.EmbeddedControllerMajorVersion, $CimBios.EmbeddedControllerMinorVersion) -join '.' } else { $null } } $script:LnvEnvironmentInfo = $Info return $Info } function Write-LnvLogEnvironmentHeader { <# .SYNOPSIS Writes the environment block at the top of a log file. .DESCRIPTION Emits the Get-LnvEnvironmentInfo detail as a readable block through Write-LnvLog, so that every log file a customer sends in states which module version produced it, on which exact Windows patch level, and on which machine. .PARAMETER Component The cmdlet the log belongs to, shown in the first line of the block. .PARAMETER Refresh Re-collect the environment information instead of using the cached result. .EXAMPLE Write-LnvLogEnvironmentHeader -Component 'Get-LnvUpdate' #> [CmdletBinding()] param ( [string]$Component = 'Get-LnvUpdate', [switch]$Refresh ) $Info = Get-LnvEnvironmentInfo -Refresh:$Refresh $Unknown = 'unknown' $Fields = [ordered]@{ 'Module version' = "$($Info.ModuleVersion) ($($Info.ModuleRoot))" 'PowerShell' = "$($Info.PSVersion) ($($Info.PSEdition)), 64-bit process: $($Info.Is64BitProcess), elevated: $($Info.IsElevated)" 'User' = "$($Info.UserName) on $($Info.ComputerName)" 'OS' = "$($Info.OSName) [$($Info.OSEdition) / $($Info.OSInstallationType)]" 'OS version' = "$($Info.OSVersion) (build $($Info.OSBuild), UBR $($Info.OSUpdateBuildRevision), release $(if ($Info.OSDisplayVersion) { $Info.OSDisplayVersion } else { $Unknown }), Windows $($Info.OSFamily))" 'OS architecture' = "$($Info.OSArchitecture), language $($Info.OSLanguage) ($($Info.OSLanguageCode))" 'OS installed' = "$(if ($Info.OSInstallDate) { $Info.OSInstallDate.ToString('yyyy-MM-dd') } else { $Unknown }), last boot $(if ($Info.LastBootUpTime) { $Info.LastBootUpTime.ToString('yyyy-MM-dd HH:mm:ss') } else { $Unknown })" 'Machine' = "$($Info.Manufacturer) $($Info.Model) ($($Info.ProductName))" 'Serial number' = "$($Info.SerialNumber)" 'BIOS' = "$($Info.BiosVersion), released $(if ($Info.BiosReleaseDate) { $Info.BiosReleaseDate.ToString('yyyy-MM-dd') } else { $Unknown }), EC $($Info.EmbeddedControllerVersion)" } Write-LnvLog -Always -NoTimestamp -Message ('=' * 78) Write-LnvLog -Always -NoTimestamp -Message " $($Info.ModuleName) - $Component - started $((Get-Date).ToString('yyyy-MM-dd HH:mm:ss zzz'))" Write-LnvLog -Always -NoTimestamp -Message ('-' * 78) foreach ($Field in $Fields.GetEnumerator()) { Write-LnvLog -Always -NoTimestamp -Message (' {0,-16}: {1}' -f $Field.Key, $Field.Value) } Write-LnvLog -Always -NoTimestamp -Message ('=' * 78) } function Format-LnvDependencyTrace { <# .SYNOPSIS Turns the trace collected by Resolve-XMLDependency -Trace into readable log lines. .DESCRIPTION Each trace entry becomes one line stating whether the test passed, which kind of test it was, and the 'Got / Expected' detail the test produced, for example: [FAIL] _WindowsBuildVersion [ Got: 26200, Expected: 22631 ] Entries are in evaluation order; because the parser recurses into a group before it records the group itself, a group node appears after its children. These lines are what a support engineer reads in a log file a customer sent in, so only the detail that says why a test came out the way it did is kept. Test-MachineSatisfiesDependency narrates its way through a test on the debug stream - which hardware IDs matched, which device is being looked at, which comparison is about to be tried - and on a test that passed, none of that narration adds anything to the comparison it ends in. A _Driver test for a chipset package listing three hardware IDs produces over two thousand characters of it. The narration is therefore dropped on a passing test and kept on a failing one, where 'Matched devices: 0 exact, 0 wildcard', or which of several devices was being tested, is the answer. A package may list hundreds of hardware IDs as Or-branches - the ThinkPad monitor file package lists over 250 - and on a machine that has none of them every one of those tests reports the same failure. A run of consecutive tests that share a node name and a result and carry no per-test detail is therefore reported as a single line naming the first few values and the number of remaining ones: [FAIL] _PnPID x 258 - Monitor\IBM0A2E, Monitor\IBM0A41, Monitor\IBM0A4B, +255 more Tests that produced their own 'Got / Expected' detail are never collapsed, because each of those lines says something different. .PARAMETER Trace The trace collection filled in by Resolve-XMLDependency -Trace. .PARAMETER FailedOnly Only return the lines for tests that did not pass. This is what identifies the reason a package was reported as not applicable. A test that failed inside a group the machine satisfied anyway is left out, see the comment on $HasSatisfiedAncestor. .PARAMETER PassedOnly Only return the lines for tests that passed. This is what identifies an already installed package, where the tests that passed are the ones that prove it: the tests in an Or-tree that did not match are not a problem and only add noise. .PARAMETER IncludeGroups Also return a line for the And/Or/Not group nodes, not just the individual tests. .EXAMPLE Format-LnvDependencyTrace -Trace $Trace -FailedOnly #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Position = 0)] [AllowNull()] [AllowEmptyCollection()] [object[]]$Trace, [switch]$FailedOnly, [switch]$PassedOnly, [switch]$IncludeGroups ) if (-not $Trace) { return } # Runs shorter than this stay expanded: seeing two or three hardware IDs in full is more # useful than being told there were two or three of them. $CollapseThreshold = 5 $SampleCount = 3 # Debug records that never belong in a log file: they describe how a test went about its # work, not what it found. The per-pair 'Matched device' record in particular is written # once for every hardware ID the package lists times every ID the machine reports for a # device, which is where a _Driver test's thousands of characters come from. # A driver rank, and the note that a device looks like it is on a generic driver, are only # ever acted on when -FailInboxDrivers is in play, and that case reports itself with # 'Failed because device is using an inbox driver', which is kept. [string]$NarrationAlways = @( "^Matched device '.+' with required " '^Adding device - HardwareIDs matched only when using wildcards$' '^Added runspace process \d+ to job: ' '^Passed Driver(?:Date|Version) test$' ' DriverRank is 0x' 'using a generic or inbox driver$' ) -join '|' # Kept only on a test that failed, where they say which device, or which of several # candidate devices, the failure is about. On a passing test the comparison is the answer # and these only repeat what the package title already said. [string]$NarrationOnPass = @( '^Matched devices: ' '^Testing ' '^Got unsupported with ProductVersion, trying comparison with FileVersion$' ) -join '|' $GetStatus = { param ($Entry) if ($Entry.Unsupported) { 'UNSUPPORTED' } elseif ($Entry.Passed) { 'PASS' } else { 'FAIL' } } # Reduces the records a test produced to the ones worth logging, and rewrites two of them: # 'Trying to match driver based on Date' only announces the comparison that follows, so the # word is carried onto that comparison rather than kept as a sentence of its own, and an # external detection is reported by command line without the temporary directory it was # expanded into, which is gone by the time anyone reads the log. $SelectDetail = { param ($Entry) $Kept = [System.Collections.Generic.List[string]]::new() [string]$Subject = $null foreach ($Line in @($Entry.Detail)) { if ([string]::IsNullOrWhiteSpace($Line)) { continue } if ($Line -match '^Trying to match driver based on (\w+)') { $Subject = $Matches[1] continue } [string]$Emit = $Line if ($Line -match '^\[\s*Got') { if ($Subject) { $Emit = $Line -replace '^\[\s*Got', "[$Subject Got" $Subject = $null } } elseif ($Line -match '^Starting external process: File: (?<File>.+?) Arguments: (?<Arguments>.*) WorkingDirectory: ') { $Emit = ('Ran {0} {1}' -f ($Matches['File'] -replace '^.*[\\/]'), $Matches['Arguments']).Trim() } elseif ($Line -match $NarrationAlways) { continue } elseif ($Entry.Passed -and $Line -match $NarrationOnPass) { continue } # An external detection that has to be retried (elevation, ShellExecute) reports its # command line once per attempt, and once the temporary directory is out of the line # the attempts are indistinguishable. Two identical lines in a row say nothing twice. if ($Kept.Count -and $Kept[$Kept.Count - 1] -eq $Emit) { continue } $Kept.Add($Emit) } $Kept } $FormatEntry = { param ($Entry) $Line = '[{0}] {1}' -f (& $GetStatus $Entry), $Entry.Node if ($Entry.Negated) { $Line += ' (negated by a Not node)' } [string[]]$Detail = @(& $SelectDetail $Entry) if ($Detail.Count) { $Line += ' ' + ($Detail -join ' | ') } elseif ($Entry.Value) { $Line += ' ' + $Entry.Value } elseif ($Entry.Summary) { $Line += ' ' + $Entry.Summary } elseif ($Entry.Xml) { $Line += ' ' + $Entry.Xml } $Line } # Only a test that is nothing but a value can be collapsed: one that reported detail worth # keeping, or that a Not node inverted, has to be read on its own line. $IsCollapsible = { param ($Entry) [bool]$Entry.Value -and -not $Entry.Negated -and @(& $SelectDetail $Entry).Count -eq 0 } # A test that failed inside a group the machine satisfied anyway did not decide anything. # The common case is a package built for '10/11' whose Or-tree holds a WIN10-only and a # WIN11-only _OS test: on a Windows 11 machine the WIN10 test fails, the Or passes, and # reporting that failure tells a support engineer the OS was not supported when it was. # Entries are recorded in evaluation order with a group following its own children, so the # group enclosing an entry is the next entry recorded at a shallower depth. $HasSatisfiedAncestor = { param ([int]$Index) [int]$Depth = $Trace[$Index].Depth for ($i = $Index + 1; $i -lt $Trace.Count; $i++) { if ($Trace[$i].Depth -lt $Depth) { if ($Trace[$i].Passed) { return $true } $Depth = $Trace[$i].Depth } } $false } $Selected = [System.Collections.Generic.List[object]]::new() for ($Position = 0; $Position -lt $Trace.Count; $Position++) { $Entry = $Trace[$Position] if (-not $IncludeGroups -and $Entry.Type -ne 'Test') { continue } if ($FailedOnly) { if ($Entry.Passed) { continue } if (& $HasSatisfiedAncestor $Position) { continue } } if ($PassedOnly -and -not $Entry.Passed) { continue } $Selected.Add($Entry) } $Index = 0 while ($Index -lt $Selected.Count) { $First = $Selected[$Index] $Status = & $GetStatus $First # Find how far the run of identical-looking tests reaches $Last = $Index if (& $IsCollapsible $First) { while ($Last + 1 -lt $Selected.Count) { $Next = $Selected[$Last + 1] if ($Next.Node -ne $First.Node -or -not (& $IsCollapsible $Next) -or (& $GetStatus $Next) -ne $Status) { break } $Last++ } } $RunLength = $Last - $Index + 1 if ($RunLength -ge $CollapseThreshold) { $Sample = @($Selected[$Index..($Index + $SampleCount - 1)].Value) -join ', ' $Line = '[{0}] {1} x {2} - {3}' -f $Status, $First.Node, $RunLength, $Sample $Line += ', +{0} more' -f ($RunLength - $SampleCount) $Line } else { for ($i = $Index; $i -le $Last; $i++) { & $FormatEntry $Selected[$i] } } $Index = $Last + 1 } } # SIG # Begin signature block # MIItugYJKoZIhvcNAQcCoIItqzCCLacCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQURG+kM0Fxxua4Nq6W98sR/+5l # QpyggibcMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B # AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw # ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz # 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS # 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7 # bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI # SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH # trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14 # Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2 # h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt # 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR # iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER # ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K # Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd # BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS # y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk # BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC # hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS # b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV # HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh # hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO # 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo # 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h # UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x # aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMIIFkDCCA3ig # AwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG # EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl # cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMw # ODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UE # ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYD # VQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEppz1Y # q3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lX # FllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxe # TsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbu # yntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I # 9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/CNdaSaTC5qmg # Z92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtmmnTK3kse # 5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKy # Ebe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwh # HbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/ # Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwID # AQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4E # FgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQADggIBALth2X2p # bL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY # ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdN # Oj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4 # i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJ # EVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9NcCOGDErcgdLM # MpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N0XWs0Mr7QbhD # parTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb/UdK # Dd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP # 0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLS # oCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9T # dSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+MIIGsDCCBJigAwIBAgIQCK1AsmDS # nEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UE # ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYD # VQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAwMDAwWhcN # MzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs # IEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5n # IFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+kjmjYXPXr # NCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9NH1MgFcS # a0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9URnokCF4 # RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegYE2XFf7JP # hSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS4+jWufcx # 4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJawv9qYFSL # ScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+wc86LJiUG # soPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eRGv7bUKJG # yGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF23r9Yy3IQ # KUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCKZFTBzCEa # 6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhECAwEAAaOC # AVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2O/hfEYb7 # /mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1Ud # DwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcBAQRrMGkw # JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcw # AoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJv # b3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAHBgVngQwB # AzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6mdNW4AIa # pfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/SfQnuxaB # RVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzYgBoRGRjN # YZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9kNF2ht0c # sGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ9d3OVG3Z # XQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAewQ3+ViCCC # cPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5LmTl/eeqxJ # zy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HASIRLlk2r # REDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xry7fwdxPm # 5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhRILutG4UI # 4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFuv2jiJmCG # 6sivqf6UHedjGzqGVnhOMIIGtDCCBJygAwIBAgIQDcesVwX/IZkuQEMiDDpJhjAN # BgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg # SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2Vy # dCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcNMzgwMTE0MjM1OTU5 # WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV # BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB # MjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtHgx # 0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oRjzUXMmxCqvkbsDpz # 4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+QdSKWM06qchUP+AbdJ # gMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRuQL37QXbDhAktVJMQ # bzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0Xm+nt5pnYJU3Gmq6 # bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQVESYOszFI2Wv82wnJ # RfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2qHxJ0ucS638ZxqU1 # 4lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF0LJAQQZxst7VvwDD # jAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgxCZSKi17yVp2NL+cn # T6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9Xr/u6bDTnYCTKIsDq # 1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7OgWmnhFr4yUozZtqg # PrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOCAV0wggFZMBIGA1Ud # EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esrikFb2L9RJ7MtOMB8G # A1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjAT # BgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYD # VR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 # VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEwvb4LyLU0pn/N0IfF # iBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8G0iP5kvN2n7Jd2E4 # /iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40y8S4F3/a+Z1jEMK/ # DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCDA/JYsq7pGdogP8HR # trYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADVZKON/gnZruMvNYY2 # o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4EWj7PtspIHBldNE2K # 9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpVfHIqQ6Ku/qjTY6hc # 3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0c1ugMZyZZd/BdHLi # Ru7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7OigizwJWeukcyIPbAv # jSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2rtY/9TCA6TD8dC3J # E3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz0scmbKvFoW2jNrbM # 1pD2T7m3XDCCBu0wggTVoAMCAQICEAhP3DNPfkVO28MPj/mSGDUwDQYJKoZIhvcN # AQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw # PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2 # IFNIQTI1NiAyMDI1IENBMTAeFw0yNjA4MDUwMDAwMDBaFw0zNzExMDQyMzU5NTla # MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UE # AxMyRGlnaUNlcnQgU0hBMjU2IFJTQTQwOTYgVGltZXN0YW1wIFJlc3BvbmRlciAy # MDI2IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2e6byyf7NSvjU # m0xls/04xjD4fAkOkbnGQi7+Wpx81iYxfzViaxSIctuH3KSl5YEYpMuFgGsA31N2 # D9ATMbfZdw5uaAhuWevQKhDdZIB4NnqcfpfpWQXJiQnDdAElETC+bhSEvNLGbA8D # twUpFMQ4yyYQSPqomT92osQAv6hBi47ATZS6JfVWe6XxhF4jJZ3iSAuf2Cros1cz # RSmWRHqMv9AfGZvp8ygYElhudpQjtcPpwoOl6QrZJUyV3iINvN4cO05prGV0fkjG # 426xDr2d3z9lcSIHkdvGPdGUrXdxfVbgOUVcp2/8ISEzwKPW++Wa+E2ujI91EZtu # kGWDJ/xZ27k3oHKEXBRGfRTqjOU+jE3ba/5++JSE/7oNHnjs5mekExYN96LV/mxU # bCKJb8pBNY4r3uD7hEmk/M81XhVgwDA7aMzYC3LZBg9WY5BMmbSay5ecmtJuXaB/ # 0nKWmQmVZeqTVDgsmzHP5MQuhAJkiWNuC9MmCg9TZHXbJ2/yLVSov9p16UDTLtT0 # +aa1vN71fHeu1qMLlLNB3WOB/ADCxr3S/1hxI92Z6jKgEED/btwIvbfuXkNNhg8M # tDg43c4tMZae9FvqMOt/9PvmAxF9TNIsIFB8G6yb36ZJZGUL8N/pL971DyLXcK6H # M5PYnH5X+eVtczhCgHCVQCF6XDAlPQIDAQABo4IBlTCCAZEwDAYDVR0TAQH/BAIw # ADAdBgNVHQ4EFgQUFMljijAu1Er7bpTz5uNAfvXszeIwHwYDVR0jBBgwFoAU729T # SunkBnx6yuKQVvYv1Ensy04wDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoG # CCsGAQUFBwMIMIGVBggrBgEFBQcBAQSBiDCBhTAkBggrBgEFBQcwAYYYaHR0cDov # L29jc3AuZGlnaWNlcnQuY29tMF0GCCsGAQUFBzAChlFodHRwOi8vY2FjZXJ0cy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2 # U0hBMjU2MjAyNUNBMS5jcnQwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL2NybDMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1waW5nUlNBNDA5 # NlNIQTI1NjIwMjVDQTEuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG # /WwHATANBgkqhkiG9w0BAQsFAAOCAgEAjcU6YR6dUgrfmawJgH59KECxa9Ji8sEi # 2g10CBDaMiqsaxWyW5cwlT/6ZF5sFznazqVsoC85U9dqLOYqQwst+UQQoNlDHgKR # La3xoc+OReFreFhnTXSG0Vrd2E2CZqUfm+5a+He1MJ/h+tNLuA+0Zzhn/Fo+FDYA # HWZHx4R79ZsfRFYe9UiXpXBDf6DkUo183Y38NYmR/XfDYf7YZ+oR9t3flbDwK+hg # GMs0gNNp1w9Z2CyOyI5or/sSwomAuNQ0hWC9xoU4stD8aWsD7RkcmgVRs6vlIk3z # PKQ+ylcheWkMlj+CoVRlFE55pv0ZWCaFt04lwP/rdGHE9qEVQZtyRE42ox7oNgC/ # r+Y4bSlZ3dw9K2x1xLtu6PkPKeLBFjzKigwfqm3Hm+k/+lnME8F5kPZTgiy2HLEH # klpryqs6QHnPXrRNeIzkAMyylnRN8P0wmirS0WkU+ywpEWFZ4QNg+9xS43tTuW9x # 0eXh7NDc1P/sV+zWxHXKH8tFt1ncHdVzqrZaYPyYMLSn2TOXajveJW1L3joiQSPs # WRGxkbDDW15jERFE4LvjnGu2O9zD1nLJSMdlYZEikl4w2w+q4IN/R+TIe0H4ngCI # 1moJCTbevGH4punIxM1Uoi0nmX3ZK+XbRT01uowE5ViXWHng0RgsmrX/EdYUo80r # 3TfMlkD0/YMwggdWMIIFPqADAgECAhAFfARoFtaNDha0m0G6FxiVMA0GCSqGSIb3 # DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFB # MD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5 # NiBTSEEzODQgMjAyMSBDQTEwHhcNMjYwNTE0MDAwMDAwWhcNMjcwNjE3MjM1OTU5 # WjBeMQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExFDASBgNV # BAcTC01vcnJpc3ZpbGxlMQ8wDQYDVQQKEwZMZW5vdm8xDzANBgNVBAMTBkxlbm92 # bzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVacqHydjmqo6bF2tHw # cETPkOZLgPv1tJeIerpLcfmxLoX40GsUiY5l96B13bcfKjKBJQvHsj5D08RfGnif # K+sGsAX8xCgaYNLxq0COKn51GgLLMNgzlE4rR/8mUMuWK2fKXs24dmn7teE2+e+2 # dz/GfHXgrAIPvIr4PN+dZAFeoj6/wfpLLHZXtQLxKVmtDFc7gQZkM6Z8o0HrH2eX # 2MVNQwbZZWvSQhqRN66/LMcXxbLP8SAb6nKRCExflbI5i+MpEq+xOdiJxkP5dC5s # rQ31JGLDdhbuAcUJEdXATzgP1pT9is3uWZm3fro71Kvfa0XLwBZWR2ut8sQ6KUhd # 0Nsmd5c0f2PiD2uTd9mDWHQ34bu9mDunZaeWZrIlUP9MJ8TMM82ao/4tjFNK5m3T # hZUbCGwoepenXq8yjbKMKqHHvcoJY0SmkAIlUzWoRBbRC+uN0TwcR048sZDPo+ZC # gdONLPrjnsIU+NxuhfDMnj4UYbkRbtvnJ+U0O4Eu+ajRS87Li+jN2jHrABvAUXtP # J7DkkxTTfaspzlfxueoqVKcGLnu4PUAthKg3g4hleGgeo7s+krEDAzyvdBnY+b8U # dRH/BCxDr7G5ys/bCMPEzcUFT5LrrVBHO6N2U65iYkamzEo4okrrCNFDJPZC1G/p # NleTeiIadSPTT0tJGPK+nDo1AgMBAAGjggIDMIIB/zAfBgNVHSMEGDAWgBRoN+Dr # tjv4XxGG+/5hewiIZfROQjAdBgNVHQ4EFgQU7R8eucYUNncCkcWYWfCltSt/mxYw # PgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5k # aWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEF # BQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIx # Q0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 # VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwgZQG # CCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy # dC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEu # Y3J0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggIBAC0PiOF1B8Qdp2pcfb3P # 6MuzaeTUw77bfMSCcnwY8xFtkyRRp9H6X4ZaSTdAQge3d6NG7OcHnGQ3FKraZqT3 # ugjNEXvH1MXr8SNgFb7t7TyKDK0RmRK9idsycLJfRsmbj0FSTL12jqrzyHrQqkTq # KaDqYfIVan7VxP2On8gq3/OT368z1YJbRgfu/rNpXoYpXTKSfxaIXNE7rEiUwu+T # vi8wgfLqC/a7ujC8vbYRWgaHHsz3bjFVN/h1p78adds2GdxPzYFoS12+JKq2cKGe # Ma5Hbr2YQfHsbpQlzZglB3mGCci+uhN5YGaHleo3Ohd0BHci32RFOA1xdD8fS9TD # pGRMGLKF9z5WTmiGZVR4x54Qwtf4clbA5V0P9d4ppmLZJKdeaHd/JKONVJ9nYV1H # NA52CgvgRWpUc+jxnPL5A3J9qIvUXp98ZQifEyHBFX2VqWTdu9MldzklRo/eAT1k # MUVheRAcd32yLDiFvn7lp2nasJs8Hh+6bUxfHKbKBrNrw7CmcTDzFK119BkykWS0 # vL7xsbCEbavLIhW/ZqEjWA9k/v2FS2wbBQ9YOC8F3magnslxWlZWsym3Dxdm0Hr1 # WKHbnFcBscMiBQ+fhLDAqIG8tPPnNhc7QhOOlxQlbfEDh8QmQbUo2H9hJJEGiHJS # VwuAKbcWembFyAgLV02ASxS7MYIGSDCCBkQCAQEwfTBpMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0 # ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExAhAFfARo # FtaNDha0m0G6FxiVMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBSnPMO6nN4IbiYA2PVvsv6mRfqC # xzANBgkqhkiG9w0BAQEFAASCAgBT0DqNOy1fu82zxrqdG6fi+kTPkI2vBckdM0mS # f8cWNcZi4yD2wxem7sjvswDNZb1nkkh/O1gVnvrJQZ6zLiWZ/OuV54fYtPsnq7XG # Kt0gdp+huFezvJmXRcqzpc9Zg/o4D2vsusqseWkbbb2mloMia67PLae72GU2knQq # GN1Ov2daN8hem77xi0ToGYjW3qsh+6lCQdnhIvpg7a9ur4/5b/UPkUyDNeJMpL7O # nqgoCIh+MytduTBWhiguuszuxYzp5KZVM1WSdz3Ao2Yd9O+xJSFjv7ePTuSt2ZCt # gRUZOT5fIVCg7K82JeMKD+jtI3peQ0hZ6K7VIIEJZFhYnMSvyYjCHPm9ZnBkK4bS # vkVAnscUIndjLyt6fJ0Qz3AG5UQmElSFehn2/RdQtz73Z9xBRZyGnasSgvr2GUGI # ESGWmm9v7bZ0D57PZLRU+uUmq/U8YJmj5Z/PvHn81eASdQ6Y5vtctIBgcPsMrKzC # 8VISXqqGq6aH+uh8MurlrlIY5cmsFr05DMYWF20p/MVUBPGjR8u6q+IE2l0pyEg9 # SvOuFpVmuqmxpu+F50T7EypRbNE0opRL3LMz6dB8F0i/Zxvh+jkg+bUox6tTzLw/ # qePVbGlnTmk3K28F5q2UGNhIqNWUpkdEP4o9ko7yuYBsHHKlfPmAXD1COwO2HsSO # pD/TY6GCAyYwggMiBgkqhkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYT # AlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQg # VHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEC # EAhP3DNPfkVO28MPj/mSGDUwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMx # CwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNjA5MjQxODEyMjNaMC8GCSqG # SIb3DQEJBDEiBCAe4lUpeg1/Av9Ntq2z7XSX9yX33daPIJtsJgYF5CWRNTANBgkq # hkiG9w0BAQEFAASCAgCTxh06fkE09v2lHYFapc3rlNkMtGMJ4iSbeVwwqAWMbxqP # jbLXBve4ohK3+bKcwIU9Gx8QfF+zzgSpeEMcD9UtIKDUF6GMTK5LF3IVSJ0mkv1r # sSwEpznI1iL33RwDvYAtbVzVvbQsgd4wXtIkot+mj5wLnXQgO1ufM/VFt0sbQRUQ # JykvJ9e1g68g5WlUFroBK/klxi+R9SKpjGJV/oOq1H96T87pZcGmYlBIUcD/pBSW # jmtOOV1HXjTdbfBu+/21+3Mrhs3NigNvhNQzfUuzhjs28NgJy3enGpok4saiom2L # q2/fBJSn25sB25D5ozB4Z73Dm5JVufEz9MlPQVorh47jlS/kJxFcg8TO7sjnSU25 # 7ZkJPY4X8CO4blXIALsEtD4ym1eMPdB83Ug2QoH0p3C6GJ5z/Cy95AYW4jUHRKup # NVKgOICUbBOLLaNhyDh+TlxkmCmclnUGctL9LxjUDH6LPzzdHTR55bVMKT/GDE4e # EZuWGJAh8yS6Nky4Loux5CUW2ikQ9Ha3pgprLNKaUnZV34n1dnousERcYLoy5I4E # UbQaFGbGXB+s4W0t8lETiARmRphYfJK2sz2syHpGPafDabAOqfzQ7PHeaqQEbULz # 00YGCEOSOaRrTLgWwksEKSgYnqYq5yVAlDLcpd67mBjsNDKJ2EGAA7wmzXXo1Q== # SIG # End signature block |