M365NetworkTestingTool.psm1


#exported functions#
function Invoke-M365NetworkTestingClient {
    [CmdletBinding()]
    param(
        [switch]$DebugWithGlobalVariables
    )
    try {
        (Get-Runspace -Name MainWindow).Dispose()
        (Get-Runspace -Name Splash).Dispose()
    } catch {
        # No previous runspace to remove
    }

    if ($DebugWithGlobalVariables) {
        $Global:uiHash = [hashtable]::Synchronized(@{})
        $Global:variableHash = [hashtable]::Synchronized(@{})
    } else {
        $uiHash = [hashtable]::Synchronized(@{})
        $variableHash = [hashtable]::Synchronized(@{})
    }

    
    #store runspaces in jobs array so we can dispose of them when we're done
    $Jobs = @{}
    $uiHash.Jobs = $Jobs
    $Hosts = @{}
    $uiHash.Hosts = $Hosts
    $Handles = @{}
    $uiHash.Handles = $Handles
    $RunspaceOutput = @{}
    $uiHash.RunspaceOutput = $RunspaceOutput    
    
    $uiHash.resultsHash = $null
    $uiHash.AdminMode = $false
    
    #set static paths for info links
    $variableHash.navInfoOs = "https://myadvisor.fasttrack.microsoft.com/"
    $variableHash.navInfoInternet = "https://myadvisor.fasttrack.microsoft.com/"
    $variableHash.navInfoHeadset = "https://myadvisor.fasttrack.microsoft.com/"
    $variableHash.navInfoTool = "https://myadvisor.fasttrack.microsoft.com/"
    $variableHash.navCallQuality = "https://docs.microsoft.com/en-ca/SkypeForBusiness/optimizing-your-network/media-quality-and-network-connectivity-performance"
    $variableHash.InternetTestIp = "13.107.64.2" #need this as we may not have the tool installed yet to pull the .config file and read the setting
    $variableHash.ReadyToStartTests = $false

    $variableHash.ThisModule = Get-Module M365NetworkTestingTool
    [array]$variableHash.allModules = Get-Module M365NetworkTestingTool -ListAvailable
    $variableHash.RootPath = Split-Path -Path $variableHash.ThisModule.Path
    $variableHash.osMinVer = "6.1.7601"
    
    $variableHash.resultsAnalyzerTempFile = $env:TEMP + "\results_temp.csv"
    $variableHash.resultsAnalyzerTextFile = $env:TEMP + "\results_analyzer.txt"
    $variableHash.connectivityCheckResults = $env:TEMP + "\connectivity_results.txt"

    [System.Version]$variableHash.ToolVersion = "2018.7.0.28"
    $variableHash.toolName = "Microsoft Skype for Business Network Assessment Tool"
    $variableHash.networkAssessmentToolPath = "C:\Program Files\Microsoft Skype for Business Network Assessment Tool\NetworkAssessmentTool.exe"
    $variableHash.networkAssessmentToolConfig = "C:\Program Files\Microsoft Skype for Business Network Assessment Tool\NetworkAssessmentTool.exe.config"
    $variableHash.networkAssessmentToolWorkingDirectory = "C:\Program Files\Microsoft Skype for Business Network Assessment Tool\"

    $uiHash.checkMark = $variableHash.RootPath + "\assets\check.png"
    $uiHash.errorMark = $variableHash.RootPath + "\assets\error.png"
    $uiHash.warningMark = $variableHash.RootPath + "\assets\warning.png"
    $uiHash.questionMark = $variableHash.RootPath + "\assets\question.png"

    $uiHash.AutoUpdate = (Get-M365RegistryEntries).AutoUpdate
    $uiHash.CheckUpdateContent = "Check for Updates"

    $splashMain = {
        $uiHash.Hosts.("RspSplash") = $Host
        Add-Type -AssemblyName PresentationFramework
        
        $uiContent = Get-Content -Path ($variableHash.rootPath + "\Splash.xaml")
        
        [xml]$xAML = $uiContent -replace 'mc:Ignorable="d"','' -replace "x:N",'N'  -replace '^<Win.*', '<Window'
        $xmlReader = (New-Object System.Xml.XmlNodeReader $xAML)
        $uiHash.Splash = [Windows.Markup.XamlReader]::Load($xmlReader)

        $xAML.SelectNodes("//*[@Name]") | ForEach-Object {

            $uiHash.Add($_.Name, $uiHash.Splash.FindName($_.Name))

        }

        $uiHash.Splash.Add_SourceInitialized(
            {

                $assets = $variableHash.RootPath + "\assets\"
                $uiHash.imgSplashLogo.Source = $variableHash.RootPath + "\assets\corplogo_small.png"
                $uiHash.imgTeamsLogo.Source = $variableHash.RootPath + "\assets\msft_teams_logo_small.png"
                $uiHash.imgSFBLogo.Source = $variableHash.RootPath + "\assets\sfb_logo_small.png"
                $uiHash.Splash.Icon = $assets + "msft_logo.png"

                $uiUpdateSplash = {

                    $uiHash.txtSplashStatus.Text = $uiHash.SplashStatusText

                }

                #Create timer to handle updating the UI
                $timer = new-object System.Windows.Threading.DispatcherTimer
                $timer.Interval = [TimeSpan]"0:0:0:0.100"
                $timer.Add_Tick($uiUpdateSplash)
                $timer.Start()
            }
        )

        $uiHash.Splash.Add_Loaded(
            {

                $wid = [System.Security.Principal.WindowsIdentity]::GetCurrent()
                $prp = New-Object System.Security.Principal.WindowsPrincipal($wid)
                $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator
                
                if ($prp.IsInRole($adm)) {
                    
                    $uiHash.AdminMode = $true

                }

                $codeBootstrap = {
                    
                    Invoke-M365NetworkTestingToolAutoUpdate -CheckForUpdates
                
                }

                Invoke-NewRunspace -codeBlock $codeBootstrap -RunspaceHandleName Bootstrap

            }
        )

        $uiHash.Splash.ShowDialog() | Out-Null
        $uiHash.Splash.Error = $Error

    }

    Write-Output "Attempting to start the UI..."
    Invoke-NewRunspace -codeBlock $splashMain -RunspaceHandleName Splash

    do {

        Start-Sleep -Milliseconds 500

    } until ($uiHash.Handles.Bootstrap.IsCompleted)

    #close out possible runspaces

    if ($variableHash.ExitApplication) {

        Write-Output "Exiting due to new update..."
        exit

    }
    
    $mainWindow = {

        $uiHash.Hosts.("RspMain") = $Host
        Add-Type -AssemblyName PresentationFramework
        
        $uiContent = Get-Content -Path ($variableHash.RootPath + "\MainWindow.xaml")
        
        [xml]$xAML = $uiContent -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
        $xmlReader = (New-Object System.Xml.XmlNodeReader $xAML)
        $uiHash.Window = [Windows.Markup.XamlReader]::Load($xmlReader)

        $xAML.SelectNodes("//*[@Name]") | ForEach-Object {

            $uiHash.Add($_.Name, $uiHash.Window.FindName($_.Name))

        }

        #region EVENTS #
            $uiHash.Window.Add_SourceInitialized(
                {

                    $uiHash.Window.Resources.ConnectivityTimeout = @(90,120,150,190)
                    $uiHash.Window.Resources.NumAudioTests = @(1..50)
                    $uiHash.Window.Resources.AudioTestDelay = @(1,5,10,20,30,60,90,120)

                    [int]$uiHash.NumAudioTests = (Get-M365RegistryEntries).NumberOfAudioTestIterations
                    [int]$uiHash.AudioTestDelay = (Get-M365RegistryEntries).TestIntervalInSeconds
                    [int]$uiHash.ConnectivityTimeout = (Get-M365RegistryEntries).ConnectivityTimeoutInSeconds

                    if ($uiHash.NumAudioTests) {

                        $uiHash.cmbNumAudioTests.SelectedItem = $uiHash.NumAudioTests

                    } else {

                        $uiHash.NumAudioTests = $uiHash.cmbNumAudioTests.SelectedItem = $uiHash.Window.Resources.NumAudioTests[0]

                    }

                    if ($uiHash.AudioTestDelay) {

                        $uiHash.cmbAudioTestDelay.SelectedItem = $uiHash.AudioTestDelay

                    } else {

                        $uiHash.AudioTestDelay = $uiHash.cmbAudioTestDelay.SelectedItem = $uiHash.Window.Resources.AudioTestDelay[0]

                    }

                    if ($uiHash.ConnectivityTimeout) {
                        
                        $uiHash.cmbConnectivityTimeout.SelectedItem = $uiHash.ConnectivityTimeout

                    } else {

                        $uiHash.ConnectivityTimeout = $uiHash.cmbConnectivityTimeout.SelectedItem = $uiHash.Window.Resources.ConnectivityTimeout[0]

                    }

                    if ($uiHash.AdminMode){

                        $uiHash.Window.Title = "Administrator: " + $uiHash.Window.Title

                    }
                   
                    #set image locations
                    $assets = $variableHash.RootPath + "\assets\"

                    $uiHash.imgMSFTLogo.Source = $assets + "msft_logo.png"
                    $uiHash.imgInternetLogo.Source = $assets + "network.png"
                    $uiHash.imgHeadsetLogo.Source = $assets + "headset.png"
                    $uiHash.imgToolLogo.Source = $assets + "tool.png"
                    $uiHash.Window.Icon = $assets + "msft_logo.png"

                    $uiHash.TestQualitySource = $uiHash.questionMark
                    $uiHash.TestQualityDetailSource = $uiHash.questionMark
                    $uiHash.TestConnectivitySource = $uiHash.questionMark
                    $uiHash.TestConnectivityDetailSource = $uiHash.questionMark
                    
                    $uiHash.PacketLossRateSource = $uiHash.questionMark
                    $uiHash.RoundTripTimeSource = $uiHash.questionMark
                    $uiHash.JitterSource = $uiHash.questionMark
                    $uiHash.PacketReorderRatioSource = $uiHash.questionMark

                    $uiHash.imgInfoOs.Source = $assets + "info.png"
                    $uiHash.imgInfoInternet.Source = $assets + "info.png"
                    $uiHash.imgInfoHeadset.Source = $assets + "info.png"
                    $uiHash.imgInfoTool.Source = $assets + "info.png"
                    $uiHash.imgEmailResults.Source = $assets + "email.png"
                    $uiHash.reportQuality.Source = $assets + "doc.png"
                    $uiHash.reportConnectivity.Source = $assets + "doc.png"

                    $uiHash.ActionButtonText = "Checking"
                    $uiHash.ActionButtonFill = "#FFFBBC0B" #yellow
                    $uiHash.CheckUpdateEnabled = $true

                    $uiUpdateBlock = {

                        $uiHash.barTest.Value = $uiHash.ProgressValue

                        $uiHash.txtStatus.Text = $uiHash.StatusText
                        $uiHash.txtActionButton.Text = $uiHash.ActionButtonText
                        $uiHash.elipActionButton.Fill = $uiHash.ActionButtonFill
                        
                        $uiHash.txtConnectivityDetail.Text = $uiHash.ConnectivityDetailText
                        $uiHash.txtPacketLossRate.Text = $uiHash.PacketLossRateText
                        $uiHash.txtRoundTripTime.Text = $uiHash.RoundTripTimeText
                        $uiHash.txtJitter.Text = $uiHash.JitterText
                        $uiHash.txtPacketReorderRatio.Text = $uiHash.PacketReorderRatioText

                        #this block needs to be set as we look for the update from a previous runspace
                        $uiHash.txtCurrentVersion.Text = $uiHash.CurrentVersionText
                        $uiHash.txtPSGVersion.Text = $uiHash.PSGVersionText

                        #update check/x-marks
                        $uiHash.imgTestQuality.Source = $uiHash.TestQualitySource
                        $uiHash.imgTestConnectivity.Source = $uiHash.TestConnectivitySource
                        $uiHash.imgQualityDetail.Source = $uiHash.TestQualityDetailSource
                        $uiHash.imgConnectivityDetail.Source = $uiHash.TestConnectivityDetailSource
                        $uiHash.imgPacketLossRate.Source = $uiHash.PacketLossRateSource
                        $uiHash.imgRoundTripTime.Source = $uiHash.RoundTripTimeSource
                        $uiHash.imgJitter.Source = $uiHash.JitterSource
                        $uiHash.imgPacketReorderRatio.Source = $uiHash.PacketReorderRatioSource

                        #validate pre-requisites
                        if ($variableHash.OSReady -and $variableHash.InternetReady -and $variableHash.ToolReady -and $variableHash.TestingIdle) {
                            $uiHash.ActionButtonFill = "#FF80CC28"
                            $uiHash.ActionButtonText = "Start"
                        }
                    }

                    #Create timer to handle updating the UI
                    $timer = new-object System.Windows.Threading.DispatcherTimer
                    $timer.Interval = [TimeSpan]"0:0:0:0.30"
                    $timer.Add_Tick($uiUpdateBlock)
                    $timer.Start()
                }
            )

            $uiHash.Window.Add_Loaded(
                {
                    #close splash screen
                    $uiHash.Splash.Dispatcher.Invoke([action]{$uiHash.Splash.Close()})
                    (Get-Runspace -Name Bootstrap).Dispose()
                    (Get-Runspace -Name Splash).Dispose()

                    function CheckOs {
                    
                        $osVersion = [System.Environment]::OSVersion

                        if ($osVersion.Version -ge $variableHash.osMinVer) {

                            if ([System.Environment]::Is64BitOperatingSystem) {

                                $uiHash.txtOs.Text = "Windows version $($osVersion.Version.ToString()) has been detected and is a 64-bit operating system."
                                $uiHash.imgOs.Source = $uiHash.checkMark
                                $variableHash.OSReady = $true

                            } else {

                                #not 64-bit
                                $uiHash.txtOs.Text = "Windows version $($osVersion.Version.ToString()) has been detected and is not a 64-bit operating system."
                                $uiHash.imgOs.Source = $uiHash.errorMark

                            }
                        } else {

                            #not Win7 or higher
                            $uiHash.txtOs.Text = "Windows version $($osVersion.Version.ToString()) has been detected and does not match the minimum required version. Click the info button to learn more."
                            $uiHash.imgOs.Source = $uiHash.errorMark

                        }

                    }

                    CheckOs

                    $preReqInternetCheck = {

                        Invoke-InternetConnectionTest

                    }                    

                    $preReqEndpointCheck = {

                        Invoke-CheckCertifiedEndpoint

                    }
                        
                    $preReqToolCheck = {

                        Invoke-CheckToolInstall

                    }
                        
                    #these two are used elsewhere in the module and they take longer so we
                    #put them in a runspace to surface the UI faster and keep them in root functions
                    #so other functions can call them

                    Invoke-NewRunspace -codeBlock $preReqInternetCheck -RunspaceHandleName PreReqInternetCheck
                    Invoke-NewRunspace -codeBlock $preReqEndpointCheck -RunspaceHandleName PreReqEndpointCheck
                    Invoke-NewRunspace -codeBlock $preReqToolCheck -RunspaceHandleName PreReqToolCheck
                    
                    Invoke-CalculateEstimatedTime
                    
                    $uiHash.StatusText = "Click the start button to begin testing"
                    $variableHash.TestingIdle = $true

                }
            )

            $uiHash.Window.Add_Closing(
                {
                    #this is where we do our cleanup to prevent memory leaks.
                    #we don't want these runspaces to consume memory if they're
                    #not in use even after the window has been closed.

                    Remove-Item $variableHash.resultsAnalyzerTextFile -Force -ErrorAction SilentlyContinue
                    Remove-Item $variableHash.connectivityCheckResults -Force -ErrorAction SilentlyContinue
                    Remove-Item $variableHash.resultsAnalyzerTempFile -Force -ErrorAction SilentlyContinue
                    
                    Set-M365RegistryEntries

                }
            )

            $uiHash.Window.Add_Closed(
                {
                    $uiHash.Jobs.GetEnumerator() | ForEach-Object {

                        if ($_.Name -eq "MainWindow") {

                            #leave MainWindow as this causes issues. We clean it up on launch. Maybe a better way?

                        } else {

                            (Get-Runspace -Name $_.Name).Dispose()

                        }
                    }
                }
            )

            $uiHash.cmbConnectivityTimeout.Add_DropDownClosed(
                {

                    $uiHash.ConnectivityTimeout = $uiHash.cmbConnectivityTimeout.SelectedItem

                }
            )

            $uiHash.cmbNumAudioTests.Add_DropDownClosed(
                {

                    [int]$uiHash.NumAudioTests = $uiHash.cmbNumAudioTests.SelectedItem
                    #$timeCalc = [math]::Round($(([int]$uiHash.NumAudioTests * (40 + [int]$uiHash.AudioTestDelay)) / 60),1) # 40s/test
                    #$uiHash.txtEstimatedTimeToRun.Text = "Estimated time to complete tests (minutes): " + $timeCalc
                    Invoke-CalculateEstimatedTime
                }
            )

            $uiHash.cmbAudioTestDelay.Add_DropDownClosed(
                {

                    [int]$uiHash.AudioTestDelay = $uiHash.cmbAudioTestDelay.SelectedItem
                    Invoke-CalculateEstimatedTime
                }
            )

            $uiHash.barTest.Add_ValueChanged(
                {

                    $uiHash.Window.Resources.ProgressValue = $uiHash.barTest.Value / 100

                }
            )

            $uiHash.elipActionButton.Add_MouseUp(
                {
                    Invoke-ActionButton
                }
            )
            
            $uiHash.txtActionButton.Add_MouseUp(
                {
                    Invoke-ActionButton
                }
            )

            $uiHash.navMailTo.Add_Click(
                {
                    Start-Process $uiHash.navMailTo.NavigateUri
                }
            )

            $uiHash.navMyAdvisor.Add_Click(
                {
                    Start-Process $uiHash.navMyAdvisor.NavigateUri
                }
            )

            $uiHash.imgInfoOs.Add_MouseUp(
                {
                    Start-Process $variableHash.navInfoOs
                }
            )

            $uiHash.imgInfoInternet.Add_MouseUp(
                {
                    Start-Process $variableHash.navInfoInternet
                }
            )

            $uiHash.imgInfoHeadset.Add_MouseUp(
                {
                    Start-Process $variableHash.navInfoHeadset
                }
            )

            $uiHash.imgInfoTool.Add_MouseUp(
                {
                    Start-Process $variableHash.navInfoTool
                }
            )

            $uiHash.reportQuality.Add_MouseUp(
                {
                    Start-Process $variableHash.resultsAnalyzerTextFile
                }
            )

            $uiHash.reportConnectivity.Add_MouseUp(
                {
                    Start-Process $variableHash.connectivityCheckResults
                }
            )

            $uiHash.imgTestQuality.Add_MouseUp(
                {
                    Start-Process $variableHash.navCallQuality
                }
            )

            $uiHash.imgPacketLossRate.Add_MouseUp(
                {
                    Start-Process $variableHash.navCallQuality
                }
            )
            
            $uiHash.imgRoundTripTime.Add_MouseUp(
                {
                    Start-Process $variableHash.navCallQuality
                }
            )
            
            $uiHash.imgJitter.Add_MouseUp(
                {
                    Start-Process $variableHash.navCallQuality
                }
            )

            $uiHash.imgPacketReorderRatio.Add_MouseUp(
                {
                    Start-Process $variableHash.navCallQuality
                }
            )

            $uiHash.imgQualityDetail.Add_MouseUp(
                {
                    Start-Process $variableHash.navCallQuality
                }
            )

        #end region

        [void]$uiHash.Window.Dispatcher.InvokeAsync{$uiHash.Window.ShowDialog()}.Wait()
        $uiHash.Error = $Error
    }

    Invoke-NewRunspace -codeBlock $mainWindow -RunspaceHandleName MainWindow

    if (!$DebugWithGlobalVariables) {

        while (!$uiHash.Handles.MainWindow.IsCompleted) {

            Start-Sleep -Seconds 1

        }
    
        Write-Output "Closing the application..."

    }

}

