Private/System/Utilities/ReusableHelpers/WaitingHeartbeatScripts/Wait-TerminalState.ps1
|
function Wait-TerminalState { <# .SYNOPSIS Generic wait helper for "poll until terminal state" workflows with low-noise console animation. .DESCRIPTION - PollScript runs on schedule (PollSeconds) and returns an object (or $null if not found). - GetStatus extracts a status string from the object. - TerminalStates is a hashtable mapping status string -> terminal handling metadata. Interactive mode: - Animates a single-line spinner every TickMs (default 250ms) - Hides console cursor while spinning - Clears spinner line before writing logs Non-interactive mode: - No spinner/cursor operations, just polls and logs on change By default, throws on timeout. .PARAMETER Target Friendly label for logs (search name, action identity, computer name, etc.) .PARAMETER PollScript Scriptblock that returns the current object (or $null). Called on the poll schedule. .PARAMETER GetStatus Scriptblock that takes the object and returns a status string. .PARAMETER TerminalStates Hashtable mapping status => @{ Level='Ok|Warn|Error|Info'; Message='...'; Return=$true } - Message can be a string OR a scriptblock: { param($obj,$status) "..." } .PARAMETER TimeoutSeconds Total time to wait before timing out. .PARAMETER PollSeconds How often to poll the backing system. .PARAMETER TickMs Spinner refresh interval in milliseconds (interactive only). .PARAMETER Frames Spinner frames (defaults to braille spinner). .PARAMETER NotFoundToken Special internal token used when PollScript returns $null. .PARAMETER NotFoundMessage Log line emitted when transitioning into "not found" phase. .PARAMETER WaitingMessage Prefix for the animated waiting line. .PARAMETER HeartbeatSeconds If > 0, emit a "still waiting" log every N seconds even if status hasn't changed. .PARAMETER OnStatusChange Optional callback invoked when status changes. Useful for custom logs/telemetry. .PARAMETER ThrowOnTimeout If set (default), throws on timeout; otherwise returns $null or the last object (see ReturnLastOnTimeout). .PARAMETER ReturnLastOnTimeout If set, returns last seen object on timeout instead of $null (when ThrowOnTimeout is false). .PARAMETER ContextFormatter Scriptblock to build timeout context string: { param($lastObj,$lastStatus) "..." } #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Target, [Parameter(Mandatory)] [ValidateNotNull()] [scriptblock]$PollScript, [Parameter(Mandatory)] [ValidateNotNull()] [scriptblock]$GetStatus, [Parameter(Mandatory)] [ValidateNotNull()] [hashtable]$TerminalStates, [ValidateRange(1, 86400)] [int]$TimeoutSeconds = 2400, [ValidateRange(1, 3600)] [int]$PollSeconds = 20, [ValidateRange(50, 2000)] [int]$TickMs = 100, [string[]]$frames = @( '|', '/', '-', '\' ), [string]$NotFoundToken = '<notfound>', [string]$NotFoundMessage = 'Not found yet...', [string]$WaitingMessage = 'Waiting ', [ValidateRange(0, 3600)] [int]$HeartbeatSeconds = 120, [scriptblock]$OnStatusChange, [switch]$ThrowOnTimeout = $true, [switch]$ReturnLastOnTimeout, [scriptblock]$ContextFormatter = { param($lastObj, $lastStatus) "LastSeenStatus=$lastStatus" } ) # --- UI helpers --- $interactive = $Host.UI -and $Host.UI.RawUI -and -not [Console]::IsOutputRedirected $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $nextPoll = Get-Date $nextBeat = if ($HeartbeatSeconds -gt 0) { (Get-Date).AddSeconds($HeartbeatSeconds) } else { [datetime]::MaxValue } $lastStatus = $null $lastObj = $null $spin = 0 # Cursor restore safety $cursorHidden = $false $cursorWasVisible = $true function _HideCursor { if (-not $interactive) { return $false } try { $script:cursorWasVisible = [Console]::CursorVisible [Console]::CursorVisible = $false return $true } catch { return $false } } function _ShowCursor { if (-not $interactive) { return } try { [Console]::CursorVisible = [bool]$script:cursorWasVisible } catch { } } function _ClearLine { if (-not $interactive) { return } # Clear generous width and return carriage Write-Host -NoNewline ("`r" + (' ' * 140) + "`r") } function _WriteWaitLine([string]$text) { if (-not $interactive) { return } Write-Host -NoNewline ("`r{0}" -f $text) } function _EmitLog([string]$level, [string]$message) { # Assumes you have Write-Log; replace if needed Write-Log -Level $level -Message $message } try { if (-not $interactive) { # --- Non-interactive: poll loop only --- while ((Get-Date) -lt $deadline) { $obj = $null try { $obj = & $PollScript } catch { $obj = $null } $status = $NotFoundToken if ($null -ne $obj) { $lastObj = $obj try { $status = & $GetStatus $obj } catch { $status = $NotFoundToken } } if ($status -ne $lastStatus) { if ($status -eq $NotFoundToken) { _EmitLog 'Info' ("{0}: {1}" -f $Target, $NotFoundMessage) } else { _EmitLog 'Info' ("{0}: Status={1}" -f $Target, $status) } if ($OnStatusChange) { & $OnStatusChange $obj $status $lastStatus } $lastStatus = $status } elseif ((Get-Date) -ge $nextBeat) { _EmitLog 'Info' ("{0}: Still waiting (Status={1})..." -f $Target, $status) $nextBeat = (Get-Date).AddSeconds($HeartbeatSeconds) } if ($TerminalStates.ContainsKey($status)) { $meta = $TerminalStates[$status] $lvl = $meta.Level $msg = if ($meta.Message -is [scriptblock]) { & $meta.Message $obj $status } else { [string]$meta.Message } if ($msg) { _EmitLog $lvl $msg } return $obj } Start-Sleep -Seconds $PollSeconds } } else { # --- Interactive: tick loop with scheduled polls --- while ((Get-Date) -lt $deadline) { if (-not $cursorHidden) { $cursorHidden = _HideCursor } # Animate $frame = Get-DotPulse -Index $spin -Frames $Frames $spin++ $now = Get-Date $secsToPoll = [Math]::Max(0, [int][Math]::Ceiling(($nextPoll - $now).TotalSeconds)) # Poll when due if ($now -ge $nextPoll) { $obj = $null try { $obj = & $PollScript } catch { $obj = $null } $status = $NotFoundToken if ($null -ne $obj) { $lastObj = $obj try { $status = & $GetStatus $obj } catch { $status = $NotFoundToken } } if ($status -ne $lastStatus) { _ClearLine if ($status -eq $NotFoundToken) { _EmitLog 'Info' ("{0}: {1}" -f $Target, $NotFoundMessage) } else { _EmitLog 'Info' ("{0}: Status={1}" -f $Target, $status) } if ($OnStatusChange) { & $OnStatusChange $obj $status $lastStatus } $lastStatus = $status $spin = 0 } elseif ($HeartbeatSeconds -gt 0 -and $now -ge $nextBeat) { _ClearLine _EmitLog 'Info' ("{0}: Still waiting (Status={1})..." -f $Target, $status) $nextBeat = (Get-Date).AddSeconds($HeartbeatSeconds) } # Terminal? if ($TerminalStates.ContainsKey($status)) { $meta = $TerminalStates[$status] $lvl = $meta.Level $msg = if ($meta.Message -is [scriptblock]) { & $meta.Message $obj $status } else { [string]$meta.Message } _ClearLine Write-Host "" # newline after spinner line if ($msg) { _EmitLog $lvl $msg } return $obj } # Next poll schedule $nextPoll = (Get-Date).AddSeconds($PollSeconds) } # Update wait line (every TickMs) $phase = if ($lastStatus -eq $NotFoundToken) { 'not found' } elseif ($lastStatus) { $lastStatus } else { 'starting' } _WriteWaitLine ("{0}{1} ({2}, next poll in {3}s) " -f $WaitingMessage, $frame, $phase, $secsToPoll) Start-Sleep -Milliseconds $TickMs } } # Timeout $ctx = & $ContextFormatter $lastObj $lastStatus $msg = "Timed out waiting for terminal state. Target='$Target'. $ctx" if ($ThrowOnTimeout) { throw $msg } _ClearLine _EmitLog 'Warn' $msg if ($ReturnLastOnTimeout -and $lastObj) { return $lastObj } return $null } finally { if ($cursorHidden) { _ShowCursor } _ClearLine } } # SIG # Begin signature block # MIIfAgYJKoZIhvcNAQcCoIIe8zCCHu8CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD2X7204okUs8Mz # BWpTI+6XiOvtkQF+VSNz1qLP0jdONqCCGEowggUMMIIC9KADAgECAhAR+U4xG7FH # qkyqS9NIt7l5MA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNVBAMME1ZBRFRFSyBDb2Rl # IFNpZ25pbmcwHhcNMjUxMjE5MTk1NDIxWhcNMjYxMjE5MjAwNDIxWjAeMRwwGgYD # VQQDDBNWQURURUsgQ29kZSBTaWduaW5nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEA3pzzZIUEY92GDldMWuzvbLeivHOuMupgpwbezoG5v90KeuN03S5d # nM/eom/PcIz08+fGZF04ueuCS6b48q1qFnylwg/C/TkcVRo0WFcKoFGT8yGxdfXi # caHtapZfbSRh73r7qR7w0CioVveNBVgfMsTgE0WKcuwxemvIe/ptmkfzwAiw/IAC # Ib0E0BjiX4PySbwWy/QKy/qMXYY19xpRItVTKNBtXzADUtzPzUcFqJU83vM2gZFs # Or0MhPvM7xEVkOWZFBAWAubbMCJ3rmwyVv9keVDJChhCeLSz2XR11VGDOEA2OO90 # Y30WfY9aOI2sCfQcKMeJ9ypkHl0xORdhUwZ3Wz48d3yJDXGkduPm2vl05RvnA4T6 # 29HVZTmMdvP2475/8nLxCte9IB7TobAOGl6P1NuwplAMKM8qyZh62Br23vcx1fXZ # TJlKCxBFx1nTa6VlIJk+UbM4ZPm954peB/fIqEacm8LkZ0cPwmLE5ckW7hfK4Trs # o+RaudU1sKeA+FvpOWgsPccVRWcEYyGkwbyTB3xrIBXA+YckbANZ0XL7fv7x29hn # gXbZipGu3DnTISiFB43V4MhNDKZYfbWdxze0SwLe8KzIaKnwlwRgvXDMwXgk99Mi # EbYa3DvA/5ZWikLW9PxBFD7Vdr8ZiG/tRC9I2Y6fnb+PVoZKc/2xsW0CAwEAAaNG # MEQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQW # BBRfYLVE8caSc990rnrIHUjoB7X/KjANBgkqhkiG9w0BAQsFAAOCAgEAiGB2Wmk3 # QBtd1LcynmxHzmu+X4Y5DIpMMNC2ahsqZtPUVcGqmb5IFbVuAdQphL6PSrDjaAR8 # 1S8uTfUnMa119LmIb7di7TlH2F5K3530h5x8JMj5EErl0xmZyJtSg7BTiBA/UrMz # 6WCf8wWIG2/4NbV6aAyFwIojfAcKoO8ng44Dal/oLGzLO3FDE5AWhcda/FbqVjSJ # 1zMfiW8odd4LgbmoyEI024KkwOkkPyJQ2Ugn6HMqlFLazAmBBpyS7wxdaAGrl18n # 6bS7QuAwCd9hitdMMitG8YyWL6tKeRSbuTP5E+ASbu0Ga8/fxRO5ZSQhO6/5ro1j # PGe1/Kr49Uyuf9VSCZdNIZAyjjeVAoxmV0IfxQLKz6VOG0kGDYkFGskvllIpQbQg # WLuPLJxoskJsoJllk7MjZJwrpr08+3FQnLkRuisjDOc3l4VxFUsUe4fnJhMUONXT # Sk7vdspgxirNbLmXU4yYWdsizz3nMUR0zebUW29A+HYme16hzrMPOeyoQjy4I5XX # 3wXAFdworfPEr/ozDFrdXKgbLwZopymKbBwv6wtT7+1zVhJXr+jGVQ1TWr6R+8ea # tIOFnY7HqGaxe5XB7HzOwJKdj+bpHAfXft1vUoiKr16VajLigcYCG8MdwC3sngO3 # JDyv2V+YMfsYBmItMGBwvizlQ6557NbK95EwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqG # SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYg # MjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphB # cr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6p # vF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHe # HYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEd # gkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjU # jsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bR # VFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeS # LsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIV # NSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL # 6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2Zd # SoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFU # eEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw # DQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/ # T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQ # E7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9r # EVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y # 1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gx # dEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3t # y9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcy # tL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEB # YTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud # /v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiS # uEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZP # ubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsF # ADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV # BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB # MjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzEL # MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE # aWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUg # MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMr # V7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8 # dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7M # rxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZ # ZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFO # nHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+n # igNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeIt # K/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1 # zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk # 8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsW # eupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAk # prxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0G # A1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQG # fHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYB # BQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEy # NTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hB # MjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB # MA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWL # pQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgj # g8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3Q # YIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5 # bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUG # tMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNE # suEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6U # Arb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG # 0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWV # FjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5 # t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjs # arfNZzGCBg4wggYKAgEBMDIwHjEcMBoGA1UEAwwTVkFEVEVLIENvZGUgU2lnbmlu # ZwIQEflOMRuxR6pMqkvTSLe5eTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3 # AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG # AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBcAuvzXtRl # gCbMNmF4ngHC3sY8BNe3ydGhuIOBQTeD/DANBgkqhkiG9w0BAQEFAASCAgBLIaLo # 0EGTrIVGC9wgi37XAtWCUQA0hhJmLxlkpQXnXQK0hawPwkjARoKHofNIhBiAgfnr # A5mRpmyy/vA5sbcHRpNdwUIgeaUdTiLa21k6lh3JlvywNR5IT6UzX1RMAIwWsmWR # 6rQT99Jpa2nvUbPzGr6Pice7QdAAvqU2Hd0I8BdrdGwcJ2MbQlABqVfZ1zsi1bwV # Lgki5oZdb8td3HkFrJRrPwEHtBWG/kNqKUMLwO+3t4fJ+l/ZA4Zfpi0YYaMc40ZL # 411oxdSK2PScTwbyGsu/pH82akVEs3DndcL1UOrYWnTpTwPtMInytNgNsXvQ5C+p # 5eSql4b8CwpPDMeLuJlazrSlEncpGCDrtHS0ecQdJmw2cnX9jxtbyhMzpiHgIMVc # eywwXAzf5k7pP7/O34LQen5L/EnsTh0Y4Kcs5eNIcqaO0SjEx42VmcjXv4ibC2tS # lsWrwDfgDKx7xP9ojwxwKFrwPm0kup6mEHY1m9xUKIqUrFCtY9vVmy/1IH9dLEHx # EJOAEnzbuAsYk6Ndw/3uk7fUI0MLuUOoi0H+voSe0r6EGtZ8clzD7E/+cuok3BHi # C6bQ8wMIHxQJrAvjk85JgOHJpXoiQCMRrwEcaH/ejOEcw0EgCW5xq4drR0m+O2RQ # FDZH2vqtZLldabdu1RSlitZA+LjE/xRTNIa0y6GCAyYwggMiBgkqhkiG9w0BCQYx # ggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg # UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeVdGgwDQYJYIZI # AWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ # BTEPFw0yNjA2MDkyMzIwMTNaMC8GCSqGSIb3DQEJBDEiBCDpvlsbgRCbD12puchM # jME/p+9fl4tvGCOemQ1OHfhMYzANBgkqhkiG9w0BAQEFAASCAgAyz53LFfSSNbEJ # bzHpzCfe4I2GgarCx8Migd4ng6h7ZC1Q5Am/vE7PbMXbbbm5a7a2MpoV1CqlI9db # oDRKGAsuZgC/xzhRCsyGX789aHuubrSx5yu23kG+32iq73aVDX1+eZZhJQBiCai2 # aix/mEx/gwtOPpUPx1QI3ow8GOL+xaLy06gFTDL+Mp4cYTZYrIlZ29vkfoazxbFb # aNka7RQA9kULs7wy6S/h/0KKuOw1SXhiW36g4+Q3+WS+AEyCFo4iWMLwuiwe9DUW # 8cJwoJDbM43Lo37VHwure6fDAZB7JdY9J2Czy0XL30LHuPqiRD/QuX9RNbOG/6z9 # 0i318SOPNYqgFml3isxKiQwwkCvp+g+yysJcn4CdG8nsu3fALFztu9s+RZ1CzrjM # onfyy8MfY9r15+nLZ3oAWJNdENaNAnS1CvbRP2kepuwW9WKlYuH6V312TCHa1i7f # NQMbfGJIqeYtAQ8cSDxD8szP+XplXHXc3LBt4DXS7rArN+UzNbJuGmLxiDWG4J6f # 6sxdyuUR4TCdTmY2/hUi7/+0xTRMXV/07HG27Ub8EvPi4/T09ox5fvUQ5xplakUY # PijgSgbhIzP70QEi/aAE5yNE0MYsWH8IFdHr8YsYKa+aqQu/jUkibEXk2MIoXtRw # 9MXH83Ucevcsb/JggJ1SALrTbFEuTg== # SIG # End signature block |