Execution.psm1

function Set-ScriptArgs {
    [CmdletBinding()]
    param(
        [Parameter()]
        [System.Collections.Generic.Dictionary`2[System.String, System.Object]]
        $BoundParameters,

        [Parameter()]
        [System.Collections.Generic.List`1[System.Object]]
        $UnboundArguments
    )

    $global:PSCommandPath = $MyInvocation.PSCommandPath
    Log trace "global:PSCommandPath: '$global:PSCommandPath'"

    $argumentList = [System.Collections.ArrayList]@()

    $parameters = (Get-Command $global:PSCommandPath).Parameters

    Log trace 'Parse bound parameters...'
    foreach ($key in $BoundParameters.Keys) {
        $type = $parameters.Values | Where-Object { $_.Name -eq $key } | Select-Object -ExpandProperty ParameterType

        Log trace "Parametertype: '$($type.FullName)'"
        switch ($type) {
            ([System.Management.Automation.SwitchParameter]) { $value = '$true' }

            ([System.Boolean]) {
                try {
                    $boolValue = [System.Convert]::ToBoolean($BoundParameters[$key])
                } catch {
                    $boolValue = $false
                }

                $value = if ($boolValue) { '$true' } else { '$false' }
            }

            default { $value = $BoundParameters[$key] }
        }

        Log trace "-$key`:$value"
        $argumentList.Add("-$key`:$value") > $null
    }
    Log trace 'Parse bound parameters... Done.'

    Log trace 'Parse unbound arguments...'
    foreach ($arg in $UnboundArguments) {
        Log trace "$arg"
        $argumentList.Add($arg) > $null
    }
    Log trace 'Parse unbound arguments... Done.'

    $global:ScriptArgs = $argumentList
    Log trace "global:ScriptArgs: '$global:ScriptArgs'"
}

function Invoke-SelfElevation() {
    # Self-elevate the script if required
    if ($PSVersionTable.Platform -eq 'unix') {
        if ((id -u) -ne 0) {
            Log trace 'Try self elevation on Unix platform...'

            $executionInfo = Get-ExecutionInfo
            & sudo $executionInfo.Executable @($executionInfo.ArgumentList)
            exit 0
        }
    } else {
        if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'administrator')) {
            if ([int] (Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
                Log trace 'Try self elevation on Windows platform...'

                $executionInfo = Get-ExecutionInfo

                $hashAndFile = Get-HashAndFile -Executable $executionInfo.Executable -Arguments $executionInfo.Arguments
                Log trace "Write mutex file with hash to '$($hashAndFile.File)'"
                New-Item -Path $hashAndFile.File -ItemType File -Force > $null

                Log trace "Start-Process -FilePath $($executionInfo.Executable) -ArgumentList $($executionInfo.Arguments) -WorkingDirectory $PSScriptRoot -Verb runas"
                Start-Process -FilePath $executionInfo.Executable -ArgumentList $executionInfo.Arguments -WorkingDirectory $PSScriptRoot -Verb runas
                exit 0
            }
        }
    }
}

function Exit-WithAndWaitOnExplorer([int] $ExitCode) {
    if ($PSVersionTable.Platform -ne 'unix') {
        $executionInfo = Get-ExecutionInfo

        $hashAndFile = Get-HashAndFile -Executable $executionInfo.Executable -Arguments $executionInfo.Arguments
        Log trace "Check if mutex file exists '$($hashAndFile.FileName)'..."
        $mutexFileExists = Test-Path -Path $hashAndFile.File
        if ($mutexFileExists) {
            Log trace "Mutex file exists '$($hashAndFile.File)'"
            Remove-Item -Path $hashAndFile.File -Force > $null
        }

        if ((Get-ScriptIsCalledFromUI) -or $mutexFileExists) {
            Log info 'Press any key to continue . . . '
            $HOST.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') > $null
            $HOST.UI.RawUI.FlushInputBuffer()
        }
    }

    exit $ExitCode
}