function Invoke-NewRunspace {
    [cmdletbinding()]
    param(
        [parameter(Mandatory=$true)][scriptblock]$codeBlock,
        [parameter(Mandatory=$true)][string]$RunspaceHandleName
    )

    $testingRunspace = [runspacefactory]::CreateRunspace()
    $testingRunspace.ApartmentState = "STA"
    $testingRunspace.ThreadOptions = "ReuseThread"
    $testingRunspace.Open()
    $testingRunspace.SessionStateProxy.SetVariable("uiHash",$uiHash)
    $testingRunspace.SessionStateProxy.SetVariable("variableHash",$variableHash)
    $testingRunspace.Name = $RunspaceHandleName

    $uiHash.RunspaceOutput.($RunspaceHandleName) = New-Object System.Management.Automation.PSDataCollection[psobject]

    $testingCmd = [PowerShell]::Create().AddScript($codeBlock)
    
    $testingCmd.Runspace = $testingRunspace
    
    $testingHandle = $testingCmd.BeginInvoke($uiHash.RunspaceOutput.($RunspaceHandleName),$uiHash.RunspaceOutput.($RunspaceHandleName))

    #store the handle in the global sync'd hashtable; arraylist we'll use the window.closed() event to clean up
    $uiHash.Jobs.($RunspaceHandleName) = $testingCmd
    $uiHash.Handles.($RunspaceHandleName) = $testingHandle
}

