Private/DownloadFilesInParallel.ps1

function DownloadFilesInParallel {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.Collections.ArrayList]
        $FileInfoList,

        [Parameter(Mandatory)]
        [string]
        $OutputPath,

        [Parameter()]
        [int]
        $MaxSimultaneousTransfers = 4
    )

    $ENDED_BITSJOB_STATES = @("Suspended", "Error", "TransientError", "Transferred", "Canceled")
    $ENDED_JOB_STATES = @("Blocked", "Completed", "Disconnected", "Failed", "Stopped", "Suspended")

    try {
        $startTime = Get-Date
        $minutesTaken = 0
        $currentDownloadNumber = 0
        $downloadsFinished = 0
        $totalUpdateFiles = $FileInfoList.Count
        $activeTransfers = New-Object -TypeName "System.Collections.ArrayList"
        $filesLeft = New-Object -TypeName "System.Collections.Queue"

        $FileInfoList | ForEach-Object { $filesLeft.Enqueue($_) }

        while ($filesLeft.Count -gt 0 -or $activeTransfers.Count -gt 0) {
            $elapsedTime = (Get-Date) - $startTime
            $minutesTakenNew = [int]([Math]::Floor($elapsedTime.TotalMinutes))

            if ($minutesTakenNew -gt $minutesTaken) {
                Write-Verbose "Downloading $totalUpdateFiles update file$( if ($totalUpdateFiles -ne 1) { "s" } ), $downloadsFinished update file$( if ($downloadsFinished -ne 1) { "s" } ) downloaded."
                Write-Verbose "Elapsed time: $minutesTakenNew minute$( if ($minutesTakenNew -ne 1) { "s" } )."
                $minutesTaken = $minutesTakenNew
            }

            # Start another transfer if we're below the max simultaneous transfers threshold
            if ($filesLeft.Count -gt 0 -and $activeTransfers.Count -lt $MaxSimultaneousTransfers) {
                $currentDownloadNumber += 1
                $downloadProgress = "$currentDownloadNumber/$totalUpdateFiles"

                $parsedFileInfo = $filesLeft.Dequeue()
                $fileDownloadUri = $parsedFileInfo["Uri"]
                $fileName = $parsedFileInfo["FileName"]
                $fileDownloadPath = Join-Path $OutputPath $fileName -ErrorAction Stop
                $fileDownloadPathTemp = "$fileDownloadPath.tmp"

                if (Test-Path $fileDownloadPath) {
                    Write-Verbose "Skipping already downloaded update file $fileName ($downloadProgress)."
                    continue
                }

                if (Test-Path $fileDownloadPathTemp) {
                    Write-Verbose "Restarting download for incomplete update file $fileName ($downloadProgress)..."

                    try {
                        Remove-Item -Path $fileDownloadPathTemp -ErrorAction Stop
                    } catch {
                        Write-Error -Message "Could not remove incomplete update file ($fileDownloadPathTemp). Please remove this file manually and try again."
                        return
                    }
                } else {
                    Write-Output "Downloading update file $fileName ($downloadProgress)..."
                }

                try {
                    $transferParams = GetBitsTransferSplatBase -Source $fileDownloadUri
                    $newBitsJob = Start-BitsTransfer @transferParams `
                        -DisplayName "Import-WsusUpdate parallel download for $fileName ($downloadProgress)" `
                        -Destination $fileDownloadPathTemp `
                        -Asynchronous `
                        -ErrorAction Stop
                    [void]($activeTransfers.Add(@{
                        "FileName" = $fileName
                        "TempPath" = $fileDownloadPathTemp
                        "Path" = $fileDownloadPath
                        "BitsJob" = $newBitsJob
                    }))

                    # Immediately restart the loop to queue up another job asap.
                    continue
                } catch {
                    Write-Warning "BITS transfer for update file $fileName failed with the following error: $_"
                    Write-Verbose "Full error info:"
                    Write-Verbose ($_ | Format-List -Force | Out-String)
                    Write-Warning "Retrying download once more with Invoke-WebRequest as a fallback..."

                    try {
                        $webRequestParams = GetWebRequestSplatBase -Uri $fileDownloadUri
                        $webRequestParams["OutFile"] = $fileDownloadPathTemp

                        # TODO: Deserialization in job prevents WebSession from working, although this likely won't matter right now.
                        # "Cannot convert the "Microsoft.PowerShell.Commands.WebRequestSession" value of type "Deserialized.Microsoft.PowerShell.Commands.WebRequestSession" to type "Microsoft.PowerShell.Commands.WebRequestSession".
                        $webRequestParams["SessionVariable"] = $null
                        $webRequestParams["WebSession"] = $null

                        $job = Start-Job -Name "Import-WsusUpdate parallel download fallback for $fileName ($downloadProgress)" -ErrorAction Stop -ScriptBlock {
                            [CmdletBinding()]
                            param ()
                            # Hide progress since this is a parallel background download and it will speed up
                            # Invoke-WebRequest.
                            $ProgressPreference = "SilentlyContinue"

                            $webRequestParams = $using:webRequestParams
                            [void](Invoke-WebRequest @webRequestParams -ErrorAction Stop)
                        }
                        [void]($activeTransfers.Add(@{
                            "FileName" = $fileName
                            "TempPath" = $fileDownloadPathTemp
                            "Path" = $fileDownloadPath
                            "WebRequestJob" = $job
                        }))
                    } catch {
                        Write-Warning "Failed to start fallback download for update file $fileName (URI: $fileDownloadUri) with the following error: $_"
                        throw
                    }
                }
            }

            # We don't need to start another job - track progress of our current transfers
            if ($activeTransfers.Count -gt 0) {
                $transferJustFinished = $false

                for ($index = 0; $index -lt $activeTransfers.Count; $index += 1) {
                    $activeTransfer = $activeTransfers[$index]

                    if ($null -ne $activeTransfer["BitsJob"]) {
                        $updateBitsJob = Get-BitsTransfer -JobId $activeTransfer["BitsJob"].JobId -ErrorAction Stop

                        if ($updateBitsJob.JobState -notin $ENDED_BITSJOB_STATES) {
                            continue
                        }

                        if ($updateBitsJob.JobState -ne "Transferred") {
                            Write-Error -Message "Download for update file $( $activeTransfer["FileName"] ) ended unfinished with state $( $updateBitsJob.JobState )." `
                                -Category OperationStopped `
                                -ErrorId "ParallelBITSTransferFailed"
                            return
                        }

                        Complete-BitsTransfer -BitsJob $activeTransfer["BitsJob"] -ErrorAction Stop
                    } elseif ($null -ne $activeTransfer["WebRequestJob"]) {
                        $jobState = $activeTransfer["WebRequestJob"]

                        if ($jobState.State -notin $ENDED_JOB_STATES) {
                            continue
                        }

                        if ($jobState.State -ne "Completed") {
                            Write-Error -Message "Fallback download for update file $( $activeTransfer["FileName"] ) ended unfinished with state $( $jobState.State )." `
                                -Category OperationStopped `
                                -ErrorId "ParallelInvokeWebRequestTransferFailed"
                            return
                        }

                        Remove-Job -Job $activeTransfer["WebRequestJob"] -ErrorAction SilentlyContinue -Confirm:$false
                    } else {
                        throw "Unexpected error: BitsJob and WebRequestJob were both null."
                    }

                    Write-Verbose "Finished downloading update file $( $activeTransfer["FileName"] )..."
                    Write-Verbose "Moving finished download to its proper path $( $activeTransfer["Path"] )."
                    Move-Item -Path $activeTransfer["TempPath"] -Destination $activeTransfer["Path"] -ErrorAction Stop
                    $downloadsFinished += 1
                    $activeTransfers.RemoveAt($index)
                    $index -= 1
                    $transferJustFinished = $true
                }

                if ($transferJustFinished) {
                    # Skip sleep and start new job immediately
                    continue
                }
            }

            # Check for progress updates every so often
            Start-Sleep -Milliseconds 500
        }
    } finally {
        if ($activeTransfers.Count -gt 0) {
            Write-Verbose "Cleaning up $( $activeTransfers.Count ) active transfer$( if ( $activeTransfers.Count -gt 1 ) { "s" } )."

            while ($activeTransfers.Count -gt 0) {
                if ($null -ne $activeTransfers[0]["BitsJob"]) {
                    Remove-BitsTransfer -BitsJob $activeTransfers[0]["BitsJob"] -ErrorAction SilentlyContinue
                } elseif ($null -ne $activeTransfers[0]["WebRequestJob"]) {
                    [void](Stop-Job -Job $activeTransfers[0]["WebRequestJob"] -ErrorAction SilentlyContinue -Confirm:$false)
                    Remove-Job -Job $activeTransfers[0]["WebRequestJob"] -ErrorAction SilentlyContinue -Confirm:$false
                } else {
                    Write-Verbose "Skipping removal of lingering transfer as both BitsJob and WebRequestJob were null."
                }

                $activeTransfers.RemoveAt(0)
            }
        }
    }
}