function Get-ScriptIsCalledFromUI() {
    # Check distance from current process to 'explorer.exe' to
    # determine if the user start the script in explorer or UI.
    # If the hierarchy is like this above, then we can determine
    # that the user executed the script from 'explorer.exe' UI.
    #
    # 1) pwsh.exe|powershell.exe
    # 2) -> cmd.exe
    # 3) -> explorer.exe

    $calledFromUI = $false

    $processNames = [System.Collections.ArrayList]@()

    $currentProcessId = $PID

    $indent = 2
    Log trace 'ProcessHierarchy:'
    while ($true) {
        $currentProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $currentProcessId"
        $currentProcessName = $currentProcess.Name

        if (-not $currentProcessId -or -not $currentProcessName) {
            # We are on the top -> break
            break
        }

        Log trace "$(''.PadLeft($indent))$currentProcessName -> $currentProcessId"

        $processNames.Add($currentProcessName) > $null

        $indent += 2
        $currentProcessId = $currentProcess.ParentProcessId
    }

    $expectedProcessNames = @('(pwsh.exe)|(powershell.exe)', 'cmd.exe', 'explorer.exe')

    if ($processNames.Count -ge $expectedProcessNames.Count) {
        $indent = 2
        Log trace 'Check ProcessHierarchy:'
        for ($i = 0; $i -lt $expectedProcessNames.Count; $i++) {
            if ($processNames[$i] -imatch $expectedProcessNames[$i]) {
                $calledFromUI = $true
                Log trace "$(''.PadLeft($indent))$($processNames[$i]) -> $($expectedProcessNames[$i]) -> OK"
            } else {
                $calledFromUI = $false
                Log trace "$(''.PadLeft($indent))$($processNames[$i]) -> $($expectedProcessNames[$i]) -> FAIL"
                break
            }

            $indent += 2
        }
    }

    return $calledFromUI
}