function Invoke-ActionButton {

    $actionBlock = {

        if ($uiHash.ActionButtonText -eq "Start") {

            Invoke-InternetConnectionTest
            if (!$variableHash.InternetReady){
                throw
            }

            Remove-Item $variableHash.resultsAnalyzerTextFile -Force -ErrorAction SilentlyContinue
            Remove-Item $variableHash.connectivityCheckResults -Force -ErrorAction SilentlyContinue
            Remove-Item $variableHash.resultsAnalyzerTempFile -Force -ErrorAction SilentlyContinue
            Remove-Item $variableHash.AudioTestSavePath -Force -ErrorAction SilentlyContinue
            Remove-Item $variableHash.ConnectionTestSavePath -Force -ErrorAction SilentlyContinue

            $variableHash.TestingIdle = $false
            $uiHash.TestQualitySource = $uiHash.questionMark
            $uiHash.TestQualityDetailSource = $uiHash.questionMark
            $uiHash.TestConnectivitySource = $uiHash.questionMark
            $uiHash.TestConnectivityDetailSource = $uiHash.questionMark
            $uiHash.PacketLossRateSource = $uiHash.questionMark
            $uiHash.RoundTripTimeSource = $uiHash.questionMark
            $uiHash.JitterSource = $uiHash.questionMark
            $uiHash.PacketReorderRatioSource = $uiHash.questionMark

            $uiHash.JitterText = ""
            $uiHash.PacketLossRateText = ""
            $uiHash.PacketReorderRatioText = ""
            $uiHash.RoundTripTimeText = ""

            $totalIterations = $uiHash.NumAudioTests

            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.barTest.Visibility = "Visible"})

            Invoke-ConnectivityTest

            if ($variableHash.NumTestsFromXml -ne "1") {

                $uiHash.StatusText = "Config file NumIterations is {0}. Please change it back to 1." -f $variableHash.NumTestsFromXml
                throw

            }

            for ($testIteration = 1; $testIteration -le $totalIterations; $testIteration++) {

                if ($uiHash.StopTesting) {

                    break;

                }

                try {
                    
                    $uiHash.StatusText = "Running audio quality test {0}/{1}" -f $testIteration, $totalIterations
                    $uiHash.ProgressValue = $testIteration / ($totalIterations + 1) * 100
                    Invoke-AudioTest -testIteration $testIteration -totalIterations $totalIterations -ErrorVariable AudioTestError
                    
                    $uiHash.StatusText = "Pausing for {0} seconds..." -f $uiHash.AudioTestDelay
                    Start-Sleep -Seconds $uiHash.AudioTestDelay

                } catch {

                    $uiHash.StatusText = $_.Exception.Message
                    throw

                }

                if ($audioTestError) {

                    $uiHash.StatusText = $audioTestError
                    throw

                }
            }
            
            Invoke-ProcessAudioResults

            $uiHash.ProgressValue = 0
            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.barTest.Visibility = "Hidden"})
            $uiHash.StatusText = "Click the start button to begin testing"

        }

        if ($uiHash.ActionButtonText -eq "Install") {
            Invoke-NetworkAssessmentToolServicing -ServicingType Install
            Invoke-CheckToolInstall
        }

        if ($uiHash.ActionButtonText -eq "Update") {
            Invoke-NetworkAssessmentToolServicing -ServicingType Uninstall
            Invoke-NetworkAssessmentToolServicing -ServicingType Install
            Invoke-CheckToolInstall
        }

        $variableHash.TestingIdle = $true        
    }

    if ($uiHash.ActionButtonText -eq "Stop") {

        $uiHash.ActionButtonText = "Stopping"
        $uiHash.StopTesting = $true        

    } elseif ($uiHash.ActionButtonText -eq "Running") {

        #do nothing

    } else {

        Invoke-NewRunspace -codeBlock $actionBlock -RunspaceHandleName RspActionButton

    }
    
}

