DeliveryOptimizationTroubleshooter.ps1
<#PSScriptInfo .VERSION 1.0.0 .GUID 163529f1-bbbd-4228-8e59-445c2be3d870 .AUTHOR carmenf .COMPANYNAME Microsoft .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES The Delivery Optimization Troubleshooter Tool can be used to verify device settings are properly configured to use Delivery Optimization. #> <# .DESCRIPTION Delivery Optimization Troubleshooter Tool #> [CmdLetBinding()] Param() #----------------------------------------------------------------------------------# # Print Functions Add-Type -TypeDefinition @" public enum TestResult { Unset, Fail, Pass, Disabled, Warn, } "@ function Print-Result([string] $outputName, [string] $outputMsg, [TestResult] $result) { $outputName = AddSpace($outputName) Write-Host $outputName -NoNewline switch ($result) { "Fail" { Write-Host -ForegroundColor Red "FAIL " -NoNewline } "Pass" { Write-Host -ForegroundColor Green "PASS " -NoNewline } "Warn" { Write-Host -ForegroundColor Yellow "WARN " -NoNewline } "Disabled" { Write-Host -ForegroundColor Yellow "DISABLED " -NoNewline } default { Write-Host -ForegroundColor Red "ERROR " -NoNewline } } Write-Host $outputMsg } function Print-Header() { $outputName = AddSpace("Test") Write-Host "" Write-Host $outputName -NoNewline Write-Host "Result " -NoNewline Write-Host "Details" $outputName = AddSpace("----") Write-Host $outputName -NoNewline Write-Host "------ " -NoNewline Write-Host "-------" } function AddSpace([string] $text) { return $text + ' ' * (30 - $text.Length) } #----------------------------------------------------------------------------------# # Device Check function Check-AdminPrivileges([string] $invocationLine) { if (IsElevated) { return $true; } $ScriptPath = $MyInvocation.PSCommandPath # The new process can't resolve working dir when script is launched like .\dolog.ps1, so we have to parse # and rebuild the full script path and param list. $scriptParams = "" $firstParam = $invocationLine.IndexOf('-') if($firstParam -gt 0) { $scriptParams = $invocationLine.Substring($firstParam-1) } $scriptCmd = "$ScriptPath $scriptParams" $arg = "-NoExit -Command `"$scriptCmd`"" #Check Powershell version to use the right path if ($PSVersionTable.PSVersion.Major -lt 7) { $PSPath = "powershell.exe" } else { $PSPath = "pwsh.exe" } $proc = Start-Process $PSPath -ArgumentList $arg -Verb Runas -ErrorAction Stop return $false } function IsElevated { $wid = [System.Security.Principal.WindowsIdentity]::GetCurrent() $prp = new-object System.Security.Principal.WindowsPrincipal($wid) $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator $isElevated = $prp.IsInRole($adm) return $isElevated } function Check-NetInterface() { $outputName = "Network Interface" $result = [TestResult]::Unset $msg = " " try { $query = "SELECT * FROM Win32_NetworkAdapter WHERE NOT PNPDeviceID LIKE 'ROOT\\%'" $interfaces = Get-WmiObject -Query $query | Sort index $networkInterface = @() #Save in a string all the interfaces found foreach ($interface in $interfaces) { $name = $interface.NetConnectionID $description = $interface.Name if ($name) { $networkInterface += "($name) $description " } } if ($networkInterface) { $msg = $networkInterface -join " - " $result = [TestResult]::Pass } else { $msg = "No network" $result = [TestResult]::Fail } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } } function Check-CacheFolder() { $outputName = "Cache Folder Access" $result = [TestResult]::Unset $msg = "" try { $dosvcWorkingDir = $doConfig.WorkingDirectory if (!(Test-Path $dosvcWorkingDir)) { throw "Cache folder not found: $dosvcWorkingDir" } $acl = Get-Acl $dosvcWorkingDir $IdentityReferenceDO = "NT SERVICE\DoSvc" $IdentityReferenceNS = "NT AUTHORITY\NETWORK SERVICE" $inheritanceFlags = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor ([System.Security.AccessControl.InheritanceFlags]::ObjectInherit) # Filter to DO/NS permissions $permissionEntries = $acl.Access | where { @($IdentityReferenceDO, $IdentityReferenceNS) -contains $_.IdentityReference.Value } # This might be interesting here: Write-Verbose $permissionEntries # Look for Allow/FullControl/Full inheritance $permissionEntries = $permissionEntries | where { ($_.AccessControlType -eq "Allow") -and ($_.FileSystemRights -eq "FullControl") -and ($_.InheritanceFlags -eq $inheritanceFlags) } if ($permissionEntries) { $result = [TestResult]::Pass } else { $msg = "Required permissions missing" $result = [TestResult]::Fail } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error "Cache folder permission check failed:" $_.Exception } } function Check-Service([string] $serviceName) { $outputName = "Service Status" $result = [TestResult]::Unset $msg = "" try { $service = Get-Service -Name $serviceName if ($service -and ($service.StartType -ne "Disabled")) { if ($service.Status -eq "Running") { $msg = "$serviceName running" $result = [TestResult]::Pass } else { $msg = "$serviceName stopped" $result = [TestResult]::Warn } } else { $msg = "$serviceName disabled" $result = [TestResult]::Fail } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } } function Check-KeyAccess() { $outputName = "Registry Key Access" $result = [TestResult]::Unset $msg = "" try { Remove-PSDrive HKU -ErrorAction SilentlyContinue $drive = New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS $testPath = Test-Path -Path HKU:\S-1-5-20\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization if (!$testPath) { throw "Registry Key not found" } # TODO: Check permissions on key # $doConfig.WorkingDirectory is the cache path, which may be redirected elsewhere. The state directory doesn't follow that redirection. $path = "$env:windir\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization\State\dosvcState.dat" $testPath = Test-Path -Path $path -PathType Leaf if (!$testPath) { $msg = "Registry file not found" $result = [TestResult]::Fail } else { # TODO: Check permissions on file $result = [TestResult]::Pass } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } finally { Remove-PSDrive HKU -ErrorAction SilentlyContinue } } function Check-RAMRequired() { $outputName = "RAM" $result = [TestResult]::Unset $msg = "" try { $totalRAM = (Get-WmiObject Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object -ExpandProperty Sum)/1GB if ($totalRAM -ge $doConfig.MinTotalRAM) { $msg = "$totalRAM GB" $result = [TestResult]::Pass } else { $msg = "Local RAM: $ramTotal GB | RAM Requirements: $minTotalRAM GB." $result = [TestResult]::Fail } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } } function Check-DiskRequired() { $outputName = "Disk" $result = [TestResult]::Unset $msg = "" try { $diskSize = Get-WmiObject -Class win32_logicaldisk | Where-Object DeviceId -eq $env:SystemDrive | Select-Object @{N='Disk'; E={$_.DeviceId}}, @{N='Size'; E={[math]::Round($_.Size/1GB,2)}}, @{N='FreeSpace'; E={[math]::Round($_.FreeSpace/1GB,2)}} if ($diskSize.FreeSpace -ge $doConfig.MinTotalDiskSize) { $msg = $diskSize.Disk + " | Total Size: " + $diskSize.Size + "GB | Free Space: " + $diskSize.FreeSpace + "GB" $result = [TestResult]::Pass } else { $msg = "Free Space Requirements: $minDiskSize GB. | Local Free Space: $diskSize.FreeSpace GB" $result = [TestResult]::Fail } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } } function Check-Vpn() { $outputName = "VPN" $result = [TestResult]::Unset $msg = "" try { $vpn = Get-VpnConnection if (!$vpn) { $result = [TestResult]::Pass } else { $activeVPN = $vpn | Where-Object ConnectionStatus -eq "Connected" | Select-Object -ExpandProperty Name if ($activeVPN) { $msg = "Connected: $activeVPN" $result = [TestResult]::Warn } else { $AllVPN = (($vpn | Select-Object -ExpandProperty Name) -join " - ") $msg = "Not connected: $AllVPN" $result = [TestResult]::Pass } } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } } function Check-PowerBattery() { $outputName = "Power" $result = [TestResult]::Unset $msg = "" try { $battery = Get-WmiObject -Class win32_battery #PC: if (!$battery) { $plan = Get-WmiObject -Class win32_powerplan -Namespace "root\cimv2\power" | Where-Object IsActive -eq true | Select-Object -ExpandProperty ElementName $msg = "A/C: $plan" $result = [TestResult]::Pass } #Notebook: else { $batteryPercentage = $battery.EstimatedChargeRemaining $batteryStatus = Get-WmiObject -Class BatteryStatus -Namespace root\wmi -ComputerName "localhost" -ErrorAction SilentlyContinue -ErrorVariable ProcessError if ($ProcessError) { $result = [TestResult]::Fail $msg = "WMI Error ( Check https://learn.microsoft.com/en-us/previous-versions/tn-archive/ff406382(v=msdn.10) ) | Error: " + $ProcessError.Exception } elseif ($batteryStatus.PowerOnline) { $result = [TestResult]::Pass $msg = "A/C: $batteryPercentage% (charging)" } else { $batteryLevelForSeeding = $doConfig.BatteryPctToSeed if ($batteryPercentage -ge $batteryLevelForSeeding) { $result = [TestResult]::Pass } else { $result = [TestResult]::Fail } $msg = "Battery: $batteryPercentage% ($batteryLevelForSeeding% required to upload)" } } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } } function Print-OSInfo() { # Check OS Version $os = Get-WmiObject -Class Win32_OperatingSystem Write-Host "Windows" $os.Version -NoNewline switch ($os.BuildNumber) { "10240" { Write-Host " - TH1" } "10586" { Write-Host " - TH2" } "14393" { Write-Host " - RS1" } "15063" { Write-Host " - RS2" } "16299" { Write-Host " - RS3" } "17134" { Write-Host " - RS4" } "17763" { Write-Host " - RS5" } "18362" { Write-Host " - Titanium 19H1" } "18363" { Write-Host " - Vanadium 19H2" } "19041" { Write-Host " - Vibranium 20H1" } "19042" { Write-Host " - Vibranium (v2) 20H2" } "19645" { Write-Host " - Manganese" } "19043" { Write-Host " - Vibranium (v3) 21H1" } "19044" { Write-Host " - Vibranium (v4) 21H2" } "20348" { Write-Host " - Iron" } "22000" { Write-Host " - Cobalt" } "22621" { Write-Host " - Nickel" } default { Write-Host "" } } # Check UUS Version $uusVerPath = "$env:ProgramData\Microsoft\Windows\UUS\State\_active.uusver" if (Test-Path $uusVerPath) { $uusVersion = Get-Content $uusVerPath Write-Host "UUS" $uusVersion } $PSVersion = "PS Version " + $PSVersionTable.PSVersion Write-Verbose $PSVersion } #----------------------------------------------------------------------------------# # Connection Check function Test-Port([int] $port, [switch] $optional) { $outputName = "Check Port" $oldPreference = $Global:ProgressPreference $result = [TestResult]::Unset $msg = "$port" try { $Global:ProgressPreference = 'SilentlyContinue' $resultTest = Test-NetConnection -Computer localhost -Port $port -WarningAction SilentlyContinue -InformationLevel 'Quiet' if ($resultTest) { $result = [TestResult]::Pass } else { $downloadMode = Check-DownloadMode -noOutput if (!$downloadMode -or $optional) { $result = [TestResult]::Warn } else { $result = [TestResult]::Fail } } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } finally { $Global:ProgressPreference = $oldPreference } } function Check-DownloadMode([switch] $noOutput) { $outputName = "Download Mode" $msg = $doConfig.DownloadMode $result = [TestResult]::Fail $peerModes = @("Lan", "Group", "Internet") $downloadMode = $doConfig.DownloadMode if ($peerModes -contains $downloadMode) { $result = [TestResult]::Pass } if ($noOutput) { return ($result -eq "Pass") } Print-Result -outputMsg $msg -outputName $outputName -result $result } function Test-Hostname([string] $urlHostName, [switch] $noOutput) { $outputName = "Host Connection" $msg = $urlHostName $result = [TestResult]::Unset try { $dnsHostnames = Resolve-DnsName $urlHostName | Select-Object -Unique -Property NameHost | % {[string]$_.NameHost} $dnsHostnames = $dnsHostnames | Where {!$_.Equals("")} $result = [TestResult]::Fail # Check if the list of hostnames is empty if ($dnsHostnames -eq $null) { $msg = "DNS resolution failed" } else { foreach($dnsHostname in $dnsHostnames) { $test = Test-Connection $dnsHostname -Quiet if ($test) { $result = [TestResult]::Pass break } } } if ($noOutput) { return ($result -eq "Pass") } Print-Result -outputMsg $msg -outputName $outputName -result $result } catch { Write-Error $_.Exception } } function Test-InternetInfo() { # Check Request Timeout # Check if the Request comes back with a StatusCode error # Check if the Request comes back with a StatusCode 200 (success) # Check if the WebRequest comes back with Content-Type "text/json" (DO services all return json). Captive portal falls under here. # Check if the Json misses some information. $resultInt = [TestResult]::Unset $outputNameInt = "Internet Access" $msgInt = "" $resultIp = [TestResult]::Unset $outputNameIp = "External IP" $msgIp = "Not found" $url = "https://geo.prod.do.dsp.mp.microsoft.com/geo/" try { $httpResponse = Invoke-WebRequest -Uri $url if ($httpResponse.StatusCode -eq 200) { Write-Verbose $httpResponse.RawContent if ($httpResponse.Headers["Content-Type"] -eq "text/json") { $contentJson = ConvertFrom-Json $httpResponse.Content if (($contentJson.KeyValue_EndpointFullUri.Length -gt 0) -and ($contentJson.ExternalIpAddress.Length -gt 4)) { $resultInt = [TestResult]::Pass $resultIp = [TestResult]::Pass $msgIp = $contentJson.ExternalIpAddress } else { $resultInt = [TestResult]::Fail $resultIp = [TestResult]::Fail $msgInt = "GEO response incomplete!" } } elseif ($httpResponse.Headers["Content-Type"] -eq "text/html") { $resultInt = [TestResult]::Warn $resultIp = [TestResult]::Fail $msgInt = "Possible captive portal detected!" } else { $resultInt = [TestResult]::Fail $resultIp = [TestResult]::Fail $msgInt = "Invalid GEO response!" } } else { $testHostname = Test-Hostname -urlHostName "https://www.microsoft.com/" -noOutput if ($testHostname) { $msgInt = "Internet access but unable to reach DO's GEO service. Status Code: $httpResponse.StatusCode - $httpResponse.StatusDescription" $resultInt = [TestResult]::Disabled $publicIp = (Invoke-WebRequest -uri "http://ifconfig.me/ip").Content if ($publicIp) { $msgIp = "$publicIp" $resultIp = [TestResult]::Pass } else { $resultIp = [TestResult]::Fail } } else { $resultInt = [TestResult]::Fail $resultIp = [TestResult]::Fail $msgInt = "No internet access!" } } Print-Result -outputMsg $msgInt -outputName $outputNameInt -result $resultInt Print-Result -outputMsg $msgIp -outputName $outputNameIp -result $resultIp } catch { Write-Error $_.Exception } } function Check-ByteRange() { $outputName = "HTTP Byte-Range Support" $result = [TestResult]::Unset $msg = "" try { $uri = "http://dl.delivery.mp.microsoft.com/filestreamingservice/files/52fa8751-747d-479d-8f22-e32730cc0eb1" $request = [System.Net.WebRequest]::Create($uri) # Set request $request.Method = "GET" $request.AddRange("bytes", 0, 9) $return = $request.GetResponse() $statusCode = [int]$return.StatusCode $contentRange = $return.GetResponseHeader("Content-Range") $msg = "$statusCode - " + $return.StatusCode + ", Content-Range: $contentRange" if(($statusCode -eq 206) -and ($contentRange -eq "bytes 0-9/25006511")) { $result = [TestResult]::Pass } else { $result = [TestResult]::Fail } Print-Result -outputMsg $msg -outputName $outputName -result $result Write-Verbose $return.Headers.ToString() } catch { Write-Error $_.Exception } } #----------------------------------------------------------------------------------# # Main script $admin = Check-AdminPrivileges($MyInvocation.Line) if (!$admin) { return } $doConfig = Get-DOConfig -Verbose Write-Host "" Print-OSInfo Print-Header Check-DownloadMode Check-NetInterface Test-InternetInfo Check-Service -serviceName "dosvc" # 7680 - DO port Test-Port -port 7680 # 3544 - Teredo port Test-Port -port 3544 -optional Check-ByteRange Check-CacheFolder Check-KeyAccess Check-RAMRequired Check-DiskRequired Check-Vpn Check-PowerBattery $hostNames = @("dl.delivery.mp.microsoft.com", "emdl.ws.microsoft.com", "download.windowsupdate.com") foreach($hostName in $hostNames) { Test-Hostname -urlHostName $hostName } Write-Host "" |