Last active
October 16, 2025 10:58
-
-
Save jiripolasek/7452d72d56422bd3ecfeaa1826fc2534 to your computer and use it in GitHub Desktop.
Mirrors Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo detection flow; Prints results of each step to help diagnose mismatches on a user's machine.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| Detect-DefaultBrowser.ps1 (PowerShell 5.1+ compatible) | |
| Prints step-by-step diagnostics for default browser detection (http and optional https). | |
| USAGE | |
| powershell -NoProfile -ExecutionPolicy Bypass -File .\Detect-DefaultBrowser.ps1 -Verbose | |
| .\Detect-DefaultBrowser.ps1 -IncludeHttps -TestUrl "https://example.com" | |
| .\Detect-DefaultBrowser.ps1 -TestUrl "https://example.com" -Launch | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [string]$TestUrl = $null, # e.g. "https://example.com" | |
| [switch]$Launch, # actually launch the browser with TestUrl | |
| [switch]$IncludeHttps # also inspect HTTPS in addition to HTTP | |
| ) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| function Write-Step { | |
| param([string]$Title, [string]$Value) | |
| Write-Host ("`n=== {0} ===" -f $Title) -ForegroundColor Cyan | |
| if ($null -eq $Value -or $Value -eq '') { $Value = '<null or empty>' } | |
| Write-Host $Value | |
| } | |
| function Write-Section { | |
| param([string]$Title) | |
| Write-Host "`n####################### $Title #######################" -ForegroundColor Yellow | |
| } | |
| function Dump-RegKey { | |
| param( | |
| [Parameter(Mandatory)][string]$RegPath, | |
| [string[]]$ValueNames = @($null, 'ProgId','ApplicationName','FriendlyTypeName') | |
| ) | |
| $out = [ordered]@{} | |
| foreach ($vn in $ValueNames) { | |
| $val = [Microsoft.Win32.Registry]::GetValue($RegPath, $vn, $null) | |
| $k = if ($vn) { $vn } else { '(Default)' } | |
| $out[$k] = $val | |
| } | |
| [pscustomobject]$out | |
| } | |
| # ---------- P/Invoke SHLoadIndirectString ---------- | |
| if (-not ([System.Management.Automation.PSTypeName]'ShlwapiNative').Type) { | |
| Add-Type -Language CSharp -TypeDefinition @' | |
| using System; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| public static class ShlwapiNative | |
| { | |
| [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)] | |
| public static extern int SHLoadIndirectString( | |
| string pszSource, | |
| StringBuilder pszOutBuf, | |
| uint cchOutBuf, | |
| IntPtr ppvReserved | |
| ); | |
| } | |
| '@ | |
| } | |
| function Resolve-IndirectString { | |
| param([Parameter(Mandatory)][string]$String) | |
| if (-not $String.StartsWith('@')) { return $String } | |
| $sb = New-Object System.Text.StringBuilder 512 | |
| $hr = [ShlwapiNative]::SHLoadIndirectString($String, $sb, 512, [IntPtr]::Zero) | |
| if ($hr -eq 0) { return $sb.ToString() } | |
| throw ("SHLoadIndirectString failed (HRESULT=0x{0:X8}) for '{1}'" -f $hr, $String) | |
| } | |
| function Normalize-AppName { | |
| param([string]$Name) | |
| if (-not $Name) { return $null } | |
| $n = $Name -replace '(?i)\bURL\b','' -replace '(?i)\bHTML\b','' -replace '(?i)\bDocument\b','' -replace '(?i)\bWeb\b','' | |
| $n.TrimEnd() | |
| } | |
| function Parse-CommandPattern { | |
| param([string]$Command) | |
| if ([string]::IsNullOrWhiteSpace($Command)) { return $null } | |
| # Firefox WindowsApps quoting hack (Store build sometimes lacks quotes) | |
| if ($Command -match 'firefox\.exe' -and $Command -match '\\WindowsApps\\' -and -not $Command.TrimStart().StartsWith('"')) { | |
| $idx = $Command.IndexOf('firefox.exe', [StringComparison]::Ordinal) | |
| if ($idx -ge 0) { | |
| $pathEnd = $idx + 'firefox.exe'.Length | |
| $Command = '"' + $Command.Insert($pathEnd, '"') | |
| } | |
| } | |
| $trim = $Command.Trim() | |
| $exe = $null; $args = '' | |
| if ($trim.StartsWith('"')) { | |
| $end = $trim.IndexOf('"',1) | |
| if ($end -gt 0) { | |
| $exe = $trim.Substring(1, $end-1) | |
| $args = $trim.Substring($end+1).Trim() | |
| } else { | |
| $exe = $trim.Trim('"') | |
| } | |
| } else { | |
| $space = $trim.IndexOf(' ') | |
| if ($space -gt 0) { | |
| $exe = $trim.Substring(0,$space) | |
| $args = $trim.Substring($space+1).Trim() | |
| } else { | |
| $exe = $trim | |
| } | |
| } | |
| [pscustomobject]@{ Command=$Command; Path=$exe; Arguments=$args } | |
| } | |
| function Validate-PathOrUri { | |
| param([string]$PathOrUri) | |
| $isFile = $false; $isUri = $false; $uriRef = $null | |
| if ($PathOrUri) { $isFile = Test-Path -LiteralPath $PathOrUri -PathType Leaf } | |
| if (-not $isFile -and $PathOrUri) { $isUri = [Uri]::TryCreate($PathOrUri, [UriKind]::Absolute, [ref]$uriRef) } | |
| $nearest = $null | |
| if (-not $isFile -and $PathOrUri) { | |
| try { | |
| $dir = [System.IO.Path]::GetDirectoryName($PathOrUri) | |
| while ($dir -and -not (Test-Path -LiteralPath $dir -PathType Container)) { | |
| $dir = [System.IO.Path]::GetDirectoryName($dir) | |
| } | |
| $nearest = $dir | |
| } catch {} | |
| } | |
| [pscustomobject]@{ ExistsAsFile=$isFile; IsAbsoluteUri=$isUri; NearestExistingFolder=$nearest } | |
| } | |
| function Get-FileDiagnostics { | |
| param([string]$FilePath) | |
| if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { return $null } | |
| $ver = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($FilePath) | |
| $sig = $null | |
| try { $sig = Get-AuthenticodeSignature -LiteralPath $FilePath } catch {} | |
| $sigObj = $null | |
| if ($sig) { | |
| $sigObj = [pscustomobject]@{ | |
| Status = $sig.Status | |
| StatusMessage = $sig.StatusMessage | |
| SignerSubject = $sig.SignerCertificate.Subject | |
| Issuer = $sig.SignerCertificate.Issuer | |
| NotBefore = $sig.SignerCertificate.NotBefore | |
| NotAfter = $sig.SignerCertificate.NotAfter | |
| } | |
| } | |
| [pscustomobject]@{ | |
| VersionInfo = [pscustomobject]@{ | |
| FileVersion = $ver.FileVersion | |
| ProductVersion = $ver.ProductVersion | |
| ProductName = $ver.ProductName | |
| FileDescription = $ver.FileDescription | |
| CompanyName = $ver.CompanyName | |
| OriginalFilename = $ver.OriginalFilename | |
| } | |
| Signature = $sigObj | |
| } | |
| } | |
| # ---------- Edge fallback constants ---------- | |
| $MSEdgePath = Join-Path ${env:ProgramFiles(x86)} 'Microsoft\Edge\Application\msedge.exe' | |
| $MSEdgeArgumentsPattern = '--single-argument %1' | |
| $MSEdgeName = 'Microsoft Edge' | |
| # ---------- Main ---------- | |
| $protocols = @('http'); if ($IncludeHttps) { $protocols += 'https' } | |
| $aggregate = @() | |
| foreach ($proto in $protocols) { | |
| Write-Section $proto | |
| $r = [ordered]@{ | |
| Protocol = $proto | |
| ProgIdSource = $null | |
| ProgId = $null | |
| UserChoiceLatestDump = $null | |
| UserChoiceDump = $null | |
| HKCR_ApplicationNameRaw = $null | |
| HKCR_ApplicationNameResolved = $null | |
| HKCR_ApplicationNameClean = $null | |
| Command_HKCR = $null | |
| Command_HKCU_Classes = $null | |
| Command_HKLM_Classes = $null | |
| Command_HKLM_Wow6432_Classes = $null | |
| CommandChosenSource = $null | |
| CommandPatternFinal = $null | |
| ExtractedPath = $null | |
| ExtractedArguments = $null | |
| PathValidation = $null | |
| FileDiagnostics = $null | |
| FinalBrowserPath = $null | |
| FinalBrowserName = $null | |
| FinalArgumentsPattern = $null | |
| ExceptionMessage = $null | |
| FallbackUsed = $false | |
| } | |
| try { | |
| # 1) ProgId (UserChoiceLatest -> UserChoice) | |
| $ucLatest = "HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\${proto}\UserChoiceLatest" | |
| $ucLegacy = "HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\${proto}\UserChoice" | |
| $r.UserChoiceLatestDump = Dump-RegKey -RegPath $ucLatest | |
| $r.UserChoiceDump = Dump-RegKey -RegPath $ucLegacy | |
| $progId = [Microsoft.Win32.Registry]::GetValue($ucLatest,'ProgId',$null) | |
| $r.ProgIdSource = 'UserChoiceLatest' | |
| if (-not $progId) { | |
| $progId = [Microsoft.Win32.Registry]::GetValue($ucLegacy,'ProgId',$null) | |
| $r.ProgIdSource = 'UserChoice' | |
| } | |
| $r.ProgId = $progId | |
| Write-Step ("{0} ProgId source" -f $proto) $r.ProgIdSource | |
| Write-Step ("{0} ProgId" -f $proto) $r.ProgId | |
| if (-not $progId) { throw ("ProgId not found for {0}." -f $proto) } | |
| # 2) Application Name (HKCR\<ProgId>\Application\ApplicationName OR HKCR\<ProgId>\(Default)) | |
| $rawAppName = [Microsoft.Win32.Registry]::GetValue("HKEY_CLASSES_ROOT\${progId}\Application",'ApplicationName',$null) | |
| if (-not $rawAppName) { $rawAppName = [Microsoft.Win32.Registry]::GetValue("HKEY_CLASSES_ROOT\${progId}",$null,$null) } | |
| $r.HKCR_ApplicationNameRaw = $rawAppName | |
| Write-Step ("{0} Raw application name" -f $proto) $r.HKCR_ApplicationNameRaw | |
| $resolvedName = $rawAppName | |
| if ($resolvedName -and $resolvedName.StartsWith('@')) { $resolvedName = Resolve-IndirectString $resolvedName } | |
| $r.HKCR_ApplicationNameResolved = $resolvedName | |
| $r.HKCR_ApplicationNameClean = Normalize-AppName $resolvedName | |
| Write-Step ("{0} Application name after @-resolve" -f $proto) $r.HKCR_ApplicationNameResolved | |
| Write-Step ("{0} Cleaned application name" -f $proto) $r.HKCR_ApplicationNameClean | |
| # 3) Commands from multiple hives | |
| $cmdHKCR = [Microsoft.Win32.Registry]::GetValue("HKEY_CLASSES_ROOT\${progId}\shell\open\command",$null,$null) | |
| $cmdHKCU_Classes = [Microsoft.Win32.Registry]::GetValue("HKEY_CURRENT_USER\Software\Classes\${progId}\shell\open\command",$null,$null) | |
| $cmdHKLM_Classes = [Microsoft.Win32.Registry]::GetValue("HKEY_LOCAL_MACHINE\Software\Classes\${progId}\shell\open\command",$null,$null) | |
| $cmdHKLM_W6432 = [Microsoft.Win32.Registry]::GetValue("HKEY_LOCAL_MACHINE\Software\WOW6432Node\Classes\${progId}\shell\open\command",$null,$null) | |
| $r.Command_HKCR = $cmdHKCR | |
| $r.Command_HKCU_Classes = $cmdHKCU_Classes | |
| $r.Command_HKLM_Classes = $cmdHKLM_Classes | |
| $r.Command_HKLM_Wow6432_Classes = $cmdHKLM_W6432 | |
| Write-Step ("{0} Command (HKCR)" -f $proto) $cmdHKCR | |
| Write-Step ("{0} Command (HKCU\Software\Classes)" -f $proto) $cmdHKCU_Classes | |
| Write-Step ("{0} Command (HKLM\Software\Classes)" -f $proto) $cmdHKLM_Classes | |
| Write-Step ("{0} Command (HKLM\WOW6432\Classes)" -f $proto) $cmdHKLM_W6432 | |
| # Choose command (prefer HKCR, then HKCU\Classes, then HKLM\Classes, then HKLM\WOW6432) | |
| $chosen = $cmdHKCR | |
| $src = 'HKCR' | |
| if ([string]::IsNullOrWhiteSpace($chosen) -and $cmdHKCU_Classes) { $chosen = $cmdHKCU_Classes; $src='HKCU\Software\Classes' } | |
| if ([string]::IsNullOrWhiteSpace($chosen) -and $cmdHKLM_Classes) { $chosen = $cmdHKLM_Classes; $src='HKLM\Software\Classes' } | |
| if ([string]::IsNullOrWhiteSpace($chosen) -and $cmdHKLM_W6432) { $chosen = $cmdHKLM_W6432; $src='HKLM\WOW6432\Classes' } | |
| if ([string]::IsNullOrWhiteSpace($chosen)) { throw ("No command found under any hive for {0} ({1})." -f $progId, $proto) } | |
| if ($chosen.StartsWith('@')) { $chosen = Resolve-IndirectString $chosen } | |
| $parsed = Parse-CommandPattern -Command $chosen | |
| $r.CommandChosenSource = $src | |
| $r.CommandPatternFinal = $parsed.Command | |
| $r.ExtractedPath = $parsed.Path | |
| $r.ExtractedArguments = $parsed.Arguments | |
| Write-Step ("{0} Command chosen source" -f $proto) $r.CommandChosenSource | |
| Write-Step ("{0} Final command pattern" -f $proto) $r.CommandPatternFinal | |
| Write-Step ("{0} Extracted path" -f $proto) $r.ExtractedPath | |
| Write-Step ("{0} Extracted args" -f $proto) $r.ExtractedArguments | |
| # 4) Validate path/URI | |
| $val = Validate-PathOrUri -PathOrUri $r.ExtractedPath | |
| $r.PathValidation = $val | |
| $nearestText = $val.NearestExistingFolder | |
| if (-not $nearestText) { $nearestText = '<none>' } | |
| $pvText = ("ExistsAsFile={0}; IsAbsoluteUri={1}; NearestExistingFolder={2}" -f $val.ExistsAsFile, $val.IsAbsoluteUri, $nearestText) | |
| Write-Step ("{0} Path validation" -f $proto) $pvText | |
| if ([string]::IsNullOrWhiteSpace($r.ExtractedPath)) { | |
| throw ("Default browser path empty for {0}." -f $proto) | |
| } | |
| if (-not $val.ExistsAsFile -and -not $val.IsAbsoluteUri) { | |
| throw ("Command validation failed for {0}: {1}" -f $proto, $r.CommandPatternFinal) | |
| } | |
| # 5) File diagnostics (if file) | |
| if ($val.ExistsAsFile) { | |
| $diag = Get-FileDiagnostics -FilePath $r.ExtractedPath | |
| $r.FileDiagnostics = $diag | |
| $verText = '<none>' | |
| if ($diag -and $diag.VersionInfo) { | |
| $verText = $diag.VersionInfo | Format-List | Out-String | |
| } | |
| Write-Step ("{0} FileVersionInfo" -f $proto) $verText | |
| $sigText = '<none>' | |
| if ($diag -and $diag.Signature) { | |
| $sigText = $diag.Signature | Format-List | Out-String | |
| } | |
| Write-Step ("{0} Signature" -f $proto) $sigText | |
| } | |
| # 6) Final outputs | |
| $r.FinalBrowserPath = $r.ExtractedPath | |
| if ($r.HKCR_ApplicationNameClean) { | |
| $r.FinalBrowserName = $r.HKCR_ApplicationNameClean | |
| } elseif ($r.HKCR_ApplicationNameResolved) { | |
| $r.FinalBrowserName = $r.HKCR_ApplicationNameResolved | |
| } else { | |
| $r.FinalBrowserName = '<unknown>' | |
| } | |
| $r.FinalArgumentsPattern = $r.ExtractedArguments | |
| Write-Step ("{0} Final Browser Path" -f $proto) $r.FinalBrowserPath | |
| Write-Step ("{0} Final Browser Name" -f $proto) $r.FinalBrowserName | |
| Write-Step ("{0} Final Args Pattern" -f $proto) $r.FinalArgumentsPattern | |
| # Optional: preview / launch with a test URL | |
| if ($TestUrl) { | |
| $argsToUse = $r.FinalArgumentsPattern | |
| if ($argsToUse -match '%1') { | |
| $argsToUse = $argsToUse -replace '%1', ('"{0}"' -f $TestUrl) | |
| } elseif ($argsToUse) { | |
| $argsToUse = $argsToUse + ' ' + ('"{0}"' -f $TestUrl) | |
| } else { | |
| $argsToUse = ('"{0}"' -f $TestUrl) | |
| } | |
| Write-Step ("{0} Launch Preview" -f $proto) ('"{0}" {1}' -f $r.FinalBrowserPath, $argsToUse) | |
| if ($Launch) { | |
| Start-Process -FilePath $r.FinalBrowserPath -ArgumentList $argsToUse | |
| Write-Host "Launched." -ForegroundColor Green | |
| } else { | |
| Write-Host "(Not launching; pass -Launch to actually start)" -ForegroundColor DarkYellow | |
| } | |
| } | |
| } | |
| catch { | |
| $r.ExceptionMessage = $_.Exception.Message | |
| $r.FallbackUsed = $true | |
| Write-Step ("{0} Exception (fallback to Edge)" -f $proto) $r.ExceptionMessage | |
| $r.FinalBrowserPath = $MSEdgePath | |
| $r.FinalBrowserName = $MSEdgeName | |
| $r.FinalArgumentsPattern = $MSEdgeArgumentsPattern | |
| Write-Step ("{0} Final (Fallback) Path" -f $proto) $r.FinalBrowserPath | |
| Write-Step ("{0} Final (Fallback) Name" -f $proto) $r.FinalBrowserName | |
| Write-Step ("{0} Final (Fallback) Args" -f $proto) $r.FinalArgumentsPattern | |
| } | |
| $aggregate += [pscustomobject]$r | |
| } | |
| # Emit structured results (pipe to ConvertTo-Json if needed) | |
| $aggregate |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment