BW.Utils.PSCron.psm1

using namespace System.Management.Automation
using namespace System.Collections
using namespace Microsoft.PowerShell

$__ScriptPath = Split-Path (Get-Variable MyInvocation -Scope Script).Value.Mycommand.Definition -Parent

Add-Type -Path "$__ScriptPath\lib\Cronos-0.7.0\netstandard2.0\Cronos.dll"

# .ExternalHelp BW.Utils.PSCron-help.xml
function Get-PSCronDate {

    [OutputType( [PSCronDateTime] )]
    param(
    
        [Parameter(Position=1)]
        [datetime]
        $Date = ( Get-Date ),
        
        [Parameter(Position=2)]
        [PSCronTicks]
        $Resolution = [PSCronTicks]::Minute
        
    )

    return [PSCronDateTime]::new( $Date, $Resolution )

}


# .ExternalHelp BW.Utils.PSCron-help.xml
function Test-PSCronShouldRun {

    [OutputType( [bool] )]
    [CmdletBinding()]
    param(

        [Parameter( Mandatory, Position=1 )]
        [string]
        $Schedule,

        [Parameter( Position=2 )]
        [PSCronDateTime]
        $ReferenceDate = ( Get-PSCronDate ),

        [Parameter( ValueFromRemainingArguments, DontShow )]
        $IgnoredArguments

    )

    $ThisRun = Get-PSCronNextRun -Schedule $Schedule -ReferenceDate $ReferenceDate -Inclusive

    return $ThisRun -eq $ReferenceDate

}


# .ExternalHelp BW.Utils.PSCron-help.xml
function Get-PSCronNextRun {

    [OutputType( [PSCronDateTime] )]
    [CmdletBinding()]
    param(

        [Parameter(Mandatory, Position=1)]
        [string]
        $Schedule,

        [Parameter( Position=2 )]
        [PSCronDateTime]
        $ReferenceDate = ( Get-PSCronDate ),

        [switch]
        $Inclusive,

        [Parameter( ValueFromRemainingArguments, DontShow )]
        $IgnoredArguments

    )

    $Offset = $ReferenceDate.Local - $ReferenceDate.Utc

    $ReferenceDate = $ReferenceDate + $Offset

    $CronSchedule = [Cronos.CronExpression]::Parse( $Schedule )
    
    [PSCronDateTime]$NextRun = $CronSchedule.GetNextOccurrence( $ReferenceDate.Utc, [System.TimeZoneInfo]::Utc, $Inclusive )

    return ( $NextRun - $Offset )

}


# .ExternalHelp BW.Utils.PSCron-help.xml
function Get-PSCronSchedule {

    [OutputType( [PSCronDateTime[]] )]
    param(

        [Parameter(Mandatory, Position=1)]
        [string]
        $Schedule,

        [Parameter(Position=2)]
        [PSCronDateTime]
        $Start = ( Get-PSCronDate -Resolution Day ),

        [Parameter(Position=3)]
        [PSCronDateTime]
        $End = ( Get-PSCronDate -Date (Get-Date).AddDays( 1 ) -Resolution Day ),

        [switch]
        $IncludeStart,

        [switch]
        $IncludeEnd,

        [Parameter( ValueFromRemainingArguments, DontShow )]
        $IgnoredArguments

    )

    $CronSchedule = [Cronos.CronExpression]::Parse( $Schedule )
    
    return [PSCronDateTime[]]$CronSchedule.GetOccurrences( $Start.Utc, $End.Utc, $IncludeStart, $IncludeEnd )

}