# Copyright (c) 2023-2024 AJ Tek Corporation. All Rights Reserved.

# SIG # Begin signature block
# MIIVmwYJKoZIhvcNAQcCoIIVjDCCFYgCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUGMA3TqTU5nM+at8hqp3JGrja
# 1KegghH7MIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B
# AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy
# MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh
# MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw
# MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n
# IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE
# JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7
# fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr
# YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH
# qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv
# 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J
# mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P
# OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy
# bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe
# Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc
# uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id
# FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY
# MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw
# IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE
# DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud
# HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj
# YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa
# mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+
# BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8
# ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx
# 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo
# XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p
# 1FiAhORFe1rYMIIGGjCCBAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG
# 9w0BAQwFADBWMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk
# MS0wKwYDVQQDEyRTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYw
# HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEY
# MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1Ymxp
# YyBDb2RlIFNpZ25pbmcgQ0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
# igKCAYEAmyudU/o1P45gBkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxD
# eEDIArCS2VCoVk4Y/8j6stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk
# 9vT0k2oWJMJjL9G//N523hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7Xw
# iunD7mBxNtecM6ytIdUlh08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ
# 0arWZVeffvMr/iiIROSCzKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZX
# nYvZQgWx/SXiJDRSAolRzZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+t
# AfiWu01TPhCr9VrkxsHC5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvr
# n35XGf2RPaNTO2uSZ6n9otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn
# 3UayWW9bAgMBAAGjggFkMIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaR
# XBeF5jAdBgNVHQ4EFgQUDyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQD
# AgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYD
# VR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRw
# Oi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RS
# NDYuY3JsMHsGCCsGAQUFBwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5z
# ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAj
# BggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEM
# BQADggIBAAb/guF3YzZue6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXK
# ZDk8+Y1LoNqHrp22AKMGxQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWk
# vfPkKaAQsiqaT9DnMWBHVNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3d
# MapandPfYgoZ8iDL2OR3sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwF
# kvjFV3jS49ZSc4lShKK6BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZa
# PATHvNIzt+z1PHo35D/f7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8b
# kinLrYrKpii+Tk7pwL7TjRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7Ew
# oIJB0kak6pSzEu4I64U6gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TW
# SenLbjBQUGR96cFr6lEUfAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg
# 51Tbnio1lB93079WPFnYaOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoU
# KD85gnJ+t0smrWrb8dee2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGZjCCBM6g
# AwIBAgIRAMyLU7NDPs1zQXTEeYo/k2YwDQYJKoZIhvcNAQEMBQAwVDELMAkGA1UE
# BhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGln
# byBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNjAeFw0yMjA2MzAwMDAwMDBaFw0y
# NTA2MjkyMzU5NTlaMFkxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRsw
# GQYDVQQKDBJBSiBUZWsgQ29ycG9yYXRpb24xGzAZBgNVBAMMEkFKIFRlayBDb3Jw
# b3JhdGlvbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOLV0fXklSI7
# zs4Awbc5rzQGG/XJaEgZLEXFVvzqKWlApLYSdXMLTzW14/IEoVMmi+qunmgIF98c
# 3gUFm6dTSkf+ccSIWgHwjD/pPMX4leIO1Qen+OBup4p33XRNqHsWLzBqlWmKRcLW
# 5cyRDRXedgZDvZozhxTqLhk0tFdTcBoGULA13Z/TfotbwNCx05W304VgluUogNX2
# 4yv80QYmHa6667ewXuAPNiPWQgFE4fyXOvFsRUFEtllRZmUpOPqZtGURw2ZYd+cZ
# UFn4QrC9DzlESFEjQzMMF5iUjrKCI25jq53MkTZHnOjO9KGH5SvcLo5CbgNptpzr
# d1P7AsoHFedvErNbp6YOhKrkv7F0ksVgqBh1v2Yc1ShpzZBEJ6pL+QsdnWzl6Wzb
# B8xEYEdbNF0A0/kyFHm2tYSxxCsVO4caoL2CcsA3q/1h73MBWEcNT2fV6o25L+LY
# sNUkFRc6ZRqOQ/ub9na8jCOwuhxi8MXTGo22Crinp5bbzYA/liNm97G77mJe59za
# aaoulTWzk5y06k7VgLDYtR7IgZ47Aytnj/jr7S5/P/1F2K3lbRUdGyKdifubSgjk
# m1gubK0cm3Y6L7Ar2X1nBm+PpAZqA54jQBs9cV03MVDjZb3AvjooCi9uOCJ4998U
# TV9Sn2873/GUXE6dRB+w0fD/FcUxX2pjAgMBAAGjggGsMIIBqDAfBgNVHSMEGDAW
# gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU0JE9KqcvG1A1pTeA+vcd
# t1flqK0wDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI
# KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC
# ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT
# aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz
# Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCEGA1Ud
# EQQaMBiBFmFqLmFjY291bnRpbmdAYWp0ZWsuY2EwDQYJKoZIhvcNAQEMBQADggGB
# ACa/QJzMPVDohIgQtL4/Yr7z7DlaybhjLQ2gEx7pU6G/hNQ+kClqLR3hykkJ7fwZ
# 4cYX1TF4JWnkmQ0GZwj5bk5RaSyDatQVVIW8AQNMIEEhUftHJn1REz16CzXxlgzd
# d+GTqYmwommBa6DlFO88fwF0FL3KJgvNguQGSU9sGIGUWyQuxqFUwXRgsXpwQbpR
# 1H6qsViLN1SGPXO+iqUSyejmd9mIbc8b0IxuR6rDtU2PIcU8XzwsJv8M/L1paQut
# 4m7dNw54gsoRVp+KJWawkkEVM6xvszCo8VIk8ZGetRCBT+ZSunIb/LFripb++lR8
# tIBkq8zYEhEp9U8GQZbKNZzfelix4kRt5wUq39rpBU8aHoU4GRXFs571jb/qBz/x
# AVckN5cosppJxe+AW/TR9qKrL8uKOJ9cCLRXjPdLGSmHA5XMN6ecgRE+yfLMOauX
# pUo33dCGh36TZuSi7P4uz2trEEUfmaQlr/TEp0xtbgUxTopdUYh9xagN4bbkRmoB
# izGCAwowggMGAgEBMGkwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28g
# TGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENB
# IFIzNgIRAMyLU7NDPs1zQXTEeYo/k2YwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcC
# AQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYB
# BAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFG9Yv58zkxTV
# /11MZjC/A9ZEMTW0MA0GCSqGSIb3DQEBAQUABIICAFzoeoeIOS9pDMw21Ad26w0V
# gluSPIDsunsc3eizat7JHdZx5/1xlvJSzxBYbjbx0TPLF6bguD80WDHdumcFbjZg
# 8gCMRpa+7WK3OWIYBib6ff6HwI4p9yg5BQlgwAIjgFL27+cItKwAtXHOZxj1Qz45
# X+ivU4TtdljiBfLeItmn7X3drYeQbH5R1fQScPxtMp5k2n5P43ZSnBFbpT0tUt8S
# 4Xbk/Vegu322sm/+NlTXnULRuxjBbdXAUZMV6Q9AbSSljAcfFbQALiLaxgCFYIOz
# WMD7niLtV5hDhqr9oeuWz9T7fDYtq1pD4Irb98xiorlX0xfsCTvQMrkK4k0aVpmc
# vtr93fM7jW9DXg2vUdjsYBVqRuaz0otF20APa8XekunXy672P7N1eMt2DdMQ5LRz
# 4oJ4k3UwHfhcbTc6tm24w572X+O2Lnp2N15PDK6sPhZJ9iPd+bO/p8F6VSv+rs2N
# 5GkCpdPoM73KMQ9ZZ4W7CboLntUT9OnZ/suAioIxo7xlaWt4apAyDNVolCugnfUt
# s3c2doW0hKWShvIxxumXXkr26o8S5FITLfb8xJ5piTL6QCmMH2egxnhnpHPw0Lvw
# /dx22kh5lrwBoy0dyh/ivnYDndx9dttp9rmw5y8dutYQb/Rs/MCuVjRkFVx/hUSZ
# S177IngP8isHWNtPUi6V
# SIG # End signature block