Helpers.ps1
# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. function Wait-JobWithAnimation { <# .SYNOPSIS Waits for a background job to complete by showing a cursor and elapsed time. .DESCRIPTION Waits for a background job to complete by showing a cursor and elapsed time. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub .PARAMETER Name The name of the job(s) that we are waiting to complete. .PARAMETER Description The text displayed next to the spinning cursor, explaining what the job is doing. .PARAMETER StopAllOnAnyFailure Will call Stop-Job on any jobs still Running if any of the specified jobs entered the Failed state. .EXAMPLE Wait-JobWithAnimation Job1 Waits for a job named "Job1" to exit the "Running" state. While waiting, shows a waiting cursor and the elapsed time. .NOTES This is not a stand-in replacement for Wait-Job. It does not provide the full set of configuration options that Wait-Job does. #> [CmdletBinding()] Param( [Parameter(Mandatory)] [string[]] $Name, [string] $Description = "", [switch] $StopAllOnAnyFailure ) [System.Collections.ArrayList]$runningJobs = $Name $allJobsCompleted = $true $hasFailedJob = $false $animationFrames = '|','/','-','\' $framesPerSecond = 9 # We'll wrap the description (if provided) in brackets for display purposes. if ($Description -ne "") { $Description = "[$Description]" } $iteration = 0 while ($runningJobs.Count -gt 0) { # We'll run into issues if we try to modify the same collection we're iterating over $jobsToCheck = $runningJobs.ToArray() foreach ($jobName in $jobsToCheck) { $state = (Get-Job -Name $jobName).state if ($state -ne 'Running') { $runningJobs.Remove($jobName) if ($state -ne 'Completed') { $allJobsCompleted = $false } if ($state -eq 'Failed') { $hasFailedJob = $true if ($StopAllOnAnyFailure) { break } } } } if ($hasFailedJob -and $StopAllOnAnyFailure) { foreach ($jobName in $runningJobs) { Stop-Job -Name $jobName } $runingJobs.Clear() } Write-InteractiveHost "`r$($animationFrames[$($iteration % $($animationFrames.Length))]) Elapsed: $([int]($iteration / $framesPerSecond)) second(s) $Description" -NoNewline -f Yellow Start-Sleep -Milliseconds ([int](1000/$framesPerSecond)) $iteration++ } if ($allJobsCompleted) { Write-InteractiveHost "`rDONE - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -NoNewline -f Green # We forcibly set Verbose to false here since we don't need it printed to the screen, since we just did above -- we just need to log it. Write-Log -Message "DONE - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -Level Verbose -Verbose:$false } else { Write-InteractiveHost "`rDONE (FAILED) - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -NoNewline -f Red # We forcibly set Verbose to false here since we don't need it printed to the screen, since we just did above -- we just need to log it. Write-Log -Message "DONE (FAILED) - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -Level Verbose -Verbose:$false } Write-InteractiveHost "" } function Get-SHA512Hash { <# .SYNOPSIS Gets the SHA512 hash of the requested string. .DESCRIPTION Gets the SHA512 hash of the requested string. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub .PARAMETER PlainText The plain text that you want the SHA512 hash for. .EXAMPLE Get-SHA512Hash -PlainText "Hello World" Returns back the string "2C74FD17EDAFD80E8447B0D46741EE243B7EB74DD2149A0AB1B9246FB30382F27E853D8585719E0E67CBDA0DAA8F51671064615D645AE27ACB15BFB1447F459B" which represents the SHA512 hash of "Hello World" .OUTPUTS System.String - A SHA512 hash of the provided string #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowNull()] [AllowEmptyString()] [string] $PlainText ) $sha512= New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider $utf8 = New-Object -TypeName System.Text.UTF8Encoding return [System.BitConverter]::ToString($sha512.ComputeHash($utf8.GetBytes($PlainText))) -replace '-', '' } function Write-Log { <# .SYNOPSIS Writes logging information to screen and log file simultaneously. .DESCRIPTION Writes logging information to screen and log file simultaneously. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub .PARAMETER Message The message(s) to be logged. Each element of the array will be written to a separate line. This parameter supports pipelining but there are no performance benefits to doing so. For more information, see the .NOTES for this cmdlet. .PARAMETER Level The type of message to be logged. .PARAMETER Indent The number of spaces to indent the line in the log file. .PARAMETER Path The log file path. Defaults to $env:USERPROFILE\Documents\PowerShellForGitHub.log .PARAMETER Exception If present, the exception information will be logged after the messages provided. The actual string that is logged is obtained by passing this object to Out-String. .EXAMPLE Write-Log -Message "Everything worked." -Path C:\Debug.log Writes the message "Everything worked." to the screen as well as to a log file at "c:\Debug.log", with the caller's username and a date/time stamp prepended to the message. .EXAMPLE Write-Log -Message ("Everything worked.", "No cause for alarm.") -Path C:\Debug.log Writes the following message to the screen as well as to a log file at "c:\Debug.log", with the caller's username and a date/time stamp prepended to the message: Everything worked. No cause for alarm. .EXAMPLE Write-Log -Message "There may be a problem..." -Level Warning -Indent 2 Writes the message "There may be a problem..." to the warning pipeline indented two spaces, as well as to the default log file with the caller's username and a date/time stamp prepended to the message. .EXAMPLE try { $null.Do() } catch { Write-Log -Message ("There was a problem.", "Here is the exception information:") -Exception $_ -Level Error } Logs the message: Write-Log : 2018-01-23 12:57:37 : dabelc : There was a problem. Here is the exception information: You cannot call a method on a null-valued expression. At line:1 char:7 + try { $null.Do() } catch { Write-Log -Message ("There was a problem." ... + ~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : InvokeMethodOnNull .INPUTS System.String .NOTES The "LogPath" configuration value indicates where the log file will be created. The "" determines if log entries will be made to the log file. If $false, log entries will ONLY go to the relevant output pipeline. Note that, although this function supports pipeline input to the -Message parameter, there is NO performance benefit to using the pipeline. This is because the pipeline input is simply accumulated and not acted upon until all input has been received. This behavior is intentional, in order for a statement like: "Multiple", "messages" | Write-Log -Exception $ex -Level Error to make sense. In this case, the cmdlet should accumulate the messages and, at the end, include the exception information. #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="We need to be able to access the PID for logging purposes, and it is accessed via a global variable.")] param( [Parameter(ValueFromPipeline)] [AllowEmptyCollection()] [AllowEmptyString()] [AllowNull()] [string[]] $Message = @(), [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug')] [string] $Level = 'Informational', [ValidateRange(1, 30)] [Int16] $Indent = 0, [IO.FileInfo] $Path = (Get-GitHubConfiguration -Name LogPath), [System.Management.Automation.ErrorRecord] $Exception ) Begin { # Accumulate the list of Messages, whether by pipeline or parameter. $messages = @() } Process { foreach ($m in $Message) { $messages += $m } } End { if ($null -ne $Exception) { # If we have an exception, add it after the accumulated messages. $messages += Out-String -InputObject $Exception } elseif ($messages.Count -eq 0) { # If no exception and no messages, we should early return. return } # Finalize the string to be logged. $finalMessage = $messages -join [Environment]::NewLine # Build the console and log-specific messages. $date = Get-Date $dateString = $date.ToString("yyyy-MM-dd HH:mm:ss") if (Get-GitHubConfiguration -Name LogTimeAsUtc) { $dateString = $date.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ssZ") } $consoleMessage = '{0}{1}' -f (" " * $Indent), $finalMessage if (Get-GitHubConfiguration -Name LogProcessId) { $maxPidDigits = 10 # This is an estimate (see https://stackoverflow.com/questions/17868218/what-is-the-maximum-process-id-on-windows) $pidColumnLength = $maxPidDigits + "[]".Length $logFileMessage = "{0}{1} : {2, -$pidColumnLength} : {3} : {4} : {5}" -f (" " * $Indent), $dateString, "[$global:PID]", $env:username, $Level.ToUpper(), $finalMessage } else { $logFileMessage = '{0}{1} : {2} : {3} : {4}' -f (" " * $Indent), $dateString, $env:username, $Level.ToUpper(), $finalMessage } # Write the message to screen/log. # Note that the below logic could easily be moved to a separate helper function, but a concious # decision was made to leave it here. When this cmdlet is called with -Level Error, Write-Error # will generate a WriteErrorException with the origin being Write-Log. If this call is moved to # a helper function, the origin of the WriteErrorException will be the helper function, which # could confuse an end user. switch ($Level) { # Need to explicitly say SilentlyContinue here so that we continue on, given that # we've assigned a script-level ErrorActionPreference of "Stop" for the module. 'Error' { Write-Error $consoleMessage -ErrorAction SilentlyContinue } 'Warning' { Write-Warning $consoleMessage } 'Verbose' { Write-Verbose $consoleMessage } 'Debug' { Write-Debug $consoleMessage } 'Informational' { # We'd prefer to use Write-Information to enable users to redirect that pipe if # they want, unfortunately it's only available on v5 and above. We'll fallback to # using Write-Host for earlier versions (since we still need to support v4). if ($PSVersionTable.PSVersion.Major -ge 5) { Write-Information $consoleMessage -InformationAction Continue } else { Write-InteractiveHost $consoleMessage } } } try { if (-not (Get-GitHubConfiguration -Name DisableLogging)) { if ([String]::IsNullOrWhiteSpace($Path)) { Write-Warning 'Logging is currently enabled, however no path has been specified for the log file. Use "Set-GitHubConfiguration -LogPath" to set the log path, or "Set-GitHubConfiguration -DisableLogging" to disable logging.' } else { $logFileMessage | Out-File -FilePath $Path -Append } } } catch { $output = @() $output += "Failed to add log entry to [$Path]. The error was:" $output += Out-String -InputObject $_ if (Test-Path -Path $Path -PathType Leaf) { # The file exists, but likely is being held open by another process. # Let's do best effort here and if we can't log something, just report # it and move on. $output += "This is non-fatal, and your command will continue. Your log file will be missing this entry:" $output += $consoleMessage Write-Warning ($output -join [Environment]::NewLine) } else { # If the file doesn't exist and couldn't be created, it likely will never # be valid. In that instance, let's stop everything so that the user can # fix the problem, since they have indicated that they want this logging to # occur. throw ($output -join [Environment]::NewLine) } } } } $script:alwaysRedactParametersForLogging = @( 'AccessToken' # Would be a security issue ) $script:alwaysExcludeParametersForLogging = @( 'NoStatus' ) function Write-InvocationLog { <# .SYNOPSIS Writes a log entry for the invoke command. .DESCRIPTION Writes a log entry for the invoke command. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub .PARAMETER InvocationInfo The '$MyInvocation' object from the calling function. No need to explicitly provide this if you're trying to log the immediate function this is being called from. .PARAMETER RedactParameter An optional array of parameter names that should be logged, but their values redacted. .PARAMETER ExcludeParameter An optional array of parameter names that should simply not be logged. .EXAMPLE Write-InvocationLog -Invocation $MyInvocation .EXAMPLE Write-InvocationLog -Invocation $MyInvocation -ExcludeParameter @('Properties', 'Metrics') .NOTES The actual invocation line will not be _completely_ accurate as converted parameters will be in JSON format as opposed to PowerShell format. However, it should be sufficient enough for debugging purposes. ExcludeParamater will always take precedence over RedactParameter. #> [CmdletBinding(SupportsShouldProcess)] param( [Management.Automation.InvocationInfo] $Invocation = (Get-Variable -Name MyInvocation -Scope 1 -ValueOnly), [string[]] $RedactParameter, [string[]] $ExcludeParameter ) $jsonConversionDepth = 20 # Seems like it should be more than sufficient # Build up the invoked line, being sure to exclude and/or redact any values necessary $params = @() foreach ($param in $Invocation.BoundParameters.GetEnumerator()) { if ($param.Key -in ($script:alwaysExcludeParametersForLogging + $ExcludeParameter)) { continue } if ($param.Key -in ($script:alwaysRedactParametersForLogging + $RedactParameter)) { $params += "-$($param.Key) <redacted>" } else { if ($param.Value -is [switch]) { $params += "-$($param.Key):`$$($param.Value.ToBool().ToString().ToLower())" } else { $params += "-$($param.Key) $(ConvertTo-Json -InputObject $param.Value -Depth $jsonConversionDepth -Compress)" } } } Write-Log -Message "[$($Invocation.MyCommand.Module.Version)] Executing: $($Invocation.MyCommand) $($params -join ' ')" -Level Verbose } function DeepCopy-Object <# .SYNOPSIS Creates a deep copy of a serializable object. .DESCRIPTION Creates a deep copy of a serializable object. By default, PowerShell performs shallow copies (simple references) when assigning objects from one variable to another. This will create full exact copies of the provided object so that they can be manipulated independently of each other, provided that the object being copied is serializable. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub .PARAMETER InputObject The object that is to be copied. This must be serializable or this will fail. .EXAMPLE $bar = DeepCopy-Object -InputObject $foo Assuming that $foo is serializable, $bar will now be an exact copy of $foo, but any changes that you make to one will not affect the other. .RETURNS An exact copy of the PSObject that was just deep copied. #> { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Intentional. This isn't exported, and needed to be explicit relative to Copy-Object.")] param( [Parameter(Mandatory)] [PSCustomObject] $InputObject ) $memoryStream = New-Object System.IO.MemoryStream $binaryFormatter = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter $binaryFormatter.Serialize($memoryStream, $InputObject) $memoryStream.Position = 0 $DeepCopiedObject = $binaryFormatter.Deserialize($memoryStream) $memoryStream.Close() return $DeepCopiedObject } function New-TemporaryDirectory { <# .SYNOPSIS Creates a new subdirectory within the users's temporary directory and returns the path. .DESCRIPTION Creates a new subdirectory within the users's temporary directory and returns the path. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub .EXAMPLE New-TemporaryDirectory Creates a new directory with a GUID under $env:TEMP .OUTPUTS System.String - The path to the newly created temporary directory #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] param() $guid = [System.GUID]::NewGuid() while (Test-Path -PathType Container (Join-Path -Path $env:TEMP -ChildPath $guid)) { $guid = [System.GUID]::NewGuid() } $tempFolderPath = Join-Path -Path $env:TEMP -ChildPath $guid Write-Log -Message "Creating temporary directory: $tempFolderPath" -Level Verbose New-Item -ItemType Directory -Path $tempFolderPath } function Write-InteractiveHost { <# .SYNOPSIS Forwards to Write-Host only if the host is interactive, else does nothing. .DESCRIPTION A proxy function around Write-Host that detects if the host is interactive before calling Write-Host. Use this instead of Write-Host to avoid failures in non-interactive hosts. The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub .EXAMPLE Write-InteractiveHost "Test" Write-InteractiveHost "Test" -NoNewline -f Yellow .NOTES Boilerplate is generated using these commands: # $Metadata = New-Object System.Management.Automation.CommandMetaData (Get-Command Write-Host) # [System.Management.Automation.ProxyCommand]::Create($Metadata) | Out-File temp #> [CmdletBinding( HelpUri='http://go.microsoft.com/fwlink/?LinkID=113426', RemotingCapability='None')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="This provides a wrapper around Write-Host. In general, we'd like to use Write-Information, but it's not supported on PS 4.0 which we need to support.")] param( [Parameter( Position=0, ValueFromPipeline, ValueFromRemainingArguments)] [System.Object] $Object, [switch] $NoNewline, [System.Object] $Separator, [System.ConsoleColor] $ForegroundColor, [System.ConsoleColor] $BackgroundColor ) # Determine if the host is interactive if ([Environment]::UserInteractive -and ` ![Bool]([Environment]::GetCommandLineArgs() -like '-noni*') -and ` (Get-Host).Name -ne 'Default Host') { # Special handling for OutBuffer (generated for the proxy function) $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } Write-Host @PSBoundParameters } } function Resolve-UnverifiedPath { <# .SYNOPSIS A wrapper around Resolve-Path that works for paths that exist as well as for paths that don't (Resolve-Path normally throws an exception if the path doesn't exist.) .DESCRIPTION A wrapper around Resolve-Path that works for paths that exist as well as for paths that don't (Resolve-Path normally throws an exception if the path doesn't exist.) The Git repo for this module can be found here: https://aka.ms/PowerShellForGitHub .EXAMPLE Resolve-UnverifiedPath -Path 'c:\windows\notepad.exe' Returns the string 'c:\windows\notepad.exe'. .EXAMPLE Resolve-UnverifiedPath -Path '..\notepad.exe' Returns the string 'c:\windows\notepad.exe', assuming that it's executed from within 'c:\windows\system32' or some other sub-directory. .EXAMPLE Resolve-UnverifiedPath -Path '..\foo.exe' Returns the string 'c:\windows\foo.exe', assuming that it's executed from within 'c:\windows\system32' or some other sub-directory, even though this file doesn't exist. .OUTPUTS [string] - The fully resolved path #> [CmdletBinding()] param( [Parameter( Position=0, ValueFromPipeline)] [string] $Path ) $resolvedPath = Resolve-Path -Path $Path -ErrorVariable resolvePathError -ErrorAction SilentlyContinue if ($null -eq $resolvedPath) { return $resolvePathError[0].TargetObject } else { return $resolvedPath.ProviderPath } } function Ensure-Directory { <# .SYNOPSIS A utility function for ensuring a given directory exists. .DESCRIPTION A utility function for ensuring a given directory exists. If the directory does not already exist, it will be created. .PARAMETER Path A full or relative path to the directory that should exist when the function exits. .NOTES Uses the Resolve-UnverifiedPath function to resolve relative paths. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification = "Unable to find a standard verb that satisfies describing the purpose of this internal helper method.")] param( [Parameter(Mandatory)] [string] $Path ) try { $Path = Resolve-UnverifiedPath -Path $Path if (-not (Test-Path -PathType Container -Path $Path)) { Write-Log -Message "Creating directory: [$Path]" -Level Verbose New-Item -ItemType Directory -Path $Path | Out-Null } } catch { Write-Log -Message "Could not ensure directory: [$Path]" -Level Error throw } } function Get-HttpWebResponseContent { <# .SYNOPSIS Returns the content that may be contained within an HttpWebResponse object. .DESCRIPTION Returns the content that may be contained within an HttpWebResponse object. This would commonly be used when trying to get the potential content returned within a failing WebResponse. Normally, when you call Invoke-WebRequest, it returns back a BasicHtmlWebResponseObject which directly contains a Content property, however if the web request fails, you get a WebException which contains a simpler WebResponse, which requires a bit more effort in order to acccess the raw response content. .PARAMETER WebResponse An HttpWebResponse object, typically the Response property on a WebException. .OUTPUTS System.String - The raw content that was included in a WebResponse; $null otherwise. #> [CmdletBinding()] [OutputType([String])] param( [System.Net.HttpWebResponse] $WebResponse ) $streamReader = $null try { $content = $null if (($null -ne $WebResponse) -and ($WebResponse.ContentLength -gt 0)) { $stream = $WebResponse.GetResponseStream() $encoding = [System.Text.Encoding]::UTF8 if (-not [String]::IsNullOrWhiteSpace($WebResponse.ContentEncoding)) { $encoding = [System.Text.Encoding]::GetEncoding($WebResponse.ContentEncoding) } $streamReader = New-Object -TypeName System.IO.StreamReader -ArgumentList ($stream, $encoding) $content = $streamReader.ReadToEnd() } return $content } finally { if ($null -ne $streamReader) { $streamReader.Close() } } } # SIG # Begin signature block # MIIdkgYJKoZIhvcNAQcCoIIdgzCCHX8CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUeyAWeRIEQUB3BpSqKIKsFjJ+ # X86gghhuMIIE2jCCA8KgAwIBAgITMwAAAQRkiNk5fPOWxwAAAAABBDANBgkqhkiG # 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw # HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTgwODIzMjAyMDI0 # WhcNMTkxMTIzMjAyMDI0WjCByjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEm # MCQGA1UECxMdVGhhbGVzIFRTUyBFU046OEQ0MS00QkY3LUIzQjcxJTAjBgNVBAMT # HE1pY3Jvc29mdCBUaW1lLVN0YW1wIHNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUA # A4IBDwAwggEKAoIBAQC1gq+cu32pqHFCVNBArToafinIGtiggW7uGhpgEClrIdmn # pT4K0z3S/50PTC8erU1rLsmhM2pTL6RPRjtTBoCeQyvc89VaKmAX0+9oGFXSRVOe # gmymS9O621z/wMUs1hKaXKSLm646cafbnkj8bb0JoKBiYH21B7EHYo1XAb5TtujT # PNMUN8ZNM0KvsI2ReD82o5NPCyK2ImaZQGGVAIYwUI3cN9nGCzZxLIU/t9faPY8S # /DLqjJPmivDVm7v6uYeS3Os3YDVU1kH9pNeRJA3UEyW7lBmZC+4VRqz09BMOIBRI # 26cNRW6jXeVWQn5EGltiZFPAfX1PAn4+jwm0jkNDAgMBAAGjggEJMIIBBTAdBgNV # HQ4EFgQUFXfx3wkrUOa+m1RJ93w3t/ARFLkwHwYDVR0jBBgwFoAUIzT42VJGcArt # QPt2+7MrsMM1sw8wVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5taWNyb3Nv # ZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljcm9zb2Z0VGltZVN0YW1wUENBLmNy # bDBYBggrBgEFBQcBAQRMMEowSAYIKwYBBQUHMAKGPGh0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2kvY2VydHMvTWljcm9zb2Z0VGltZVN0YW1wUENBLmNydDATBgNV # HSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0BAQUFAAOCAQEAJj/6ScX0CG8z96Ji # vZm8uW5Xfpfwp47ECxnXhyEnYZLsvgk7DYswCknEE+CRiyYchHJgd3kyD7bx2WJA # iv/DM02ll6swsIMHhM3Q7WVX4i/K+HPiHR1TRjK5iE9VfvZpAe55ZIaqSKKvlvsp # r1Y/UfuYwCUyZJ3R5ZCFgXn9dK2sebcPhKgL86t6Rup5xz8EJ3ZO5PB1Nu1rp+Qy # hoduiDE1ZqXZeQHwW4Eb9gQhHxrm9GjwtWcMaPYWIP/k75NTSBvBZ7RYIguOoZ9s # /uOp+61s9FcoZmP07DcukoIAuQAOi9U+8iuGorNErlrmb6cpKaOmnz9FmlJrWOmB # DdarbDCCBgMwggProAMCAQICEzMAAAEEaeLbufuKDYMAAAAAAQQwDQYJKoZIhvcN # AQELBQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV # BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYG # A1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMTAeFw0xODA3MTIy # MDA4NDlaFw0xOTA3MjYyMDA4NDlaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xHjAcBgNVBAMTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjCCASIw # DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJvFlfKX9v8jamydc1lzOJvTtOOE # rY24PiXnoLggWkqRcDjYFKi9msD9DWse7OH8/wQ84mFgrlVqYZL71wB9nuppNtb9 # V3kZl/6EkfbHVa2mrgKK7bGRR1bNSodmRacwGxrtrHtrBdIzgnO+xm8czNToGgUV # AC6Rl7ZLyGFnIovnExJowhcUryZatu5Vc3z1RLMhJwYA67U2+fwmiq0/f0QUw3q7 # I8iL3r4WisEhogIB2X+YkuIxU4+HsZAkmxf6FU3KAWwQbFICopNfYgNBJIxwp3As # jUsv1zNZXP1d4D/X5IQXu30+edOCQ2JUQMibXs9wFDtgPGk/nzfn+BaSM4sCAwEA # AaOCAYIwggF+MB8GA1UdJQQYMBYGCisGAQQBgjdMCAEGCCsGAQUFBwMDMB0GA1Ud # DgQWBBS6F+MlaPS71Xsjpri2fOemgqtXrjBUBgNVHREETTBLpEkwRzEtMCsGA1UE # CxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMRYwFAYDVQQF # Ew0yMzAwMTIrNDM3OTY2MB8GA1UdIwQYMBaAFEhuZOVQBdOCqhc3NyK1bajKdQKV # MFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lv # cHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0wNy0wOC5jcmwwYQYIKwYBBQUH # AQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp # b3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0wNy0wOC5jcnQwDAYDVR0T # AQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAKcWCCzmZIAzoJFuJOzhQkoOV25Hy # O9Kk8NqBW3OOZ0gatsmKB3labM7D/GaYF7K716YWtQNWhXsqfS9ABk6eaddpFBWO # Y/vMgPXbEZxQ7ksCcUxrBwX+Z1PxGbZubizyj9RFKeE2CLIceIEnloeZQhh3lNzG # vJ2k21amNtBDSF9ABH5n6YjYAMfrMv/eCndgA3P+nqHHHfPAsy1hh8jxN4Xc/G08 # SxNKEna1UEpN513zTyHkmBKBgf7pXKj8FIzRAp9l+3Z1t2JTx33ax7pC4m57Jkoj # gjLYjXUeEW+Lf3oG1aofGKVwE+fuaJ0HvAbpiQOWDGriIaslA9i3ARHhxCKWKTF8 # w1VO0BznRcZmAIoVIcTFAXAd4mgBOQ8iIcmoc39w2Cz09WnlSWw5paKpyns51fl3 # bzBzg5bAo4uu5X/dY03aFct7+R3ljsQ6qkr0LUVmn/JeuEkXOePfHUmJYYu6M69e # Quoa1PVRU/GlXZKbh2e4dqsGVeGu1YOvOf8gMYtc5vq1B8+GJ8dBAiM5bVlOLsB4 # rzpJY2zieOAjdtQrqGcAPGVSWDDSeOl47e27KX2iHzjl7FnHk5lF+QCIykR6R9Ym # R83UCcK1epAMPRYWfreU20ZSAEoeuT1pyVFlatU/EcQtN5dfksINMQya1ll38FBI # h4l2k9jzfUqwItgwggYHMIID76ADAgECAgphFmg0AAAAAAAcMA0GCSqGSIb3DQEB # BQUAMF8xEzARBgoJkiaJk/IsZAEZFgNjb20xGTAXBgoJkiaJk/IsZAEZFgltaWNy # b3NvZnQxLTArBgNVBAMTJE1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhv # cml0eTAeFw0wNzA0MDMxMjUzMDlaFw0yMTA0MDMxMzAzMDlaMHcxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xITAfBgNVBAMTGE1pY3Jvc29mdCBU # aW1lLVN0YW1wIFBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+h # bLHf20iSKnxrLhnhveLjxZlRI1Ctzt0YTiQP7tGn0UytdDAgEesH1VSVFUmUG0KS # rphcMCbaAGvoe73siQcP9w4EmPCJzB/LMySHnfL0Zxws/HvniB3q506jocEjU8qN # +kXPCdBer9CwQgSi+aZsk2fXKNxGU7CG0OUoRi4nrIZPVVIM5AMs+2qQkDBuh/NZ # MJ36ftaXs+ghl3740hPzCLdTbVK0RZCfSABKR2YRJylmqJfk0waBSqL5hKcRRxQJ # gp+E7VV4/gGaHVAIhQAQMEbtt94jRrvELVSfrx54QTF3zJvfO4OToWECtR0Nsfz3 # m7IBziJLVP/5BcPCIAsCAwEAAaOCAaswggGnMA8GA1UdEwEB/wQFMAMBAf8wHQYD # VR0OBBYEFCM0+NlSRnAK7UD7dvuzK7DDNbMPMAsGA1UdDwQEAwIBhjAQBgkrBgEE # AYI3FQEEAwIBADCBmAYDVR0jBIGQMIGNgBQOrIJgQFYnl+UlE/wq4QpTlVnkpKFj # pGEwXzETMBEGCgmSJomT8ixkARkWA2NvbTEZMBcGCgmSJomT8ixkARkWCW1pY3Jv # c29mdDEtMCsGA1UEAxMkTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9y # aXR5ghB5rRahSqClrUxzWPQHEy5lMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9j # cmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL21pY3Jvc29mdHJvb3Rj # ZXJ0LmNybDBUBggrBgEFBQcBAQRIMEYwRAYIKwYBBQUHMAKGOGh0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljcm9zb2Z0Um9vdENlcnQuY3J0MBMG # A1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBBQUAA4ICAQAQl4rDXANENt3p # tK132855UU0BsS50cVttDBOrzr57j7gu1BKijG1iuFcCy04gE1CZ3XpA4le7r1ia # HOEdAYasu3jyi9DsOwHu4r6PCgXIjUji8FMV3U+rkuTnjWrVgMHmlPIGL4UD6ZEq # JCJw+/b85HiZLg33B+JwvBhOnY5rCnKVuKE5nGctxVEO6mJcPxaYiyA/4gcaMvnM # MUp2MT0rcgvI6nA9/4UKE9/CCmGO8Ne4F+tOi3/FNSteo7/rvH0LQnvUU3Ih7jDK # u3hlXFsBFwoUDtLaFJj1PLlmWLMtL+f5hYbMUVbonXCUbKw5TNT2eb+qGHpiKe+i # myk0BncaYsk9Hm0fgvALxyy7z0Oz5fnsfbXjpKh0NbhOxXEjEiZ2CzxSjHFaRkMU # vLOzsE1nyJ9C/4B5IYCeFTBm6EISXhrIniIh0EPpK+m79EjMLNTYMoBMJipIJF9a # 6lbvpt6Znco6b72BJ3QGEe52Ib+bgsEnVLaxaj2JoXZhtG6hE6a/qkfwEm/9ijJs # sv7fUciMI8lmvZ0dhxJkAj0tr1mPuOQh5bWwymO0eFQF1EEuUKyUsKV4q7OglnUa # 2ZKHE3UiLzKoCG6gW4wlv6DvhMoh1useT8ma7kng9wFlb4kLfchpyOZu6qeXzjEp # /w7FW1zYTRuh2Povnj8uVRZryROj/TCCB3owggVioAMCAQICCmEOkNIAAAAAAAMw # DQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n # dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y # YXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhv # cml0eSAyMDExMB4XDTExMDcwODIwNTkwOVoXDTI2MDcwODIxMDkwOVowfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP # ADCCAgoCggIBAKvw+nIQHC6t2G6qghBNNLrytlghn0IbKmvpWlCquAY4GgRJun/D # DB7dN2vGEtgL8DjCmQawyDnVARQxQtOJDXlkh36UYCRsr55JnOloXtLfm1OyCizD # r9mpK656Ca/XllnKYBoF6WZ26DJSJhIv56sIUM+zRLdd2MQuA3WraPPLbfM6XKEW # 9Ea64DhkrG5kNXimoGMPLdNAk/jj3gcN1Vx5pUkp5w2+oBN3vpQ97/vjK1oQH01W # KKJ6cuASOrdJXtjt7UORg9l7snuGG9k+sYxd6IlPhBryoS9Z5JA7La4zWMW3Pv4y # 07MDPbGyr5I4ftKdgCz1TlaRITUlwzluZH9TupwPrRkjhMv0ugOGjfdf8NBSv4yU # h7zAIXQlXxgotswnKDglmDlKNs98sZKuHCOnqWbsYR9q4ShJnV+I4iVd0yFLPlLE # tVc/JAPw0XpbL9Uj43BdD1FGd7P4AOG8rAKCX9vAFbO9G9RVS+c5oQ/pI0m8GLhE # fEXkwcNyeuBy5yTfv0aZxe/CHFfbg43sTUkwp6uO3+xbn6/83bBm4sGXgXvt1u1L # 50kppxMopqd9Z4DmimJ4X7IvhNdXnFy/dygo8e1twyiPLI9AN0/B4YVEicQJTMXU # pUMvdJX3bvh4IFgsE11glZo+TzOE2rCIF96eTvSWsLxGoGyY0uDWiIwLAgMBAAGj # ggHtMIIB6TAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQUSG5k5VAF04KqFzc3 # IrVtqMp1ApUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEj # iTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br # aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBe # BggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDCB # nwYDVR0gBIGXMIGUMIGRBgkrBgEEAYI3LgMwgYMwPwYIKwYBBQUHAgEWM2h0dHA6 # Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvZG9jcy9wcmltYXJ5Y3BzLmh0bTBA # BggrBgEFBQcCAjA0HjIgHQBMAGUAZwBhAGwAXwBwAG8AbABpAGMAeQBfAHMAdABh # AHQAZQBtAGUAbgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAZ/KGpZjgVHkaLtPY # dGcimwuWEeFjkplCln3SeQyQwWVfLiw++MNy0W2D/r4/6ArKO79HqaPzadtjvyI1 # pZddZYSQfYtGUFXYDJJ80hpLHPM8QotS0LD9a+M+By4pm+Y9G6XUtR13lDni6WTJ # RD14eiPzE32mkHSDjfTLJgJGKsKKELukqQUMm+1o+mgulaAqPyprWEljHwlpblqY # luSD9MCP80Yr3vw70L01724lruWvJ+3Q3fMOr5kol5hNDj0L8giJ1h/DMhji8MUt # zluetEk5CsYKwsatruWy2dsViFFFWDgycScaf7H0J/jeLDogaZiyWYlobm+nt3TD # QAUGpgEqKD6CPxNNZgvAs0314Y9/HG8VfUWnduVAKmWjw11SYobDHWM2l4bf2vP4 # 8hahmifhzaWX0O5dY0HjWwechz4GdwbRBrF1HxS+YWG18NzGGwS+30HHDiju3mUv # 7Jf2oVyW2ADWoUa9WfOXpQlLSBCZgB/QACnFsZulP0V3HjXG0qKin3p6IvpIlR+r # +0cjgPWe+L9rt0uX4ut1eBrs6jeZeRhL/9azI2h15q/6/IvrC4DqaTuv/DDtBEyO # 3991bWORPdGdVk5Pv4BXIqF4ETIheu9BCrE/+6jMpF3BoYibV3FWTkhFwELJm3Zb # CoBIa/15n8G9bW1qyVJzEw16UM0xggSOMIIEigIBATCBlTB+MQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29k # ZSBTaWduaW5nIFBDQSAyMDExAhMzAAABBGni27n7ig2DAAAAAAEEMAkGBSsOAwIa # BQCggaIwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFEa0Fgz7C42jI12g58Q+XFk9 # jBJVMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBho # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAADLE+DUS # Rk05N/xwP+myZCBzeDGnH3qtg7w+AsoGf73BJHawpHynGD26tuwCy7zdZ4iMx1bk # G5ssxFMYvLfHJEMyJDST1pOwR4+liQU4lBYs8UPVmUZvtf8D+/+mnic0HViPrjGL # eF5dmh0CzxxnY8KuqKhBlFKM+jmyd/lJ+F5+/2TcGXhRXaILho9zXQzv2ccFHwH8 # 2VhdivzAmZUAe3M2NS/KVXWLJH+/6XU/kkoBse8OK6lGlwFlYY6lCzl4v6e+t+EY # wSOOvrP5H9TZVs5pbQ9U7HfOPY5+mWdh1KxemdBq0qEhJ8I8xfua+HmVEGsc/5qP # rTkTZ9ggGWeLeqGCAigwggIkBgkqhkiG9w0BCQYxggIVMIICEQIBATCBjjB3MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEwHwYDVQQDExhNaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0ECEzMAAAEEZIjZOXzzlscAAAAAAQQwCQYFKw4D # AhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTE4MTIxMzE1NDUxNlowIwYJKoZIhvcNAQkEMRYEFFR09yLAIRoLU9UFcvsSQLIr # NAp7MA0GCSqGSIb3DQEBBQUABIIBACwAeCk+6mMqq1Z3VY6Fbtiyvipq+eYPOv4o # QtHpfurZA8vAeeuxCTt9p5iC8fWEsJ52ofzAUAtmjzjEGEQm0sykDEx9mofXeRFF # YhxYiA92MVtQoei7zgVSMoqhgg7zJyaCncKcOhH+58PT+2qzGtIQLkUIAG5+p5vr # 4WtWI5UFvBfAepb6yv1s7OCwHk9ZeavzG3G1KCDOHE1b5LsdmC2xxlQoDK2WsqAy # da6JtZmlu57zdqJGu/8JcxOhyK2a2ra+9T1SitshL1KoriLS4o86XhmRWbJ899wp # xJgzONkZpWCTbfK1xcR+FzSZBA1+3pwgfZDkGU7v2FZ5+rVNfJk= # SIG # End signature block |