# .ExternalHelp BW.Utils.PSCron-help.xml
function Invoke-PSCronJob {
    
    [CmdletBinding( DefaultParameterSetName='ScriptBlock' )]
    param(
    
        [Parameter( Mandatory, Position=1 )]
        [string]
        $Schedule,

        [Parameter( Mandatory, Position=2 )]
        [string]
        $Name,

        [Parameter( Mandatory, Position=3, ParameterSetName='ScriptBlock' )]
        [scriptblock]
        $Definition,

        [Parameter( Mandatory, ParameterSetName='File' )]
        [string]
        $FilePath,

        [string]
        $Description,

        [string]
        $WorkingDirectory,

        [string]
        $LogPath,

        [switch]
        $Append,

        [int]
        $TimeOut,

        [ActionPreference]
        $JobInformationPreference,

        [ActionPreference]
        $JobDebugPreference,

        [ActionPreference]
        $JobWarningPreference,

        [ActionPreference]
        $JobErrorActionPreference,

        [PSCronDateTime]
        $ReferenceDate,

        [switch]
        $PassThru
    
    )

    if ( -not( Test-PSCronShouldRun @PSBoundParameters ) ) {

        Write-Verbose ( 'SKIPPING: ' + $Name )
        return

    }

    # resolve -FilePath to a full path
    if ( $PSBoundParameters.ContainsKey( 'FilePath' ) ) {

        $PSBoundParameters['FilePath'] = $FilePath = Resolve-Path $FilePath -ErrorAction Stop |
            Select-Object -ExpandProperty Path

    }

    # resolve -WorkingDirectory to a full path
    if ( $PSBoundParameters.ContainsKey( 'WorkingDirectory' ) ) {

        $PSBoundParameters['WorkingDirectory'] = $WorkingDirectory = Resolve-Path $WorkingDirectory -ErrorAction Stop |
            Select-Object -ExpandProperty Path

    }

    # resolve -LogPath to a full path
    if ( $PSBoundParameters.ContainsKey( 'LogPath' ) ) {

        $LogFile      = Split-Path $LogPath -Leaf
        $LogDirectory = Split-Path $LogPath -Parent

        $PSBoundParameters['LogPath'] = $LogPath = Resolve-Path $LogDirectory -ErrorAction Stop |
            Select-Object -ExpandProperty Path |
            ForEach-Object {
                
                $LogDirectory = $_
                Join-Path $LogDirectory $LogFile
            
            }

    }
    
    # initialize a cron result object
    $CronJob = [PSCronJobObject]::new( $PSBoundParameters )
    $CronJob.Source = $PSCmdlet.ParameterSetName

    # create a powershell runspace
    $PowerShell = [PowerShell]::Create( [RunspaceMode]::NewRunspace )

    # create events for logging streams
    Register-ObjectEvent -InputObject $PowerShell.Streams.Information -EventName DataAdded -Action {
    
        New-Event -SourceIdentifier 'PSCronLog:Info' -MessageData $Event.Sender[-1].MessageData
    
    } > $null

    Register-ObjectEvent -InputObject $PowerShell.Streams.Verbose -EventName DataAdded -Action {
    
        New-Event -SourceIdentifier 'PSCronLog:Verbose' -MessageData $Event.Sender[-1].Message
    
    } > $null
    
    Register-ObjectEvent -InputObject $PowerShell.Streams.Debug -EventName DataAdded -Action {
    
        New-Event -SourceIdentifier 'PSCronLog:Debug' -MessageData $Event.Sender[-1].Message
    
    } > $null
    
    Register-ObjectEvent -InputObject $PowerShell.Streams.Warning -EventName DataAdded -Action {
    
        New-Event -SourceIdentifier 'PSCronLog:Warning' -MessageData $Event.Sender[-1].Message
    
    } > $null
    
    Register-ObjectEvent -InputObject $PowerShell.Streams.Error -EventName DataAdded -Action {
    
        New-Event -SourceIdentifier 'PSCronLog:Error' -MessageData ( '{0}: {1}' -f $Event.Sender[-1].FullyQualifiedErrorId, $Event.Sender[-1].Exception.Message )
        
    } > $null
    
    # create an init script for default output settings
    [ArrayList]$StreamPreferences = @(
        "`$Global:ProgressPreference = 'SilentlyContinue'"
        "`$Global:InformationPreference = '$($CronJob.JobInformationPreference)'"
        "`$Global:DebugPreference = '$($CronJob.JobDebugPreference)'"
        "`$Global:WarningPreference = '$($CronJob.JobWarningPreference)'"
        "`$Global:ErrorActionPreference = '$($CronJob.JobErrorActionPreference)'"
    )

    # if a working directory is provided we switch to that location in the init script
    if ( $CronJob.WorkingDirectory ) {

        $StreamPreferences.Add( "Set-Location -Path '$($CronJob.WorkingDirectory)' -ErrorAction Stop" ) > $null
        
    }

    # we add a variable to the $StreamPreferences with the $File name
    if ( $CronJob.FilePath ) {

        $StreamPreferences.Add( "`$Global:PSCronFile = '$($CronJob.FilePath)'" ) > $null

    }

    # add the init script
    $InitScript = [scriptblock]::Create( ( $StreamPreferences | Out-String ) )
    $PowerShell.AddScript( $InitScript, $true ) > $null
    
    # add the script
    $PowerShell.AddScript( $CronJob.Definition, $true ) > $null

    # collection for output
    # note: input cannot be assigned directly to the PSCronJobObject.Output property
    # because of the variable reference scope
    $Output = New-Object 'System.Management.Automation.PSDataCollection[psobject]'

    # run the script
    $Handle = $PowerShell.BeginInvoke( $Output, $Output )

    # wait for completion
    while ( -not $Handle.IsCompleted ) {

        Start-Sleep -Milliseconds 500

        # kill the job?
        if ( ( (Get-Date) - ([datetime]$CronJob.StartDate) ).TotalSeconds -gt $CronJob.TimeOut ) {

            Write-Warning ( '{0} has timed out, the job was stopped after {1} seconds' -f $CronJob.Name, $CronJob.TimeOut )
            $PowerShell.RunSpace.Dispose() > $null
            $PowerShell.Stop() > $null

        }
    
    }

    # record the results
    $CronJob.Output = $Output
    $CronJob.State = $PowerShell.InvocationStateInfo.State
    $CronJob.HadErrors = $PowerShell.HadErrors

    # end timestamp
    $CronJob.EndDate = Get-PSCronDate -Resolution Millisecond

    # calculate the job runtime
    $CronJob.RunTime = $CronJob.EndDate - $CronJob.StartDate

    # write status to the screen in case job is run interactively
    ''.PadRight( 80, '-' ),
    ( 'Name: ' + $CronJob.Name ),
    ( 'Description: ' + $CronJob.Description ),
    ( 'Schedule: ' + $CronJob.Schedule ),
    ( 'Reference Date: ' + $CronJob.ReferenceDate ),
    ( 'Started: ' + $CronJob.StartDate ),
    ( 'Finished: ' + $CronJob.EndDate ),
    ( 'Elapsed: {0} seconds' -f $CronJob.RunTime.TotalSeconds ),
    ( 'Result: ' + $PowerShell.InvocationStateInfo.State ),
    ( 'Errors: ' + $PowerShell.HadErrors ),
    ''.PadRight( 80, '-' ) |
    ForEach-Object { $CronJob.LogRaw( $_ ) }

    # dump the job information streams collected by the events above
    $InfoStreamIndex = 0
    Get-Event -SourceIdentifier 'PSCronLog:*' |
        Select-Object TimeGenerated, @{N='OutputStream';E={$_.SourceIdentifier.Split(':')[1].ToUpper()}}, MessageData |
        ForEach-Object {
            
            # do some hacky shit since the info events contain all the output
            # for some reason
            if ( $_.OutputStream -eq 'INFO' ) {

                # get the corresponding info object from the RunSpace
                $RunSpaceInfo = $PowerShell.Streams.Information[ $InfoStreamIndex ]

                # replace the MessageData
                $_.MessageData = $RunSpaceInfo.MessageData

                # if the $RunSpaceInfo has the 'PSHOST' tag it's from Write-Host,
                # change the OutputStream to 'HOST'
                if ( $RunSpaceInfo.Tags -contains 'PSHOST' ) {

                    $_.OutputStream = 'HOST'

                }

                # increment the counter
                $InfoStreamIndex ++

            }
        
            # send to JobLog
            $CronJob.LogMessage( $_.TimeGenerated, $_.OutputStream, $_.MessageData )
        
        }

    # if there are non-terminating errors attach them
    if ( $PowerShell.Streams.Error.Count -gt 0 ) {

        $CronJob.Errors = $PowerShell.Streams.Error |
            ForEach-Object { $_ }

    }

    # if there is a TerminatingError attach to the log file
    if ( $PowerShell.InvocationStateInfo.Reason -is [Exception] ) {

        $CronJob.LogMessage( $CronJob.EndDate, 'ERROR', $PowerShell.InvocationStateInfo.Reason.ToString() )

        $CronJob.TerminatingError = $PowerShell.InvocationStateInfo.Reason
        
    }

    # clean up events
    Get-Event -SourceIdentifier 'PSCronLog:*' | Remove-Event

    # clean up the runspace
    $PowerShell.RunSpace.Dispose() > $null
    $PowerShell.Dispose() > $null

    # pass through the job?
    if ( $PassThru ) { $CronJob }

}