function Invoke-AudioTest {
    [cmdletbinding()]
    param(
        [parameter(Mandatory=$true)][int]$testIteration,
        [parameter(Mandatory=$true)][int]$totalIterations
    )
    
    #set maximum time limit for the process to run. It should only take about 17s to execute
        
    $stopWatch = [Diagnostics.Stopwatch]::StartNew()
    [int]$timeLimit = 30
    $assessmentInstance = Start-Process -FilePath $variableHash.networkAssessmentToolPath -WorkingDirectory $variableHash.networkAssessmentToolWorkingDirectory -WindowStyle Hidden -PassThru
    
    $uiHash.ActionButtonText = "Stop"

    do {

        Start-Sleep -Milliseconds 500

    } until (($assessmentInstance.HasExited -eq $true) -or ($stopWatch.Elapsed.TotalSeconds -ge $timeLimit))

    $stopWatch.Stop()
    
    #did it complete or did we have to exit?
    if ($assessmentInstance.HasExited) {

        #read the results from the file into memory
        if ($variableHash.Delimiter -ne ",") {
            $results = Import-Csv -Path $variableHash.AudioTestSavePath -Delimiter `t
        } else {
            $results = Import-Csv -Path $variableHash.AudioTestSavePath -Delimiter $variableHash.Delimiter
        }
        
        foreach ($row in $results) {

            $row.CallStartTime = (([datetime]::parse($row.CallStartTime)).ToUniversalTime()).tostring('u')
            $row.PacketLossRate = [single]::parse($row.PacketLossRate)
            $row.RoundTripLatencyInMs = [single]::parse($row.RoundTripLatencyInMs)
            $row.RoundTripLatencyInMs = [int]$row.RoundTripLatencyInMs
            $row.PacketsSent = [int]$row.PacketsSent
            $row.PacketsReceived = [int]$row.PacketsReceived
            $row.AverageJitterInMs = [single]::parse($row.AverageJitterInMs)
            $row.PacketReorderRatio = [single]::parse($row.PacketReorderRatio)

            [array]$variableHash.TestCallData += $row
        }
    } else {
        #kill it
        $assessmentInstance.Kill()
        $uiHash.StatusText = "The test took longer than $timeLimit seconds and was killed."
        Start-Sleep -Seconds 3
    }
}

function Invoke-ConnectivityTest {
    
    $uiHash.StatusText = "Running connectivity test"
    $uiHash.ActionButtonText = "Running"
    $stopWatch = [Diagnostics.Stopwatch]::StartNew()
    [int]$timeLimit = $uiHash.ConnectivityTimeout #we've observed some PC's taking up to 66 seconds to complete this test

    $connTestArgs = "/connectivitycheck /verbose"
    $assessmentInstance = Start-Process -FilePath $variableHash.networkAssessmentToolPath -WorkingDirectory $variableHash.networkAssessmentToolWorkingDirectory -ArgumentList $connTestArgs -WindowStyle Hidden -PassThru -RedirectStandardOutput $variableHash.connectivityCheckResults
    
    do {
        
        $uiHash.ProgressValue = $stopWatch.Elapsed.Seconds / $timeLimit * 100
        Start-Sleep -Milliseconds 100

    } until (($assessmentInstance.HasExited -eq $true) -or ($stopWatch.Elapsed.TotalSeconds -ge $timeLimit))

    $stopWatch.Stop()
    
    #did it complete or did we have to exit?
    if ($assessmentInstance.HasExited) {

        $uiHash.StatusText = "Completed connectivity test"
        $uiHash.ProgressValue = 100
        Start-Sleep -Seconds 2
        #upload results

    } else {

        #kill it
        $assessmentInstance.Kill()
        $uiHash.ActionButtonText = "The test took longer than $timeLimit to complete and was killed."
        $uiHash.StatusText = "The test timed out before it completed."
        $uiHash.ActionButtonText = "Start"
        throw

    }

    #read results from file
    try {
        $connTestResults = Get-Content -Path $variableHash.connectivityCheckResults
    } catch {

        $uiHash.ActionButtonText = "Could not retrieve connectivity check results!"
        $uiHash.ActionButtonFill = "Red"
        throw

    }

    if ($connTestResults.Contains("Verifications completed successfully")) {

        $uiHash.TestConnectivitySource = $uiHash.checkMark
        $uiHash.TestConnectivityDetailSource = $uiHash.checkMark
        $uiHash.ConnectivityDetailText = "No issues detected. The tool was able to reach all transport relays on all required ports."

    } else {

        $uiHash.TestConnectivitySource = $uiHash.errorMark
        $uiHash.TestConnectivityDetailSource = $uiHash.errorMark
        $uiHash.ConnectivityDetailText = "There was a problem connecting to one or more transport or media relays on the required ports. Click the report icon to view the results."

    }

    $uiHash.ProgressValue = 0

}

function Invoke-CheckToolInstall {

    if (Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object DisplayName -eq $variableHash.toolName){
        try {
            [xml]$xmlFile = Get-Content -Path $variableHash.networkAssessmentToolConfig
        } catch {
            $uiHash.StatusText = "Error reading config XML file."
            $uiHash.ActionButtonFill = "Red"
            $uiHash.ActionButtonText = "Error"
            throw
        }
        
        $variableHash.Delimiter = ($xmlFile.configuration.AppSettings.add | Where-Object Key -eq "Delimiter").Value
        $variableHash.NumTestsFromXml = ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq NumIterations).Value
        $variableHash.AudioTestSavePath = ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq ResultsFilePath).value

        if ((Split-Path -Path $variableHash.AudioTestSavePath) -eq "") {

            $variableHash.AudioTestSavePath = $env:LOCALAPPDATA + "\Microsoft Skype for Business Network Assessment Tool\" + $variableHash.AudioTestSavePath

        }

        $variableHash.ConnectionTestSavePath = ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq OutputFilePath).value
        if ((Split-Path -Path $variableHash.ConnectionTestSavePath) -eq "") {
            $variableHash.ConnectionTestSavePath = $env:LOCALAPPDATA + "\Microsoft Skype for Business Network Assessment Tool\" + $variableHash.ConnectionTestSavePath
        }

        $installedVersion = Get-Item $variableHash.networkAssessmentToolPath

        if ([System.Version]$installedVersion.VersionInfo.FileVersion -lt [System.Version]$variableHash.ToolVersion) {

            #needs update
            $msg = "The version detected was {0} and needs to be updated to version {1}" -f  $installedVersion.VersionInfo.FileVersion, $variableHash.ToolVersion
            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtTool.Text = $msg})

            $uiHash.StatusText = "Click the Update button to upgrade the Network Assessment Tool."
            $uiHash.ActionButtonText = "Update"
            $uiHash.ActionButtonFill = "#FF80CC28" #green
            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgTool.Source = $uiHash.warningMark})
            $variableHash.ToolReady = $false

        } else {

            #good to go; but make sure the config file is modified

            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtTool.Text = "The version detected on this computer is compatible with this application."})
            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgTool.Source = $uiHash.checkMark})

            $variableHash.ToolReady = $true

        }
    } else {

        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtTool.Text = "The Network Assessment Tool was not detected. Click the install button to begin the installation."})
        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgTool.Source = $uiHash.warningMark})
        $uiHash.ActionButtonText = "Install"
        $uiHash.ActionButtonFill = "#FF80CC28" #green
        $variableHash.ToolReady = $false

    }

    $variableHash.TestingIdle = $true
}

function Invoke-InternetConnectionTest {

    $variableHash.InternetReady = $false

    try {

        $Socket = New-Object System.Net.Sockets.TCPClient
        $Connection = $Socket.BeginConnect($variableHash.InternetTestIp, 443, $null, $null)
        $Connection.AsyncWaitHandle.WaitOne(2000,$false) | Out-Null

    } catch {

        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtInternet.Text = "There was a problem executing the test for an Internet connection"})
        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgInternet.Source = $uiHash.errorMark})

    }

    if ($Socket.Connected) {

        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtInternet.Text = "Your Internet connection has been successfully verified."})
        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgInternet.Source = $uiHash.checkMark})
        $variableHash.InternetReady = $true

    } else {

        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtInternet.Text = "Unable to verify Internet connectivity to $($variableHash.InternetTestIp). An Internet connection is necessary to start testing."})
        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgInternet.Source = $uiHash.errorMark})

    }

    $Socket.Close | Out-Null

}

function Invoke-ProcessAudioResults {

    #gather test results in memory, write to file, then run results analyzer against it
    $uiHash.ProgressValue = $uiHash.ProgressValue + $uiHash.ProgressValue / 2
    $uiHash.StatusText = "Exporting results from {0} test(s)" -f $variableHash.TestCallData.Count
    Start-Sleep -Seconds 1

    try {
        ($variableHash.TestCallData | ConvertTo-Csv -Delimiter `t -NoTypeInformation).replace('"',"") | Out-File -FilePath $variableHash.resultsAnalyzerTempFile
    } catch {
        $uiHash.StatusText = "There was an error exporting the results: {0}" -f $_.Exception.Message
        Start-Sleep -Seconds 2
        throw
    }
    
    try {
        $uiHash.ProgressValue = 100
        $uiHash.StatusText = "Analyzing audio test results"
        Start-Sleep -Seconds 1
        $argList = '"{0}" {1}' -f $variableHash.resultsAnalyzerTempFile, "`t"
        $networkAssessmentResultsAnalyzer = "C:\Program Files\Microsoft Skype for Business Network Assessment Tool\ResultsAnalyzer.exe"
        Start-Process -FilePath $networkAssessmentResultsAnalyzer -WorkingDirectory $variableHash.networkAssessmentToolWorkingDirectory -ArgumentList $argList -PassThru -Wait -WindowStyle Hidden -RedirectStandardOutput $variableHash.resultsAnalyzerTextFile
    } catch {
        $uiHash.StatusText = "There was a problem starting the results analyzer tool."
        throw
    }

    #parse txt file
    $tempResults = Get-Content -Path $variableHash.resultsAnalyzerTextFile

    $uiHash.packetLossRate = ((($tempResults | Where-Object {$_ -like "Packet loss rate:*"})[1] -split ":")[1]).trim()
    $uiHash.PacketLossRateText = ((($tempResults | Where-Object {$_ -like "Packet loss rate:*"})[0] -split ":")[1]).trim()
    if ($uiHash.packetLossRate -eq "PASSED") {
        $uiHash.PacketLossRateSource = $uiHash.checkMark
    } else {
        $uiHash.PacketLossRateSource = $uiHash.errorMark
    }

    $uiHash.rttLatency = ((($tempResults | Where-Object {$_ -like "RTT latency:*"})[1] -split ":")[1]).trim()
    $uiHash.RoundTripTimeText = ((($tempResults | Where-Object {$_ -like "RTT latency:*"})[0] -split ":")[1]).trim()
    if ($uiHash.rttLatency -eq "PASSED") {
        $uiHash.RoundTripTimeSource = $uiHash.checkMark
    } else {
        $uiHash.RoundTripTimeSource = $uiHash.errorMark
    }

    $uiHash.jitter = ((($tempResults | Where-Object {$_ -like "Jitter:*"})[1] -split ":")[1]).trim()
    $uiHash.JitterText = ((($tempResults | Where-Object {$_ -like "Jitter:*"})[0] -split ":")[1]).trim()
    if ($uiHash.jitter -eq "PASSED") {
        $uiHash.JitterSource = $uiHash.checkMark
    } else {
        $uiHash.JitterSource = $uiHash.errorMark
    }

    $uiHash.packetReorderRatio = ((($tempResults | Where-Object {$_ -like "Packet reorder ratio:*"})[1] -split ":")[1]).trim()
    $uiHash.PacketReorderRatioText = ((($tempResults | Where-Object {$_ -like "Packet reorder ratio:*"})[0] -split ":")[1]).trim()
    if ($uiHash.packetReorderRatio -eq "PASSED") {
        $uiHash.PacketReorderRatioSource = $uiHash.checkMark
    } else {
        $uiHash.PacketReorderRatioSource = $uiHash.errorMark
    }

    if ($uiHash.packetLossRate -eq "PASSED" -and $uiHash.rttLatency -eq "PASSED" -and $uiHash.jitter -eq "PASSED" -and $uiHash.packetReorderRatio -eq "PASSED") {
        $uiHash.TestQualitySource = $uiHash.checkMark
        $uiHash.TestQualityDetailSource = $uiHash.checkMark
    } else {
        $uiHash.TestQualitySource = $uiHash.errorMark
        $uiHash.TestQualityDetailSource = $uiHash.errorMark
    }

    $variableHash.TestCallData = $null
    $uiHash.StopTesting = $false

}

