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 |