# .ExternalHelp BW.Utils.PSCron-help.xml
function Send-PSCronNotification {

    param(

        [Parameter( Mandatory, ValueFromPipeline )]
        [PSCronJobObject[]]
        $CronJob,

        [Parameter( Mandatory )]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $To,

        [ValidateNotNullOrEmpty()]
        [string[]]
        $Cc,

        [ValidateNotNullOrEmpty()]
        [string[]]
        $Bcc,

        [Parameter( Mandatory )]
        [ValidateNotNullOrEmpty()]
        [string]
        $From,

        [Alias( 'ComputerName' )]
        [ValidateNotNullOrEmpty()]
        [string]
        $SmtpServer,

        [ValidateNotNullOrEmpty()]
        [System.Net.Mail.MailPriority]
        $Priority,

        [Alias( 'DNO' )]
        [ValidateNotNullOrEmpty()]
        [System.Net.Mail.DeliveryNotificationOptions]
        $DeliveryNotificationOption,

        [Alias('sub')]
        [ValidateNotNullOrEmpty()]
        [string]
        $Subject = '[CRON] {0}',

        [ValidateNotNullOrEmpty()]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [switch]
        $UseSsl,

        [ValidateRange(0, 2147483647)]
        [int]
        $Port,

        [switch]
        $PassThru
        
    )

    begin {

        $SendOptions = @{
            BodyAsHtml = $true
        }
        'To', 'Cc', 'Bcc', 'From', 'SmtpServer', 'Priority', 'DeliveryNotificationOption', 'Credential', 'Port' |
            Where-Object { $_ -in $PSBoundParameters.Keys } |
            ForEach-Object { $SendOptions.$_ = $PSBoundParameters.$_ }

    }

    process {

        $CronJob | ForEach-Object {

            $_.LogMessage( (Get-Date), 'INFO', 'Send-PSCronNotification - Sending notifications...' )

            Send-MailMessage `
                -Subject ( $Subject -f $_.Name ) `
                -Body ( '<pre>{0}</pre>' -f ( $_.Log  ) ) `
                @SendOptions

            if ( $PassThru ) { $_ }

        }

    }
    
}

# SIG # Begin signature block
# MIIfWwYJKoZIhvcNAQcCoIIfTDCCH0gCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUhVMUNe3MQBe4X5orJEDQVlD0
# GPSgghlDMIIFTDCCBDSgAwIBAgIRAJXsrVtF2nXJk88FisVsbxUwDQYJKoZIhvcN
# AQELBQAwfDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3Rl
# cjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSQw
# IgYDVQQDExtTZWN0aWdvIFJTQSBDb2RlIFNpZ25pbmcgQ0EwHhcNMTkxMDE1MDAw
# MDAwWhcNMjAxMDA3MjM1OTU5WjCBlDELMAkGA1UEBhMCVVMxDjAMBgNVBBEMBTYw
# MTIwMREwDwYDVQQIDAhJbGxpbm9pczEOMAwGA1UEBwwFRWxnaW4xGjAYBgNVBAkM
# ETEyODcgQmxhY2toYXdrIERyMRowGAYDVQQKDBFTaGFubm9uIEdyYXlicm9vazEa
# MBgGA1UEAwwRU2hhbm5vbiBHcmF5YnJvb2swggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDUDfCIlFqVcaV4Jg4top2UPmJmuzCm7gKFKcLHxe+RZ4tH9cFB
# K95slekX32x1XDBR6i4STWLkQDYAhTzwOgTDT7VfELoMBTBgBVeajcz52S87A1ce
# tbBJGo3CaEAEdV6MY4icqyLWNhrcM8jZ0UEvX1OuDR7PSheQxXBsoYOnC31TtZtu
# O94twA+em9Hrl2TcmFv1uYIyKsKcAuNo46cXOUJSJqDH2cOzPPDt9hECV/oQk2eJ
# zDAG6M6YDn2r26vAC4QDv9VdMAykTbCaFWwzjGqxxvMA+zs9JWCQ5OtlHprnWvUd
# ILOtQshJVkrEXSYPj/qo4SWoz9f+d3urt2ZJAgMBAAGjggGuMIIBqjAfBgNVHSME
# GDAWgBQO4TqoUzox1Yq+wbutZxoDha00DjAdBgNVHQ4EFgQUkeJMtbtdIm9ef+Gl
# DOHtvFIhRnYwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAww
# CgYIKwYBBQUHAwMwEQYJYIZIAYb4QgEBBAQDAgQQMEAGA1UdIAQ5MDcwNQYMKwYB
# BAGyMQECAQMCMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BT
# MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGln
# b1JTQUNvZGVTaWduaW5nQ0EuY3JsMHMGCCsGAQUFBwEBBGcwZTA+BggrBgEFBQcw
# AoYyaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNBQ29kZVNpZ25pbmdD
# QS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCYGA1Ud
# EQQfMB2BG3NoYW5ub24uZ3JheWJyb29rQGdtYWlsLmNvbTANBgkqhkiG9w0BAQsF
# AAOCAQEAI2bbcfka5PxMeiZ9rEEN2A3/iDgWCwKUuzGEDJHJbGqcTi53Gt7HUOrc
# VC11eh5cU9poVhUf/O+YQW6H6jPrkSOiajZY9I5UqIXo1sunFJwdbZzX7I40cOqf
# 9Ma3HT5Gt9SA4WBqj8DO5GrF6L3QKi2BR7vdymYbQk92ZXX6GcwCGiOg7iBuF91v
# r3Lx8JoPMuhObJvPUk59yKdHXZMue01X4N5/YJdo2g5XZU0qlq4CIiHYGQrsuhpv
# IG0w1Bsw0Jjfd3I2r1Xu29XF1g6U9GgA0HdJ8BM+0pAKuLHtPzmqsWhEcEJLFN9g
# UezIZBWejqWYx9Cy0nqr5NAnPWAVxTCCBfUwggPdoAMCAQICEB2iSDBvmyYY0ILg
# ln0z02owDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpO
# ZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVT
# RVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmlj
# YXRpb24gQXV0aG9yaXR5MB4XDTE4MTEwMjAwMDAwMFoXDTMwMTIzMTIzNTk1OVow
# fDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
# A1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSQwIgYDVQQD
# ExtTZWN0aWdvIFJTQSBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUA
# A4IBDwAwggEKAoIBAQCGIo0yhXoYn0nwli9jCB4t3HyfFM/jJrYlZilAhlRGdDFi
# xRDtsocnppnLlTDAVvWkdcapDlBipVGREGrgS2Ku/fD4GKyn/+4uMyD6DBmJqGx7
# rQDDYaHcaWVtH24nlteXUYam9CflfGqLlR5bYNV+1xaSnAAvaPeX7Wpyvjg7Y96P
# v25MQV0SIAhZ6DnNj9LWzwa0VwW2TqE+V2sfmLzEYtYbC43HZhtKn52BxHJAteJf
# 7wtF/6POF6YtVbC3sLxUap28jVZTxvC6eVBJLPcDuf4vZTXyIuosB69G2flGHNyM
# fHEo8/6nxhTdVZFuihEN3wYklX0Pp6F8OtqGNWHTAgMBAAGjggFkMIIBYDAfBgNV
# HSMEGDAWgBRTeb9aqitKz1SA4dibwJ3ysgNmyzAdBgNVHQ4EFgQUDuE6qFM6MdWK
# vsG7rWcaA4WtNA4wDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
# HQYDVR0lBBYwFAYIKwYBBQUHAwMGCCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYEVR0g
# ADBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNF
# UlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwdgYIKwYBBQUHAQEE
# ajBoMD8GCCsGAQUFBzAChjNodHRwOi8vY3J0LnVzZXJ0cnVzdC5jb20vVVNFUlRy
# dXN0UlNBQWRkVHJ1c3RDQS5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVz
# ZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggIBAE1jUO1HNEphpNveaiqMm/EA
# AB4dYns61zLC9rPgY7P7YQCImhttEAcET7646ol4IusPRuzzRl5ARokS9At3Wpwq
# QTr81vTr5/cVlTPDoYMot94v5JT3hTODLUpASL+awk9KsY8k9LOBN9O3ZLCmI2pZ
# aFJCX/8E6+F0ZXkI9amT3mtxQJmWunjxucjiwwgWsatjWsgVgG10Xkp1fqW4w2y1
# z99KeYdcx0BNYzX2MNPPtQoOCwR/oEuuu6Ol0IQAkz5TXTSlADVpbL6fICUQDRn7
# UJBhvjmPeo5N9p8OHv4HURJmgyYZSJXOSsnBf/M6BZv5b9+If8AjntIeQ3pFMcGc
# TanwWbJZGehqjSkEAnd8S0vNcL46slVaeD68u28DECV3FTSK+TbMQ5Lkuk/xYpMo
# JVcp+1EZx6ElQGqEV8aynbG8HArafGd+fS7pKEwYfsR7MUFxmksp7As9V1DSyt39
# ngVR5UR43QHesXWYDVQk/fBO4+L4g71yuss9Ou7wXheSaG3IYfmm8SoKC6W59J7u
# mDIFhZ7r+YMp08Ysfb06dy6LN0KgaoLtO0qqlBCk4Q34F8W2WnkzGJLjtXX4oemO
# CiUe5B7xn1qHI/+fpFGe+zmAEc3btcSnqIBv5VPU4OOiwtJbGvoyJi1qV3AcPKRY
# LqPzW0sH3DJZ84enGm1YMIIG7DCCBNSgAwIBAgIQMA9vrN1mmHR8qUY2p3gtuTAN
# BgkqhkiG9w0BAQwFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJz
# ZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNU
# IE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBB
# dXRob3JpdHkwHhcNMTkwNTAyMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjB9MQswCQYD
# VQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdT
# YWxmb3JkMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxJTAjBgNVBAMTHFNlY3Rp
# Z28gUlNBIFRpbWUgU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQDIGwGv2Sx+iJl9AZg/IJC9nIAhVJO5z6A+U++zWsB21hoEpc5Hg7Xr
# xMxJNMvzRWW5+adkFiYJ+9UyUnkuyWPCE5u2hj8BBZJmbyGr1XEQeYf0RirNxFrJ
# 29ddSU1yVg/cyeNTmDoqHvzOWEnTv/M5u7mkI0Ks0BXDf56iXNc48RaycNOjxN+z
# xXKsLgp3/A2UUrf8H5VzJD0BKLwPDU+zkQGObp0ndVXRFzs0IXuXAZSvf4DP0REK
# V4TJf1bgvUacgr6Unb+0ILBgfrhN9Q0/29DqhYyKVnHRLZRMyIw80xSinL0m/9NT
# IMdgaZtYClT0Bef9Maz5yIUXx7gpGaQpL0bj3duRX58/Nj4OMGcrRrc1r5a+2kxg
# zKi7nw0U1BjEMJh0giHPYla1IXMSHv2qyghYh3ekFesZVf/QOVQtJu5FGjpvzdeE
# 8NfwKMVPZIMC1Pvi3vG8Aij0bdonigbSlofe6GsO8Ft96XZpkyAcSpcsdxkrk5WY
# nJee647BeFbGRCXfBhKaBi2fA179g6JTZ8qx+o2hZMmIklnLqEbAyfKm/31X2xJ2
# +opBJNQb/HKlFKLUrUMcpEmLQTkUAx4p+hulIq6lw02C0I3aa7fb9xhAV3PwcaP7
# Sn1FNsH3jYL6uckNU4B9+rY5WDLvbxhQiddPnTO9GrWdod6VQXqngwIDAQABo4IB
# WjCCAVYwHwYDVR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYE
# FBqh+GEZIA/DQXdFKI7RNV8GEgRVMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8E
# CDAGAQH/AgEAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYEVR0g
# ADBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNF
# UlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwdgYIKwYBBQUHAQEE
# ajBoMD8GCCsGAQUFBzAChjNodHRwOi8vY3J0LnVzZXJ0cnVzdC5jb20vVVNFUlRy
# dXN0UlNBQWRkVHJ1c3RDQS5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVz
# ZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggIBAG1UgaUzXRbhtVOBkXXfA3oy
# Cy0lhBGysNsqfSoF9bw7J/RaoLlJWZApbGHLtVDb4n35nwDvQMOt0+LkVvlYQc/x
# QuUQff+wdB+PxlwJ+TNe6qAcJlhc87QRD9XVw+K81Vh4v0h24URnbY+wQxAPjeT5
# OGK/EwHFhaNMxcyyUzCVpNb0llYIuM1cfwGWvnJSajtCN3wWeDmTk5SbsdyybUFt
# Z83Jb5A9f0VywRsj1sJVhGbks8VmBvbz1kteraMrQoohkv6ob1olcGKBc2NeoLvY
# 3NdK0z2vgwY4Eh0khy3k/ALWPncEvAQ2ted3y5wujSMYuaPCRx3wXdahc1cFaJqn
# yTdlHb7qvNhCg0MFpYumCf/RoZSmTqo9CfUFbLfSZFrYKiLCS53xOV5M3kg9mzSW
# mglfjv33sVKRzj+J9hyhtal1H3G/W0NdZT1QgW6r8NDT/LKzH7aZlib0PHmLXGTM
# ze4nmuWgwAxyh8FuTVrTHurwROYybxzrF06Uw3hlIDsPQaof6aFBnf6xuKBlKjTg
# 3qj5PObBMLvAoGMs/FwWAKjQxH/qEZ0eBsambTJdtDgJK0kHqv3sMNrxpy/Pt/36
# 0KOE2See+wFmd7lWEOEgbsausfm2usg1XTN2jvF8IAwqd661ogKGuinutFoAsYyr
# 4/kKyVRd1LlqdJ69SK6YMIIHBjCCBO6gAwIBAgIQPRo1cjAVgmMw0BNxfoJBCDAN
# BgkqhkiG9w0BAQwFADB9MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBN
# YW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRgwFgYDVQQKEw9TZWN0aWdvIExp
# bWl0ZWQxJTAjBgNVBAMTHFNlY3RpZ28gUlNBIFRpbWUgU3RhbXBpbmcgQ0EwHhcN
# MTkwNTAyMDAwMDAwWhcNMzAwODAxMjM1OTU5WjCBhDELMAkGA1UEBhMCR0IxGzAZ
# BgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEYMBYG
# A1UECgwPU2VjdGlnbyBMaW1pdGVkMSwwKgYDVQQDDCNTZWN0aWdvIFJTQSBUaW1l
# IFN0YW1waW5nIFNpZ25lciAjMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAMtRUP9W/vx4Y3ABk1qeGPQ7U/YHryFs9aIPfR1wLYR0SIucipUFPVmE+ZGA
# eVEs2Yq3wQuaugqKzWZPA4sBuzDKq73bwE8SXvwKzOJFsAE4irtN59QcVJjtOVjP
# W8IvRZgxCvk1OLgxLm20Hjly4bgqvp+MjBqlRq4LK0yZ/ixL/Ci5IjpmF9CqVooh
# wPOWJLTQhSZruvBvZJh5pq29XNhTaysK1nKKhUbjDRgG2sZ7QVY2mxU+8WoRoPdm
# 9RjQgFVjh2hm6w55VYJco+1JuHGGnpM3sGuj6mJso66W6Ln9i6vG9llbADxXIBgt
# cAOnnO+S63mhx13sfLSPS9/rXfyjIN2SOOVqUTprhZxMoJgIaVsG5yoZ0JWTiztr
# igUJKdjW2tvjcvpcSi97FVaGMr9/BQmdLSrPUOHmYSDbxwaAXE4URr6uV3Giqmww
# kxx+d8sG6VfNkfXVM3Ic4drKbuvzD+x5W7snnuge/i/yu3/p5dBn67gNfKQrWQOL
# le0iKM36LDvHFhGv49axUGdpxY71edCt/4fM+H+q+aLtYfjIjWnasfRRketnV9Fk
# EetkywO9SVU6RUMYLCVs0S8MLW/1QTUkoPJjWRZf2aTpLE7buzESxm34W24D3MsV
# jxuNcuzbDxWQ1hJO7uIAMSWTNW9qW6USY0ABirlpiDqIuA8ZAgMBAAGjggF4MIIB
# dDAfBgNVHSMEGDAWgBQaofhhGSAPw0F3RSiO0TVfBhIEVTAdBgNVHQ4EFgQUb02G
# B9gyJ54sKdLQEwOAgd0FgykwDgYDVR0PAQH/BAQDAgbAMAwGA1UdEwEB/wQCMAAw
# FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwQAYDVR0gBDkwNzA1BgwrBgEEAbIxAQIB
# AwgwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwRAYDVR0f
# BD0wOzA5oDegNYYzaHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNBVGlt
# ZVN0YW1waW5nQ0EuY3JsMHQGCCsGAQUFBwEBBGgwZjA/BggrBgEFBQcwAoYzaHR0
# cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNBVGltZVN0YW1waW5nQ0EuY3J0
# MCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0B
# AQwFAAOCAgEAwGjts9jUUJvv03XLDzv3JNN6N0WNLO8W+1GpLB+1JbWKn10Lwhsg
# dI1mDzbLqvY2DQ9+j0tKdENlrA0q9grta23FCTjtABv45dymCkAFR++Eygm8Q2aD
# v5/t24490UFksXACLQNXWxhvHCzLHrIA6LoJL1uBBDW5qWNtjgjFGNHhIaz5EgoU
# wBLbfiWdrB0QwFqlg9IfGmZV/Jsq4uw3V47l35Yw+MCTC0MY+QJvqVGvuFcK8xwH
# aTmPN5xt15GupS5J6Ures9CMvzmQDcCBzvAqBzoMpi1R0nLzU8b5ve/vDGlJd58s
# VsTpoQg9B67FHtaEIse8fUMbWDhiTtEFJYTFQvgfL/bb+quMVOxFimwSTTBaUuWk
# Fwki5u9v9V+GQ9+hLb1KRpKggZYsYZd/QG/YP4w1WqvRxqA7hWZUgO8fGvXxm7Ch
# J32y5wvP9i2cWBOUqYb8RVKiKG1/dA9SkUl66RL4qTuwkv19kRTpW21IlPLIlu4F
# OLPF7DA/4QcgBLHYi7z9sz5v8gJTBvSg7cmacqOXXwD7y2PQ6M10/XXJ1DZFunsS
# WXLt5/J6UAB4+EOaRtjfv1TUXrHH0bwbg/Qr5wvoR8hTnswarPb6inVTbCCFqdW4
# arokjoorCJGfNwQc9m+i3TSqkf/GFS4eQhoJKU/0xs3ikaLTQAyOeOMxggWCMIIF
# fgIBATCBkTB8MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVz
# dGVyMRAwDgYDVQQHEwdTYWxmb3JkMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQx
# JDAiBgNVBAMTG1NlY3RpZ28gUlNBIENvZGUgU2lnbmluZyBDQQIRAJXsrVtF2nXJ
# k88FisVsbxUwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAw
# GQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisG
# AQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFIBI17QeseMX0I4zpf0vbylLiB2zMA0G
# CSqGSIb3DQEBAQUABIIBANKQbgJbEAqLEsDlGJijlbnAqiz/APUyFg4hAO7rI+JO
# aJyIEsHg4GY2+Y+TGZs66MdlrGv2RsQ0r77q6OrYhngd1QJLYCP4Qb9cBMbO9eHq
# wbbs7bshWRkZoYj5WSapqCh90Ufe9PW6bzuRTEjmNNe6+nXr5Uy7he6TQUCz5XIY
# oqJPwBHf+S9vDE53pa8Bvgbu8wxFPrlITAakRTF/ktciy4APKsOCDndy21b3tZB9
# HiDMG6G6xG1TjYmovpQkfVlvYxP100CmeHqhW0rGWtXdaZdCrgcW7qXJ0b3m8pvx
# LwwtsQ0HeASUK60f1JhnvVnCO8b48t7hFk+A+U9ICEKhggNLMIIDRwYJKoZIhvcN
# AQkGMYIDODCCAzQCAQEwgZEwfTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0
# ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGln
# byBMaW1pdGVkMSUwIwYDVQQDExxTZWN0aWdvIFJTQSBUaW1lIFN0YW1waW5nIENB
# AhA9GjVyMBWCYzDQE3F+gkEIMA0GCWCGSAFlAwQCAgUAoHkwGAYJKoZIhvcNAQkD
# MQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjAwNjEzMjAzODE0WjA/Bgkq
# hkiG9w0BCQQxMgQwlbpFwLmhUIyHXfI4VW38kQnie/mPlJmzCapX5mjfArkCMxL9
# auN3dy9ZbAUadM3RMA0GCSqGSIb3DQEBAQUABIICAGEKIVskhz7jzxECqRRkGyul
# RJGIEblUP5Vh4GTRlXc3EHfkBwy4+acb3RcHCq9r5UM05/JbvptC4zPReK5CZig7
# Y1i6HXryQuC69uXbmFWmcSJFLpjTSApQA/B4KfmFgF6+h08PVqdLodKDfWF79E4l
# HV8iHJgBU1UGaGCRVvzuKNFugBl/+kPU23Dd2tB3ppkS5HQG4RRkrQag1jPxXfmK
# 3oMTCaTFvH1P4obkKnKkIYwkfGVAyuEtNdeov1iEc91rc2kZPxv6sLBKTiLf2Ye8
# YemlyKqJd4WTtH7mhkM1LBWAOc11lTr6RQsdB4KYymGOr/YoNEoTDFiGb87nodxE
# 1cMnlD1XoQSTSQj08xdCEEhbcvKb6oIL18lq2+eo27DQy+HG44HSf+r9VjktdRQp
# FNLT0ZpiUZNkLhnhUFaN316qUjZt5ELLoy47/wby8fIYKRvQ7hADH1w6C8/KT1Zo
# AljwFSDvQorQ9/HosdiBUE4gOglimwG5Zvy5NPI3rWbn2aFSxGV9C2aQaX2D7bxp
# QHOcqnOTyy/ae+E+YUCo8PIZEkqEt8iRP1VUjaHd+l3gl3CLxlZeZfDroeZVL0xC
# SFf8qjRSiJPDCfDgobuaT6NB3BOsEbRzi/8ubEpVWO0OfoH22HKrdctZYaOm2txr
# c1zqjDNAGUu8Zed7hUbh
# SIG # End signature block