function Get-ExecutionInfo() {
    $executable = Get-Process -Id $PID | Select-Object -ExpandProperty MainModule | Select-Object -ExpandProperty FileName
    $argumentList = @('-NoProfile', '-ExecutionPolicy', 'Unrestricted', '-Command', "`"& `"$global:PSCommandPath`" $global:ScriptArgs`"")
    $arguments = "$argumentList"
    Log trace "executable: $executable"
    Log trace "argumentList: $argumentList"
    Log trace "arguments: $arguments"

    $executionInfo = [pscustomobject][ordered]@{
        Executable   = $executable
        ArgumentList = $argumentList
        Arguments    = $arguments
    }
    return $executionInfo
}

function Get-HashAndFile([string] $Executable, [string] $Arguments) {
    Log trace "Get-HashAndFile -> `$Executable: '$Executable' | `$Arguments: '$Arguments'"

    $value = "$Executable $Arguments".ToLowerInvariant()
    $fileName = "psec-$(Get-Checksum -Value $value)"
    $file = (Join-Path $env:TEMP $fileName)

    $object = [pscustomobject][ordered]@{
        FileName = $fileName
        File     = $file
    }
    return $object
}

function Set-PSScriptID() {
    Log trace 'Set-PSScriptID'

    $callStack = Get-PSCallStack

    $callerInvocation = $callStack[1].InvocationInfo

    $commandPath = $callStack[0].InvocationInfo.PSCommandPath
    Log trace " commandPath: '$commandPath'"

    $argumentList = [System.Collections.ArrayList]@()

    $parameters = (Get-Command $commandPath).Parameters

    Log trace ' Parse bound parameters...'
    foreach ($key in $callerInvocation.BoundParameters.Keys) {
        $type = $parameters.Values | Where-Object { $_.Name -eq $key } | Select-Object -ExpandProperty ParameterType

        Log trace " Parametertype: '$($type.FullName)'"
        switch ($type) {
            ([System.Management.Automation.SwitchParameter]) { $value = '$true' }

            ([System.Boolean]) {
                try {
                    $boolValue = [System.Convert]::ToBoolean($callerInvocation.BoundParameters[$key])
                } catch {
                    $boolValue = $false
                }

                $value = if ($boolValue) { '$true' } else { '$false' }
            }

            default { $value = $callerInvocation.BoundParameters[$key] }
        }

        Log trace " -$key`:$value"
        $argumentList.Add("-$key`:$value") > $null
    }

    Log trace ' Parse unbound arguments...'
    foreach ($arg in $callerInvocation.UnboundArguments) {
        Log trace "$arg"
        $argumentList.Add($arg) > $null
    }

    # Construct command and arguments
    $executable = Get-Process -Id $PID | Select-Object -ExpandProperty MainModule | Select-Object -ExpandProperty FileName
    # Normalize executable
    $executable = "`"$(Get-CanonicalPath $executable)`""
    Log trace " executable: '$executable'"

    $argumentList = @('-NoProfile', '-ExecutionPolicy', 'Unrestricted', '-Command', "`"& `"`"`"$commandPath`"`"`" $argumentList`"")
    Log trace " argumentList: '$argumentList'"

    $fileHash = Get-FileHashFromCommand $executable $argumentList

    $scriptID = [guid]::NewGuid().Guid.Replace('-', '')

    Log trace " Set PSScriptInfos for scriptID: '$scriptID'"
    $global:PSScriptInfos[$scriptID] = [pscustomobject][ordered]@{
        Executable   = $executable
        ScriptPath   = $commandPath
        ArgumentList = $argumentList
    }
    $global:PSScriptInfos[$scriptID] | Add-Member -MemberType ScriptProperty -Name 'Arguments' -Value { "$($this.ArgumentList)" }
    $global:PSScriptInfos[$scriptID] | Add-Member -MemberType NoteProperty -Name 'FileHash' -Value $fileHash

    Log trace " Executable: '$($global:PSScriptInfos[$scriptID].Executable)'"
    Log trace " ScriptPath: '$($global:PSScriptInfos[$scriptID].ScriptPath)'"
    Log trace " ArgumentList: '$($global:PSScriptInfos[$scriptID].ArgumentList)'"
    Log trace " Arguments: '$($global:PSScriptInfos[$scriptID].Arguments)'"
    Log trace " FileHash: '$($global:PSScriptInfos[$scriptID].FileHash)'"

    Set-Variable -Name 'PSScriptID' -Value $scriptID -Scope ($callStack.Count - 2)
}

function Get-CanonicalPath([string] $Path) {
    if (-not (Test-Path -Path $Path -PathType Any)) {
        return $Path
    }

    $dir = [System.IO.DirectoryInfo]::new($Path)
    if ($null -ne $dir.Parent) {
        return Join-Path (Get-CanonicalPath -Path $dir.Parent.FullName) ($dir.Parent.GetFileSystemInfos($dir.Name)[0].Name)
    } else {
        return $dir.Name.ToUpperInvariant()
    }
}

function Restart-SelfElevated() {
    Log trace 'Restart-SelfElevated'

    $scriptID = Get-Variable -Name 'PSScriptID' -ValueOnly -Scope ((Get-PSCallStack).Count - 2)
    Log trace " -> `$scriptID: '$scriptID'"

    # Self-elevate the script if required
    if ($PSVersionTable.Platform -eq 'unix') {
        if ((id -u) -ne 0) {
            Log trace ' Try self elevation on Unix platform...'

            Log trace " Get PSScriptInfos for scriptID: '$scriptID'"
            $scriptInfo = $global:PSScriptInfos[$scriptID]

            & sudo $scriptInfo.Executable @($scriptInfo.ArgumentList)
            exit 0
        }
    } else {
        if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'administrator')) {
            if ([int] (Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
                Log trace ' Try self elevation on Windows platform...'

                Log trace " Get PSScriptInfos for scriptID: '$scriptID'"
                $scriptInfo = $global:PSScriptInfos[$scriptID]
                Log trace " Write mutex file -> '$($scriptInfo.FileHash.File)'"
                New-Item -Path $scriptInfo.FileHash.File -ItemType File -Force > $null

                Log trace " -> Start-Process -FilePath $($scriptInfo.Executable) -ArgumentList $($scriptInfo.Arguments) -WorkingDirectory $PSScriptRoot -Verb runas"
                Start-Process -FilePath $scriptInfo.Executable -ArgumentList $scriptInfo.Arguments -WorkingDirectory $PSScriptRoot -Verb runas
                exit 0
            }
        }
    }
}

function Exit-AndWaitOnUI([int] $ExitCode) {
    Log trace 'Exit-AndWaitOnUI'
    Log trace " -> `$ExitCode: '$ExitCode'"

    $scriptID = Get-Variable -Name 'PSScriptID' -ValueOnly -Scope ((Get-PSCallStack).Count - 2)
    Log trace " -> `$scriptID: '$scriptID'"

    if ($PSVersionTable.Platform -ne 'unix') {
        Log trace " Get PSScriptInfos for scriptID: '$scriptID'"
        $scriptInfo = $global:PSScriptInfos[$scriptID]

        Log trace " Check if mutex file exists '$($scriptInfo.FileHash.FileName)'..."
        $mutexFileExists = Test-Path -Path $scriptInfo.FileHash.File
        if ($mutexFileExists) {
            Log trace "Remove existing mutex file -> '$($scriptInfo.FileHash.File)'"
            Remove-Item -Path $scriptInfo.FileHash.File -Force > $null
        }

        if ((Get-ScriptIsCalledFromUI) -or $mutexFileExists) {
            Log info 'Press any key to continue . . . '
            $HOST.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') > $null
            $HOST.UI.RawUI.FlushInputBuffer()
        }
    }

    exit $ExitCode
}