function Invoke-CheckCertifiedEndpoint {

    [bool]$isDeviceCertified = $false
    
    if (Test-Path -path $($variableHash.RootPath + "\Deviceparserstandalone.dll")) {

        try {

            Add-Type -Path $($variableHash.RootPath + "\Deviceparserstandalone.dll")
            [array]$usbAudio = Get-WmiObject Win32_PnPEntity | Where-Object {$_.Service -eq 'usbaudio' -and $_.Status -eq "OK"}
            $usbAudio | ForEach-Object {

                if ([SkypeHelpers.CQTools.DeviceParser+Audio]::IsCertified($_.Caption)) {

                    [array]$certifiedDevices += $_.Caption

                }

            }

            if ($certifiedDevices) {

                $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtHeadset.Text = "A Teams/Skype-certified device was detected."})
                $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgHeadset.Source = $uiHash.checkMark})

            } else {

                $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtHeadset.Text = "A Teams/Skype-certified device was not detected. Please click the info icon to learn more."})
                $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgHeadset.Source = $uiHash.warningMark})

            }
            
        } catch {

            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtHeadset.Text = "There was an error loading the necessary file in memory to determine the headset type."})
            $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgHeadset.Source = $uiHash.warningMark})

        }
    } else {

        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.txtHeadset.Text = "Could not locate the necessary files to determine the headset type."})
        $uiHash.Window.Dispatcher.Invoke([action]{$uiHash.imgHeadset.Source = $uiHash.warningMark})

    }
}

function Invoke-NetworkAssessmentToolServicing {
    [CmdletBinding()]
    param(
        [parameter()][validateset("Install","Uninstall")]$ServicingType
    )
    
    begin{
        #set function constants
        $modulePath = $variableHash.RootPath
        $toolFileName = $modulePath + "\MicrosoftSkypeForBusinessNetworkAssessmentTool.exe"
        $toolInstallArgs = "/install /quiet /norestart /log $env:TEMP\ToolInstall.txt"
        $toolUninstallArgs = "/uninstall /quiet /norestart /log $env:TEMP\ToolUninstall.txt"
    }

    process{
        if ($ServicingType -eq "Uninstall") {
            
            #is it installed already?
            $uiHash.StatusText = "Looking for existing {0} so we can remove it..." -f $variableHash.toolName
            $installedResult = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object DisplayName -eq $variableHash.toolName

            if (!$installedResult) {

                $uiHash.StatusText = "{0} was not found on this system." -f $variableHash.toolName
                Start-Sleep -Seconds 1
                return

            } else {
                #yes, remove it first
                $uiHash.StatusText = "Attempting to remove existing {0}..." -f $variableHash.toolName
                Start-Sleep -Seconds 1

                try {

                    $uninstallResult = Start-Process -FilePath $installedResult.BundleCachePath -ArgumentList $toolUninstallArgs -NoNewWindow -PassThru
                    
                    do {

                        $uiHash.StatusText = "Waiting for the tool uninstall to complete..."
                        Start-Sleep -Seconds 1

                    } while ($uninstallResult.HasExited -eq $false)

                } catch {

                    throw

                }

                $installedResult = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object DisplayName -eq $variableHash.toolName

                if (!$installedResult) {
                    
                    [xml]$xmlFile = Get-Content -Path $variableHash.networkAssessmentToolConfig
                    ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq NumIterations).Value = "1"
                    ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq "Delimiter").Value = ","
                    $xmlFile.Save($variableHash.networkAssessmentToolConfig)

                    $uiHash.StatusText = "Successfully uninstalled {0}." -f $variableHash.toolName
                    Start-Sleep -Seconds 1

                } else {

                    $uiHash.StatusText = "There was an error during the removal."
                    $uiHash.ActionButtonFill = "Red"
                    $uiHash.ActionButtonText = "Error"
                    Start-Sleep -Seconds 3
                    throw

                }
            }
        }
        
        if ($ServicingType -eq "Install"){
            try{
                #look for source files first!
                $uiHash.ProgressValue = 30
                Get-Item $toolFileName | Out-Null
                $uiHash.StatusText = "Attempting to install the tool..."
                $installProcess = Start-Process -FilePath $toolFileName -ArgumentList $toolInstallArgs -NoNewWindow -PassThru

                do {
                    $uiHash.ProgressValue = 65
                    $uiHash.StatusText = "Waiting for the tool installation to complete..."
                    Start-Sleep -Seconds 1
                } while ($installProcess.HasExited -eq $false)

            }catch{
                #missing source file .exe
                $uiHash.StatusText = "Could not find installation file!"
                $uiHash.ProgressValue = 0
                $uiHash.ActionButtonFill = "Red"
                $uiHash.ActionButtonText = "Error"
                throw;
            }

            #need to verify it has been installed
            $uiHash.ProgressValue = 80
            $uiHash.StatusText = "Verifying installation..."
            Start-Sleep -Seconds 1
            $installedResult = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object DisplayName -eq $variableHash.toolName

            if ($installedResult) {
                $uiHash.ProgressValue = 100
                $uiHash.StatusText = "Successfully installed the tool!"
                Start-Sleep -Seconds 1 
                $uiHash.ToolImageSource = $uiHash.checkMark
            } else {
                $uiHash.StatusText = "Could not verify tool installation!"
                $uiHash.ActionButtonFill = "Red"
                $uiHash.ActionButtonText = "Error"
            }

            $uiHash.ProgressValue = 0
        }
    }

}

function Invoke-M365NetworkTestingToolAutoUpdate {
    [cmdletbinding()]
    param(
        [switch]$CheckForUpdates,
        [switch]$Update
    )

    $codeUpdateWindow = {
        
        $uiHash.Hosts.("RspUpdateWindow") = $Host
        Add-Type -AssemblyName PresentationFramework
        
        $uiContent = Get-Content -Path ($variableHash.rootPath + "\Update.xaml")
        
        [xml]$xAML = $uiContent -replace 'mc:Ignorable="d"','' -replace "x:N",'N'  -replace '^<Win.*', '<Window'
        $xmlReader = (New-Object System.Xml.XmlNodeReader $xAML)
        $uiHash.UpdateWindow = [Windows.Markup.XamlReader]::Load($xmlReader)

        $xAML.SelectNodes("//*[@Name]") | ForEach-Object {
            $uiHash.Add($_.Name, $uiHash.UpdateWindow.FindName($_.Name))
        }

        $uiHash.UpdateWindow.Add_SourceInitialized(
            {
                $assets = $variableHash.RootPath + "\assets\"
                $uiHash.imgDL.Source = $variableHash.RootPath + "\assets\download.png"
                $uiHash.UpdateWindow.Icon = $assets + "msft_logo.png"
                $uiHash.txtUpdateInstructions.Text = ""

                if ($uiHash.AdminMode) {

                    $uiHash.UpdateInstructionsText = "Click the download icon above to upgrade to version {0}." -f $variableHash.psgVersion

                    $codeUpdateRefresh = {

                        $uiHash.txtUpdateInstructions.Text = $uiHash.UpdateInstructionsText
    
                    }
    
                    #Create timer to handle updating the UI
                    $timer = new-object System.Windows.Threading.DispatcherTimer
                    $timer.Interval = [TimeSpan]"0:0:0:0.30"
                    $timer.Add_Tick($codeUpdateRefresh)
                    $timer.Start()

                } else {

                    $uiHash.imgDL.Cursor = $null
                    $uiHash.btnCloseUpdate.Visibility = "Visible"
                    $uiHash.txtUpdateInstructions.Text = "Please launch this application as an Administrator to update"

                }

            }
        )

        $uiHash.btnCloseUpdate.Add_Click(
            {

                $uiHash.UpdateWindow.Close()

            }
        )

        $uiHash.imgDL.Add_MouseUp(
            {
                
                if ($uiHash.AdminMode) {

                    $codeUpgradeModule = {

                        $uiHash.btnCloseUpdate.IsEnabled = $false
                        $uiHash.UpdateInstructionsText = "Updating the module..."
                    
                        try {
                
                            Update-Module $variableHash.ThisModule.Name -Force -Confirm:$false
                            Invoke-M365NetworkTestingToolCleanup
                
                        } catch {
                
                            $uiHash.UpdateInstructionsText = "Failure executing Update-Module"
                            break;
                
                        }
                        
                        $uiHash.UpdateInstructionsText = "Validating installation..."
                        $availableModules = Get-Module $variableHash.ThisModule.Name -ListAvailable
    
                        if (($availableModules | Sort-Object Version -Descending)[0].Version -eq $variableHash.psgVersion) {
    
                            Invoke-M365CreateShortcuts
                            $uiHash.UpdateInstructionsText = "Update successful! Please close and re-launch the application."
                            $uiHash.btnCloseUpdate.IsEnabled = $true
    
                            do {
                                Start-Sleep -Seconds 10
                            } until ($infinity)
    
                        } else {
    
                            $uiHash.UpdateInstructionsText = "Update unsuccessful."
                            $uiHash.btnCloseUpdate.IsEnabled = $true
    
                        }

                    }

                    Invoke-NewRunspace -codeBlock $codeUpgradeModule -RunspaceHandleName UpgradeModule

                }

            }
        )

        $uiHash.UpdateWindow.ShowDialog() | Out-Null
        $uiHash.UpdateWindow.Error = $Error

    }

    $uiHash.CheckUpdateEnabled = $false

    if ($CheckForUpdates) {

        $uiHash.SplashStatusText = "Please wait while we check for updates online..."
        [System.Version]$variableHash.psgVersion = (Find-Module $variableHash.ThisModule.Name).Version #time consuming!
        $uiHash.CurrentVersionText = "Current version: {0}" -f $variableHash.ThisModule.Version
        $uiHash.PSGVersionText = "Online version: {0}" -f $variableHash.psgVersion

        if ($variableHash.psgVersion -gt [System.Version]$variableHash.ThisModule.Version) {
            
            Invoke-NewRunspace -codeBlock $codeUpdateWindow -RunspaceHandleName UpdateWindow

            do {

                Start-Sleep -Seconds 1

            } until ($uiHash.Handles.UpdateWindow.IsCompleted)

        } else {

            $uiHash.SplashStatusText = "Up to date"
            Start-Sleep -Seconds 1

        }

    }

    $uiHash.CheckUpdateEnabled = $true

}

function Invoke-M365NetworkTestingToolCleanup {

    #remove older versions N-2
    
    $uiHash.UpdateInstructionsText = "Performing cleanup..."
    Start-Sleep -Seconds 1

    for ($i = 1; $i -le $variableHash.allModules.Count-1; $i++) {

        $uiHash.UpdateInstructionsText = "Removing old module {0}" -f $variableHash.allModules[$i].Version.ToString()
        Start-Sleep -Seconds 1
        
        try {
            
            Uninstall-Module $variableHash.allModules[$i].Name -RequiredVersion $variableHash.allModules[$i].Version -Confirm:$false -Force -ErrorVariable UpdateError
            
        } catch {

            $uiHash.UpdateInstructionsText = "Unable to remove version {0}" -f $variableHash.allModules[$i].Version.ToString()
            Start-Sleep -Seconds 1

        }

        if ($UpdateError) {

            $uiHash.UpdateInstructionsText = "Unable to remove version {0}" -f $variableHash.allModules[$i].Version.ToString()
            Start-Sleep -Seconds 1

        }

    }

}

function Invoke-CalculateEstimatedTime {

    if ($uiHash.NumAudioTests -eq 1) {

        $timeCalc = [math]::Round((40 / 60),1)

    } else {

        $timeCalc = [math]::Round($(([int]$uiHash.NumAudioTests * (40 + [int]$uiHash.AudioTestDelay)) / 60),1) # 40s/test

    }

    $uiHash.txtEstimatedTimeToRun.Text = "Estimated time to complete tests (minutes): " + $timeCalc
    
}

function Get-M365RegistryEntries {

    $m365RegPath = "HKEY_CURRENT_USER\Software\Microsoft\M365NetworkTestingTool"
    
    $tempKey = [Microsoft.Win32.Registry]::GetValue($m365RegPath,"AutoUpdate","")

    if ($tempKey -eq "False" -or [string]::IsNullOrEmpty($tempKey)) {
        $tempKey = $false
    } else {
        $tempKey = $true
    }
    
    $m365RegKeys = [PSCustomObject]@{
        AutoUpdate = $tempKey
        NumberOfAudioTestIterations = [Microsoft.Win32.Registry]::GetValue($m365RegPath,"NumberOfAudioTestIterations","")
        TestIntervalInSeconds = [Microsoft.Win32.Registry]::GetValue($m365RegPath,"TestIntervalInSeconds","")
        ConnectivityTimeoutInSeconds = [Microsoft.Win32.Registry]::GetValue($m365RegPath,"ConnectivityTimeoutInSeconds","")
    }

    return $m365RegKeys
}

function Set-M365RegistryEntries {

    $m365RegPath = "HKEY_CURRENT_USER\Software\Microsoft\M365NetworkTestingTool"

        try {

            [Microsoft.Win32.Registry]::SetValue($m365RegPath,"AutoUpdate",[bool]$uiHash.AutoUpdate)
            [Microsoft.Win32.Registry]::SetValue($m365RegPath,"NumberOfAudioTestIterations",$uiHash.cmbNumAudioTests.SelectedItem)
            [Microsoft.Win32.Registry]::SetValue($m365RegPath,"TestIntervalInSeconds",$uiHash.cmbAudioTestDelay.SelectedItem)
            [Microsoft.Win32.Registry]::SetValue($m365RegPath,"ConnectivityTimeoutInSeconds",$uiHash.cmbConnectivityTimeout.SelectedItem)

        } catch {

            throw; #need better handling here

        }
}

function Invoke-M365CreateShortcuts {
    
    New-Item -ItemType Directory -Path $($env:APPDATA + "\Microsoft\Windows\Start Menu\Programs\Microsoft 365 Network Connectivity Tool") -ErrorAction SilentlyContinue | Out-Null
    
    $thisModule = Get-Module M365NetworkTestingTool
    $rootPath = Split-Path -Path $thisModule.Path
    $appLocation = '%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe'
    $appArguments = '-WindowStyle Hidden -Command & {Invoke-M365NetworkTestingClient}'
    $WshShell = New-Object -ComObject WScript.Shell

    try {

        $Shortcut = $WshShell.CreateShortcut($env:USERPROFILE + "\Desktop\Microsoft 365 Connectivity Testing Tool.lnk")
        $Shortcut.TargetPath = $appLocation
        $Shortcut.Arguments = $appArguments
        $Shortcut.IconLocation = $rootPath + "\assets\msft_logo.ico"
        $Shortcut.WindowStyle = 7
        $Shortcut.Save()
        Write-Output "Successfully created Desktop shortcut."

    } catch {

        throw
        
    }

    try {

        $StartShortcut = $WshShell.CreateShortcut($env:APPDATA + "\Microsoft\Windows\Start Menu\Programs\Microsoft 365 Network Connectivity Tool\Microsoft 365 Connectivity Testing Tool.lnk")
        $StartShortcut.TargetPath = $appLocation
        $StartShortcut.Arguments = $appArguments
        $StartShortcut.IconLocation = $rootPath + "\assets\msft_logo.ico"
        $StartShortcut.WindowStyle = 7
        $StartShortcut.Save()
        Write-Output "Successfully created Start Menu shortcut."

    } catch {

        throw

    }

}
# SIG # Begin signature block
# MIIkOgYJKoZIhvcNAQcCoIIkKzCCJCcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCPzwv1Rq4ynVkb
# y9UDhK7gynILz1R6MuKDOOfIrTlf06CCDYMwggYBMIID6aADAgECAhMzAAAAxOmJ
# +HqBUOn/AAAAAADEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMTcwODExMjAyMDI0WhcNMTgwODExMjAyMDI0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCIirgkwwePmoB5FfwmYPxyiCz69KOXiJZGt6PLX4kvOjMuHpF4+nypH4IBtXrL
# GrwDykbrxZn3+wQd8oUK/yJuofJnPcUnGOUoH/UElEFj7OO6FYztE5o13jhwVG87
# 7K1FCTBJwb6PMJkMy3bJ93OVFnfRi7uUxwiFIO0eqDXxccLgdABLitLckevWeP6N
# +q1giD29uR+uYpe/xYSxkK7WryvTVPs12s1xkuYe/+xxa8t/CHZ04BBRSNTxAMhI
# TKMHNeVZDf18nMjmWuOF9daaDx+OpuSEF8HWyp8dAcf9SKcTkjOXIUgy+MIkogCy
# vlPKg24pW4HvOG6A87vsEwvrAgMBAAGjggGAMIIBfDAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUy9ZihM9gOer/Z8Jc0si7q7fDE5gw
# UgYDVR0RBEswSaRHMEUxDTALBgNVBAsTBE1PUFIxNDAyBgNVBAUTKzIzMDAxMitj
# ODA0YjVlYS00OWI0LTQyMzgtODM2Mi1kODUxZmEyMjU0ZmMwHwYDVR0jBBgwFoAU
# SG5k5VAF04KqFzc3IrVtqMp1ApUwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljQ29kU2lnUENBMjAxMV8yMDEx
# LTA3LTA4LmNybDBhBggrBgEFBQcBAQRVMFMwUQYIKwYBBQUHMAKGRWh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljQ29kU2lnUENBMjAxMV8y
# MDExLTA3LTA4LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQAG
# Fh/bV8JQyCNPolF41+34/c291cDx+RtW7VPIaUcF1cTL7OL8mVuVXxE4KMAFRRPg
# mnmIvGar27vrAlUjtz0jeEFtrvjxAFqUmYoczAmV0JocRDCppRbHukdb9Ss0i5+P
# WDfDThyvIsoQzdiCEKk18K4iyI8kpoGL3ycc5GYdiT4u/1cDTcFug6Ay67SzL1BW
# XQaxFYzIHWO3cwzj1nomDyqWRacygz6WPldJdyOJ/rEQx4rlCBVRxStaMVs5apao
# pIhrlihv8cSu6r1FF8xiToG1VBpHjpilbcBuJ8b4Jx/I7SCpC7HxzgualOJqnWmD
# oTbXbSD+hdX/w7iXNgn+PRTBmBSpwIbM74LBq1UkQxi1SIV4htD50p0/GdkUieeN
# n2gkiGg7qceATibnCCFMY/2ckxVNM7VWYE/XSrk4jv8u3bFfpENryXjPsbtrj4Ns
# h3Kq6qX7n90a1jn8ZMltPgjlfIOxrbyjunvPllakeljLEkdi0iHv/DzEMQv3Lz5k
# pTdvYFA/t0SQT6ALi75+WPbHZ4dh256YxMiMy29H4cAulO2x9rAwbexqSajplnbI
# vQjE/jv1rnM3BrJWzxnUu/WUyocc8oBqAU+2G4Fzs9NbIj86WBjfiO5nxEmnL9wl
# iz1e0Ow0RJEdvJEMdoI+78TYLaEEAo5I+e/dAs8DojCCB3owggVioAMCAQICCmEO
# kNIAAAAAAAMwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmlj
# YXRlIEF1dGhvcml0eSAyMDExMB4XDTExMDcwODIwNTkwOVoXDTI2MDcwODIxMDkw
# OVowfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UE
# AxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMTCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKvw+nIQHC6t2G6qghBNNLrytlghn0IbKmvpWlCq
# uAY4GgRJun/DDB7dN2vGEtgL8DjCmQawyDnVARQxQtOJDXlkh36UYCRsr55JnOlo
# XtLfm1OyCizDr9mpK656Ca/XllnKYBoF6WZ26DJSJhIv56sIUM+zRLdd2MQuA3Wr
# aPPLbfM6XKEW9Ea64DhkrG5kNXimoGMPLdNAk/jj3gcN1Vx5pUkp5w2+oBN3vpQ9
# 7/vjK1oQH01WKKJ6cuASOrdJXtjt7UORg9l7snuGG9k+sYxd6IlPhBryoS9Z5JA7
# La4zWMW3Pv4y07MDPbGyr5I4ftKdgCz1TlaRITUlwzluZH9TupwPrRkjhMv0ugOG
# jfdf8NBSv4yUh7zAIXQlXxgotswnKDglmDlKNs98sZKuHCOnqWbsYR9q4ShJnV+I
# 4iVd0yFLPlLEtVc/JAPw0XpbL9Uj43BdD1FGd7P4AOG8rAKCX9vAFbO9G9RVS+c5
# oQ/pI0m8GLhEfEXkwcNyeuBy5yTfv0aZxe/CHFfbg43sTUkwp6uO3+xbn6/83bBm
# 4sGXgXvt1u1L50kppxMopqd9Z4DmimJ4X7IvhNdXnFy/dygo8e1twyiPLI9AN0/B
# 4YVEicQJTMXUpUMvdJX3bvh4IFgsE11glZo+TzOE2rCIF96eTvSWsLxGoGyY0uDW
# iIwLAgMBAAGjggHtMIIB6TAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQUSG5k
# 5VAF04KqFzc3IrVtqMp1ApUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYD
# VR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUci06AjGQQ7kU
# BU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybC5taWNyb3Nv
# ZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAz
# XzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAz
# XzIyLmNydDCBnwYDVR0gBIGXMIGUMIGRBgkrBgEEAYI3LgMwgYMwPwYIKwYBBQUH
# AgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvZG9jcy9wcmltYXJ5
# Y3BzLmh0bTBABggrBgEFBQcCAjA0HjIgHQBMAGUAZwBhAGwAXwBwAG8AbABpAGMA
# eQBfAHMAdABhAHQAZQBtAGUAbgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAZ/KG
# pZjgVHkaLtPYdGcimwuWEeFjkplCln3SeQyQwWVfLiw++MNy0W2D/r4/6ArKO79H
# qaPzadtjvyI1pZddZYSQfYtGUFXYDJJ80hpLHPM8QotS0LD9a+M+By4pm+Y9G6XU
# tR13lDni6WTJRD14eiPzE32mkHSDjfTLJgJGKsKKELukqQUMm+1o+mgulaAqPypr
# WEljHwlpblqYluSD9MCP80Yr3vw70L01724lruWvJ+3Q3fMOr5kol5hNDj0L8giJ
# 1h/DMhji8MUtzluetEk5CsYKwsatruWy2dsViFFFWDgycScaf7H0J/jeLDogaZiy
# WYlobm+nt3TDQAUGpgEqKD6CPxNNZgvAs0314Y9/HG8VfUWnduVAKmWjw11SYobD
# HWM2l4bf2vP48hahmifhzaWX0O5dY0HjWwechz4GdwbRBrF1HxS+YWG18NzGGwS+
# 30HHDiju3mUv7Jf2oVyW2ADWoUa9WfOXpQlLSBCZgB/QACnFsZulP0V3HjXG0qKi
# n3p6IvpIlR+r+0cjgPWe+L9rt0uX4ut1eBrs6jeZeRhL/9azI2h15q/6/IvrC4Dq
# aTuv/DDtBEyO3991bWORPdGdVk5Pv4BXIqF4ETIheu9BCrE/+6jMpF3BoYibV3FW
# TkhFwELJm3ZbCoBIa/15n8G9bW1qyVJzEw16UM0xghYNMIIWCQIBATCBlTB+MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNy
# b3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExAhMzAAAAxOmJ+HqBUOn/AAAAAADE
# MA0GCWCGSAFlAwQCAQUAoIH/MBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwG
# CisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCDq/cDL
# eCF4GeyCO5qHIY/+AbREaI5pNGlZdO6fSWM0JzCBkgYKKwYBBAGCNwIBDDGBgzCB
# gKA4gDYATQAzADYANQBOAGUAdAB3AG8AcgBrAFQAZQBzAHQAaQBuAGcAQwBvAG0A
# cABhAG4AaQBvAG6hRIBCaHR0cHM6Ly93d3cucG93ZXJzaGVsbGdhbGxlcnkuY29t
# L3BhY2thZ2VzL00zNjVOZXR3b3JrVGVzdGluZ1Rvb2wgMA0GCSqGSIb3DQEBAQUA
# BIIBADXZXTyDPCe6mQ9jd1owkWdw5+ZQb3osUMSeoMrITkMmIVlz+RAaK/Vg00MY
# 34I+pHbib6u88p0+Hbv+BiASds/os/ECaORCag+lGIi3jB4GIjW6Z1IPiuwKqTsU
# x8wl5UDRxsOfgsTUXUHQRA247JLIuyfk+gJ+pXF0blBjeOs7M3cEVBjAM5zZpa4Z
# GFL7nT6Gu5j3cygxVo3IDPSlCMUDtWsAKyuaWO9AZwshDonr65kTYWoH64PmVgTo
# W5r5FeIOOwSvnj7oA25d28ikLiuuO7Wkson+wE9Vufuu/11sCvsZHlD44f9la7TN
# fGB1ITpKsceFeG+pxNvJ4wcoDK6hghNGMIITQgYKKwYBBAGCNwMDATGCEzIwghMu
# BgkqhkiG9w0BBwKgghMfMIITGwIBAzEPMA0GCWCGSAFlAwQCAQUAMIIBPAYLKoZI
# hvcNAQkQAQSgggErBIIBJzCCASMCAQEGCisGAQQBhFkKAwEwMTANBglghkgBZQME
# AgEFAAQgC2ztfQSF0KU7Qsq8ZjTpNjyauFDitwoTq+10+DvC7TECBlsDKQ0UxxgT
# MjAxODA1MjIyMDA2MjguNTA0WjAHAgEBgAID56CBuKSBtTCBsjELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEMMAoGA1UECxMDQU9DMScwJQYDVQQL
# Ex5uQ2lwaGVyIERTRSBFU046ODQzRC0zN0Y2LUYxMDQxJTAjBgNVBAMTHE1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wggg7KMIIGcTCCBFmgAwIBAgIKYQmBKgAA
# AAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUg
# QXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAxMjE0NjU1WjB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZIhvcNAQEBBQAD
# ggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoAgoX77XxoSyxf
# xcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiEVEMM1024OAiz
# Qt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+BVLHPk0ySwcSm
# XdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3wV3WsvYpCTUBR
# 0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXoeByw6ZnNPOcv
# RLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYwggHiMBAGCSsG
# AQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNoWoVtVTAZBgkr
# BgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw
# AwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBN
# MEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0
# cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoG
# CCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01p
# Y1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGVMIGSMIGPBgkr
# BgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABl
# AGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJ
# KoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM9TG+zwXiqf76
# V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0YBKKdsxAQEGb
# 3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgPF/UveYFl2am1
# a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/625Y4zu2JfmttX
# QOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZqkHimbdLhnPkd
# /DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96LjlXdqJxqgaK
# D4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5vvfHhAN/nMQek
# kzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiFAR6A+xuJKlQ5
# slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduWsqdCosnPGUFN
# 4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV42neV8HR3jDA
# /czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto229Nfj950iEkS
# MIIE2TCCA8GgAwIBAgITMwAAAKlUcNl5wIRl4gAAAAAAqTANBgkqhkiG9w0BAQsF
# ADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0xNjA5MDcxNzU2NTNa
# Fw0xODA5MDcxNzU2NTNaMIGyMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMQwwCgYDVQQLEwNBT0MxJzAlBgNVBAsTHm5DaXBoZXIgRFNFIEVTTjo4
# NDNELTM3RjYtRjEwNDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKyUhRim5VbTnd8D
# Zy7bmJFHj5WJck2adPM0QN5rEafUx5J+Dv+fNTp/p2SMcZthxRlZPpRdkS9OwO3B
# QG4nB5HU5az2HjzOUBFTJ2iE0tJmpB9uHXspiybjCa5050z8n4O3Rg3bFakN+1f3
# s5sQ3gwcpE0aw5YkRjYpkFzeHUfWlz9CjQf/qC8njusSELFHQvcEvTk2xMW7wOy2
# vI6BnMndskA8PaPrG70wdf5bBXMdFDER1PA6v+zRE95EycT+SOFXxC54CBV+80UG
# Nkm2tf1YHRHdiGQDQhA5765TFkYRW1x9Elp3SHZwKWEhGpNTnCJpbpy0eXqOTDiE
# FRlsX6ECAwEAAaOCARswggEXMB0GA1UdDgQWBBQP1UH7u/Od9L5JUtqMN1fOZA/z
# fTAfBgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEug
# SaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9N
# aWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsG
# AQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Rp
# bVN0YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQAImRilB4svciR1LI4ghAL9CqGv
# DuGwMhGGqsWGIJ276EQ7lcy1OueLRqUT04wgG6g6VxwV93NtjQuyB0VuhkV2JEa6
# dEzxNm9Snbbk5JSMySmriaq+coLTuptVq6CCOeAZLtgRrUWJA0vAL+IAnrh7h9vK
# 13W4LgS0jJM3ih4mROTZQWWO0UK5XhMQvBlndbvOhKnpH457zxuUuDldbVdZ7Ap+
# xtaVDTvn+ogvVqxVBFByZz+yAAPDjsL8PsiVq+DUZ7amNG20YAiyQonef2DaWCG8
# 04w4MlPyCszb3yS3AOuTSj8JejQ7S8Bza2pShFOhmwREWWFCrXLwQMBu/JqyoYID
# dDCCAlwCAQEwgeKhgbikgbUwgbIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xDDAKBgNVBAsTA0FPQzEnMCUGA1UECxMebkNpcGhlciBEU0UgRVNO
# Ojg0M0QtMzdGNi1GMTA0MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloiUKAQEwCQYFKw4DAhoFAAMVAF06v1a94Q8pynIOZPd1O3gaIuv0oIHB
# MIG+pIG7MIG4MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMQww
# CgYDVQQLEwNBT0MxJzAlBgNVBAsTHm5DaXBoZXIgTlRTIEVTTjoyNjY1LTRDM0Yt
# QzVERTErMCkGA1UEAxMiTWljcm9zb2Z0IFRpbWUgU291cmNlIE1hc3RlciBDbG9j
# azANBgkqhkiG9w0BAQUFAAIFAN6uX+wwIhgPMjAxODA1MjIwOTIyNTJaGA8yMDE4
# MDUyMzA5MjI1MlowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA3q5f7AIBADAHAgEA
# AgJ3fDAHAgEAAgIYVzAKAgUA3q+xbAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgor
# BgEEAYRZCgMBoAowCAIBAAIDHoCYoQowCAIBAAIDHoSAMA0GCSqGSIb3DQEBBQUA
# A4IBAQAQiInT97AIuroYAIZt8CmOjjrkTjTH/JhYAJomYERBHeHPfZD9Q67xlE8x
# +MZHSAp/Ij3Gpc3XsTRNfDfVtXMjNgDJDwfRjQAn4mn75msxLkbTKLNsClYSz01J
# EMpyJ3pe0hC0zBdJRUKuxIyxpzx8eMaVGxrpnNzaR8cjkBDXGFKq395g1EiNZuA+
# aieXVULt2OH+1pOMnEw6fFPPGGqv08FddHuarHdbISp/swXQTkuzkN7SMgC+Ujtz
# wMCszWzLsmo/kiNfMgYXwLAE4/FbRqGV0/hM5Ilqsv4VFwTvA9qebNuo3zwEPqdR
# iqbhk4VqKnFfdjURMVd1NqQ+Won+MYIC9TCCAvECAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAACpVHDZecCEZeIAAAAAAKkwDQYJYIZIAWUD
# BAIBBQCgggEyMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgaRa8ONnOdBBxxh+WfW7HT4whSsajK9VugnsT4TNcTfswgeIGCyqGSIb3
# DQEJEAIMMYHSMIHPMIHMMIGxBBRdOr9WveEPKcpyDmT3dTt4GiLr9DCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAAqVRw2XnAhGXiAAAA
# AACpMBYEFNzFnhvstQ1AoU33jYkLRhFADr06MA0GCSqGSIb3DQEBCwUABIIBAKX3
# LGiNQJxy5QHHaVSP49aM/gG4/nwAbstPlNNXnQgktTzA1V4c+wEAxqztNRMk6zv6
# G/CAuy/FkCiNDcMlRD6CBiomrHKKFKO7wGKzSqvsdhei7vBZWas2hUtt6r2fAhaK
# Wex5cISwzriHEui+nf9KaHCO7bHogS96Twt9yvGCuaq7UsqcC4xoq8U4VBaNdo+m
# Lx7G7AJHphi4lRpNQXUB7rPJKdLslijCp9XSGWMSSMxMwVyfc7z2z4yPRryd4ynU
# rXZoqs6M2bzAMMBp9sdix9tTxnFo736OE/qIYzeyGB1EO4Vaxa6dD0Iq06IpPXO7
# YpBWztwN1XY3B3Nme+c=
# SIG # End signature block