function Get-FileHashFromCommand([string] $Executable, [string[]] $ArgumentList) {
    Log trace 'Get-FileHashFromCommand'
    Log trace " -> `$Executable: '$Command'"
    Log trace " -> `$ArgumentList: '$ArgumentList'"

    $combinedValue = "$Executable $ArgumentList"
    Log trace " combinedValue: '$combinedValue'"

    $checksum = Get-Checksum -Value $combinedValue
    $fileName = "psec-$checksum"
    $file = (Join-Path $env:TEMP $fileName)

    Log trace " checksum: '$checksum'"
    Log trace " checksum: '$fileName'"
    Log trace " checksum: '$file'"

    $object = [pscustomobject][ordered]@{
        FileName = $fileName
        File     = $file
        Checksum = $checksum
    }
    return $object
}

function Invoke-Process {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Command,

        [Parameter(ValueFromRemainingArguments = $true)]
        [string[]] $CommandArgs
    )

    $commandString = $Command
    if ($commandArgs) {
        $commandString += " $commandArgs"
    }

    Write-Host "Execute: '$commandString'" -ForegroundColor DarkYellow

    $startInfo = New-Object System.Diagnostics.ProcessStartInfo
    $startInfo.FileName = $command
    $startInfo.Arguments = $commandArgs
    $startInfo.UseShellExecute = $false
    $startInfo.WorkingDirectory = Get-Location

    $process = New-Object System.Diagnostics.Process
    $process.StartInfo = $startInfo
    $process.Start() > $null

    $finished = $false
    try {
        while (-not $process.WaitForExit(100)) {
            # Non-blocking loop done to allow ctr-c interrupts
        }

        $finished = $true
        return $global:LASTEXITCODE = $process.ExitCode
    } finally {
        # If we didn't finish then an error occured or the user hit ctrl-c. Either way kill the process
        if (-not $finished) {
            $process.Kill()
        }
    }
}

function Start-NativeExecution() {
    $backupEap = $Script:ErrorActionPreference
    $Script:ErrorActionPreference = 'Continue'

    try {
        if ($args.Length -lt 1) {
            Log warning 'No arguments specified'
            return
        }

        Log trace "Execute: '$args'"

        $command = $args[0] | Get-QuotedPath
        $arguments = $args | Select-Object -Skip 1 | Get-QuotedPath

        Log trace "Command: '$command'"
        if ($arguments -and $arguments.Length -gt 0) {
            Log trace "Arguments: '$arguments'"
        }

        $wrapperScriptBlock = [ScriptBlock]::Create("& $command $arguments")

        $calledFromPrompt = Test-CalledFromPrompt
        if ($calledFromPrompt) {
            $wrapperScriptBlock = [ScriptBlock]::Create("& $command $arguments")
        } else {
            $wrapperScriptBlock = [ScriptBlock]::Create("& $command $arguments 2>&1")
        }

        Log trace "WrapperScriptBlock: '$wrapperScriptBlock'"

        $messages = & $wrapperScriptBlock

        # NOTE: If $wrapperScriptBlock's command doesn't have a native invocation,
        # $LASTEXITCODE will point to the obsolete value
        Log trace "LASTEXITCODE: $LASTEXITCODE"
        Log trace "`$?: $?"

        # Need to check both of these cases for errors as they represent different items
        # - $?: Did the powershell script block throw an error
        # - $LASTEXITCODE: Did a windows command executed by the script block end in error
        if ((-not $?) -or ($LASTEXITCODE -and $LASTEXITCODE -ne 0)) {
            if ($Error -ne $null) {
                Log error $Error[0]
            }

            Log error "Execution of '$args' failed with exit code $LASTEXITCODE."
            $logLevel = 'error'
        } else {
            $logLevel = 'info'
        }

        if ($calledFromPrompt -and (Test-Path Variable:\messages)) {
            if ($messages -is [System.Object[]]) {
                foreach ($message in $messages) {
                    if ($message.GetType() -eq [System.Management.Automation.ErrorRecord]) {
                        $lines = $message.Exception.Message.Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries)
                    } elseif ($message.GetType() -eq [string]) {
                        $lines = $message.Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries)
                    }

                    if (Test-Path Variable:\lines) {
                        $lines | Log $logLevel
                    }
                }
            }

            if ($messages -is [string]) {
                $messages.Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries) | Log $logLevel
            }
        }
    } catch {
        if ($_.Exception -and $_.Exception.Message) {
            $_.Exception.Message.Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries) | Log error
        }
    } finally {
        if (-not (Test-Path Variable:\messages)) {
            $messages = $null
        }

        $Script:ErrorActionPreference = $backupEap
    }

    return $messages
}

function Get-QuotedPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string] $Path
    )

    process {
        Log trace "Path: $Path"

        if ($Path -match '\s') {
            return "`"$Path`""
        } else {
            return $Path
        }
    }
}

function Test-CalledFromPrompt() {
    $command = (Get-PSCallStack)[-2].Command
    Log trace "PromptCommand: $command"

    return ($command -eq 'prompt')
}

function Clear-TempDirectories {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]] $AdditionalPaths,

        [Parameter()]
        [switch] $TryRun
    )

    $tempDirName = 'temp'

    $dirs = [System.Collections.ArrayList]@()

    Add-ItemWhenExists -Item (Join-Path $env:ProgramFiles $tempDirName) -List $dirs
    Add-ItemWhenExists -Item (Join-Path ${env:ProgramFiles(x86)} $tempDirName) -List $dirs
    Add-ItemWhenExists -Item (Join-Path $env:windir $tempDirName) -List $dirs

    $userDirs = Get-ChildItem 'C:/Users' -Directory -Force
    foreach ($userDir in $userDirs) {
        Add-ItemWhenExists -Item (Join-Path $userDir.FullName "AppData/Local/$tempDirName") -List $dirs
        Add-ItemWhenExists -Item (Join-Path $userDir.FullName "AppData/LocalLow/$tempDirName") -List $dirs
        Add-ItemWhenExists -Item (Join-Path $userDir.FullName "AppData/Roaming/$tempDirName") -List $dirs
    }

    if ($AdditionalPaths) {
        $AdditionalPaths | Add-ItemWhenExists -List $dirs
    }

    for ($i = 0; $i -lt $dirs.Count; $i++) {
        $items = $dirs[$i] | Get-ChildItem -Recurse -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName | Sort-Object -Descending -Property Length
        $total = $items | Measure-Object | Select-Object -ExpandProperty Count
        Log trace "Found '$total' items in '$($dirs[$i])' to remove"

        Write-Progress -Id 1 -Activity 'Clear TEMP directories' -Status "Step $($($i + 1).ToString().PadLeft($dirs.Count.ToString().Length)) of $($dirs.Count)" -CurrentOperation "Remove all items in '$($dirs[$i])'" -PercentComplete (($i + 1) / $dirs.Count * 100)

        for ($ii = 0; $ii -lt $items.Count; $ii++) {
            Write-Progress -Id 2 -ParentId 1 -Activity 'Processing items' -Status "Item $($($ii + 1).ToString().PadLeft($items.Count.ToString().Length)) of $($items.Count)" -CurrentOperation "Remove item '$($items[$ii])'" -PercentComplete (($ii + 1) / $items.Count * 100)
            if (-not $TryRun) {
                Remove-ItemSafe -Path $items[$ii] -Retries 16 -Milliseconds 10
            } else {
                Start-Sleep -Milliseconds 1
            }
        }
    }
}

function Add-ItemWhenExists {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [string] $Item,

        [Parameter()]
        [System.Collections.ArrayList] $List
    )

    process {
        if ($Item -and (Test-Path $Item)) {
            $List.Add($Item) > $null
        }
    }
}

function Remove-ItemSafe {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string] $Path,

        [Parameter()]
        [int] $Retries = 255,

        [Parameter()]
        [int] $Milliseconds = 75
    )

    process {
        Log trace "Remove item safe '$Path'..."

        while ($path -and (Test-Path -Path $Path -ErrorAction SilentlyContinue) -and ($Retries -gt 0)) {
            try {
                if ((Test-Path -Path $Path -PathType Container) -and ((Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count) -gt 0)) {
                    Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue > $null
                } else {
                    Remove-Item -Path $path -Force -ErrorAction SilentlyContinue > $null
                }
            } catch {
                Start-Sleep -Milliseconds $Milliseconds
            } finally {
                --$Retries
            }
        }
    }
}

function Invoke-WhenFileChanged {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [string] $File,

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

        [Parameter()]
        [int] $PoolingIntervalInMS = 100
    )

    Process {
        $global:FileChanged = $false
        $executeCounter = 0

        $File = Resolve-Path $File

        $filePath = Split-Path $File -Parent
        $fileName = Split-Path $File -Leaf
        $scriptBlock = [scriptblock]::Create($Action)

        $watcher = New-Object IO.FileSystemWatcher $filePath, $fileName -Property @{
            IncludeSubdirectories = $false
            EnableRaisingEvents   = $true
        }

        Log info "::: [$(Get-Date -Format s)] Register event..."
        $onChange = Register-ObjectEvent $watcher Changed -Action { $global:FileChanged = $true }
        [System.Console]::TreatControlCAsInput = $true

        try {
            while ($true) {
                if ($global:FileChanged) {
                    ++$executeCounter
                    Log info "::: [$(Get-Date -Format s)] Execute (${executeCounter}): ${Action}"
                    & $scriptBlock
                    $global:FileChanged = $false
                }

                if ($Host.UI.RawUI.KeyAvailable -and (3 -eq [int]$Host.UI.RawUI.ReadKey('AllowCtrlC, IncludeKeyUp, NoEcho').Character)) {
                    Log info "::: [$(Get-Date -Format s)] Unregister event..."
                    Unregister-Event -SourceIdentifier $onChange.Name
                    return
                }

                Start-Sleep -Milliseconds $PoolingIntervalInMS
            }
        } catch [Exception] {
            Log info "::: [$(Get-Date -Format s)] Unregister event..."
            Unregister-Event -SourceIdentifier $onChange.Name
        }
    }

    End {
        [System.Console]::TreatControlCAsInput = $false
    }
}

$global:PSScriptInfos = [hashtable]@{}

# SIG # Begin signature block
# MIIoFAYJKoZIhvcNAQcCoIIoBTCCKAECAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBrOoRlJaNf1Eyc
# Cu5aOV0CFVjCCSOOVHTV27UyY8NROKCCIRcwggWNMIIEdaADAgECAhAOmxiO+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
# twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG
# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg
# Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH
# JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf
# UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w
# 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk
# tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb
# qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm
# cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6
# 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK
# QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo
# 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB
# Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche
# MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB
# /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU
# 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j
# c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig
# NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v
# dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI
# hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd
# 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC
# qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl
# /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC
# RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT
# gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/
# a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37
# xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL
# NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0
# YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ
# RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG
# sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0
# ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr
# PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM
# gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg
# nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC
# EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0
# p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa
# khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0
# XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I
# HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2
# FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH
# X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2
# 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD
# VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k
# TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD
# AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
# ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww
# HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB
# ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j
# fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI
# moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf
# JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx
# oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3
# LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx
# 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9
# Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I
# Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug
# 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5
# Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQ
# BUSv85SdCDmmv9s/X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAw
# MDAwMFoXDTM0MTAxMzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp
# Z2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X
# 5dLnXaEOCdwvSKOXejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uU
# UI8cIOrHmjsvlmbjaedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa
# 2mq62DvKXd4ZGIX7ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgt
# XkV1lnX+3RChG4PBuOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60
# pCFkcOvV5aDaY7Mu6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17
# cz4y7lI0+9S769SgLDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BY
# QfvYsSzhUa+0rRUGFOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9
# c33u3Qr/eTQQfqZcClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw
# 9/sqhux7UjipmAmhcbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2c
# kpMEtGlwJw1Pt7U20clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhR
# B8qUt+JQofM604qDy0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYD
# VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgG
# BmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxq
# II+eyG8wHQYDVR0OBBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEw
# T6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH
# NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGD
# MIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYB
# BQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
# ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQEL
# BQADggIBAIEa1t6gqbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF
# 7SaCinEvGN1Ott5s1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrC
# QDifXcigLiV4JZ0qBXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFc
# jGnRuSvExnvPnPp44pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8
# wWkZus8W8oM3NG6wQSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbF
# KNOt50MAcN7MmJ4ZiQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP
# 4xeR0arAVeOGv6wnLEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VP
# NTwAvb6cKmx5AdzaROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvr
# moI1VygWy2nyMpqy0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2
# obhDLN9OTH0eaHDAdwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJ
# uEbTbDJ8WC9nR2XlG3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHVjCCBT6g
# AwIBAgIQDBXwscbz14x9ek55doBnRzANBgkqhkiG9w0BAQ0FADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0Ex
# MB4XDTI0MDMxMjAwMDAwMFoXDTI1MDMxMjIzNTk1OVowXjELMAkGA1UEBhMCREUx
# HzAdBgNVBAcTFkdhcm1pc2NoLVBhcnRlbmtpcmNoZW4xFjAUBgNVBAoTDU1hbnVl
# bCBUYW56ZXIxFjAUBgNVBAMTDU1hbnVlbCBUYW56ZXIwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCTsOVLH5jtnViI9CzKdwLIOODRJZAZlTZ6g0fCkQmJ
# R44iMkFBK8/O46dckAy/OLWKIGPXuatA/FpylNeSKLeqDusFTELqVgOaFPr7qr6m
# F6Y1b1NTv9139cNd2tBEkAbwWxJdzsoVGHjneejsC/u7VazQDiOHRYFJj//yrX02
# T5jFIO38sme77dAgGTfxl+E8yEq5Xzza38sw206dYA3OpX7MQsLl8TjXHkuWC/PK
# nc1FRMCfenDYq8KEtUT2b4R4EluRbs9T1ZePZ5kl1pCMsr94CicDfMOuF5QWsqAh
# xdEfzpfecVTq0u/NTBd6CzTtyFE8qHSu1yckU6qlcCJXjqSoAcLKZS1J/Qil2eUb
# /cbwtupGlx8RCj1OzA0A9rZYmrYk4AeYQEAXOr2dPDVCN0zKPcOkKlNPFDbe8mqX
# 6y+h/9Czg1EDwPFP23HKD0bGC8L5G5NHxqPezcdukGKoqtSeQy81lR3GYdiNV5wt
# YyPDcSjxgkII6ZHedg6GMGIVCL/0NHh6EXE8SJgXX+JIvjChetUZEsF18ro2VZKj
# 4hW6FzK+0HUgyeFsdU6CoWQB097UC5uPaeWyXTIkskG3m+8KvUZp88t6a+K9JI4/
# 26/o77m6WTsnVoXIOGfUlhXmu/vPc5DKwxBTMA5IcGPPJrd4pM27m8niGnzkVq/i
# jwIDAQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIw
# HQYDVR0OBBYEFA630pXEHsvRNF7iqqyeERyJpcd0MD4GA1UdIAQ3MDUwMwYGZ4EM
# AQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAO
# BgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCB
# qjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3Rl
# ZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0
# dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWdu
# aW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAC
# hlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRD
# b2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0G
# CSqGSIb3DQEBDQUAA4ICAQCrfVGhAbhMkdZAwsrSd4rsmwJWGlNbwnimYvs0xcB4
# A5QiwyPJ6cPibEEdU7soXow25Dk7DHltqlNpJP8eO/IGbmSBtqQPo9oxcNtrs/Hm
# FbJh8wKyJ2OtwRi2y7pXxWZZekIvecJ/afSzbs9BEWo2DcmjknYCZkRZmO9CWdjU
# 8sVIzBq3zUHsF8gXtm8Tp7kp6Ceo5lKoW8JbOM6RnA0ySj2iGagnQo++3qiH3u/C
# GtOlxlz5TR2J3FKXa6Kq3adGm7tSoajN121uEmmo2AoU0gLoQwAxEb4o0czrgNya
# /P34G8Tep8Wgq+wzE0KEWLWfJ4Q1u5pMPfAyKUiY6HU2tIpQEN2BKF0tSeRyGdQz
# 0p6b4k0+R5dCU3QlbuM2TbU87Nu4ZxS6IiN19qZkxEZN5LPDhS8d6BNQy34uuugg
# I/M/iKQCcBtreWEkXZ+DSMCE2D7ElfsqPqkEYemxuIN/MA3RWdlZeFzafNJErQo6
# O/MWmmrvCYunX0i+evzTO4/L7XT+Lldg1Q1psAZfowqGM691aWR6mtd+OX76O1cB
# 79IWvHl8ObNQpR5P9U++sn6r20uaWfvK7tliH0LSPLYPfDBsD09VUSx+xHy9h2W2
# KWnX6nd5VU0ig36EaT153pqX54I49b8vj+b+5JcNHeqEV8B6+6xfr1CIux1GgYnL
# 1jGCBlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0
# LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmlu
# ZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDBXwscbz14x9ek55doBnRzANBglg
# hkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3
# DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV
# MC8GCSqGSIb3DQEJBDEiBCA4w9UOpwWwPOnxznhaLRevFTtnC422PmrgzTbi7vOm
# lDANBgkqhkiG9w0BAQEFAASCAgBbayAb+l+9nco8bSkrBIi0RiCZWd75qgVNlJhc
# e0+9X46PY3Oqt7FUudXBxpUGS5yMfYvOKQB+Nm45OvifwkMhdTR4IUuUBtAgQFpm
# 5Qr/kzIXbqGLGEEmrVuHXTmETWpFxct3pOZDhAl3vMGFyuwCRlDTxi28dLqFrCkm
# gFH5QPPqguYmQJgmOTDXWDEeQIgNftUU3AwmLyprFxnHExdDTlC+5F3mY6q0fIR1
# 1EiSf3KSVR9UYPZSBMI4JGT6WLsvCFam5yII9OAVKh+YnJG4TXLrcgYRDyamzhUK
# qMFEZ5/emD93f0EMs8KswTX331kmEcCMVKHtad3bdrB1dE7b2yyOP4G2uSPo+RFN
# 2vSUhLNgEV3Mwff5X+Cz9A/IoTBrUarJpx9izMcVjEPmPG8YbJxegqQfZcL9ZzND
# NeTphAEjHJi5sbBohQBFjpiW5PAzrTbJSwoM8W2EbgpSYSL/n+TicH6yyRgims4t
# v0AvO37l7RsbvlpUh9YTYKyD5S3BI3AreyUP5q3J1SlBMb+cWc0FSP8cJ49YdmCR
# dqftRGFe+zeYwIX+hukqXbpgj60R9m1/NbmTNTloqiOEpEzfIxULXuuCzSAcUvCW
# hOxqTNqtqHdQ1jqUpid7lAMrZTtijuUklDV9H5ihYEeuUxmSQMB2bB0BZVrhVoEK
# YhGpYqGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYT
# AlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQg
# VHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OU
# nQg5pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZI
# hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA0MjgxNzUwMzZaMC8GCSqGSIb3DQEJ
# BDEiBCDTtygHuIMcddJ5kLjYRk8i8pocVBVPKzDxYVAO8kb5ZDANBgkqhkiG9w0B
# AQEFAASCAgBFBfPIIEQq4dXLohp1WKTdJRpJD31Cd55bS/X+Qy0A1pvlXhbJNV7Q
# lmB45UdJL4an1CrOdlM1ObtBSkX5hz9ATUIHYMmrHNHOslTYgQD8/DFDU1MjVnwV
# CGNyELjQr49mPLZ4HLdsdS5Qy3OOzdQtGcbVwrBd08TeQ9Os8C2LzJZiAffzj/QR
# lAGGus6MqdggV9uHOWTl5Bt+18hzqOEeLUNeqpjUsgUf9rdPIJfs5r9a9rNTBKkp
# KCImdx7ucygM3KkUjznpKK4Lkik9sJz7652ywpndbppUCEEgwWlBMk6gYtom0URB
# xov6GkZauW8pNVF7WUa6vU36VghpjvqdZeBd0rZjov3WRTSnQ+M4oeD5Ys6YAMKK
# AN72rwpdeJGCsfdffNc0ibcgCtz4r7qYJb5pMtoOliOEtZqpRg1GFGSSQ3RAl0+3
# /AuEgb+82p52pF1ONZQRgRRa1Qxwpdu5eCUt2aaKob2G98eG/g0hIpHqy6zjLGPO
# bt+LTvL3Jl3+IbOJ/wBuOTvYHnzOwqAN5GKKmfsBrttC0xKBN45BOwa17tu29Q45
# vzg3lz5Gwf0YV0OMdO7pfnSMBrS2Z6ooSGUl7AI09olGX2V3eyhFPZ/pFDnI9aGA
# zQY7vdNq7WxNbm1IMVXv1HcKxZlf+pKGz8vV8IDN2P9YeZ5mM5wHbg==
# SIG # End signature block