Updates Microsoft Surface driver model discovery

Transitions the Microsoft Surface driver model list retrieval to use a centralized Learn-based index. This change unifies the scraping logic between the CLI and UI components, ensuring consistency and simplified maintenance. Cached model lists now serve both interfaces efficiently, reducing unnecessary network requests while retaining fallback mechanisms.
This commit is contained in:
rbalsleyMSFT
2026-03-23 14:47:39 -07:00
parent aca968ccbd
commit bae29fd9c7
3 changed files with 184 additions and 171 deletions
+4 -71
View File
@@ -816,78 +816,11 @@ function Get-MicrosoftDrivers {
[int]$WindowsRelease [int]$WindowsRelease
) )
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120" # Use the shared Learn-based Microsoft model index so the CLI stays aligned with the UI.
WriteLog "Getting Surface driver information from the shared Microsoft model index"
### DOWNLOAD DRIVER PAGE CONTENT $models = @(Get-SurfaceDriverModelIndex -DriversFolder $DriversFolder | Select-Object Model, Link)
WriteLog "Getting Surface driver information from $url"
$OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent
$VerbosePreference = $OriginalVerbosePreference
WriteLog "Complete" WriteLog "Complete"
WriteLog "Loaded $($models.Count) Surface models from the shared Microsoft model index."
### PARSE THE DRIVER PAGE CONTENT FOR MODELS AND DOWNLOAD LINKS
WriteLog "Parsing web content for models and download links"
$html = $webContent.Content
# Regex to match divs with selectable-content-options__option-content classes
$divPattern = '<div[^>]*class="selectable-content-options__option-content(?: ocHidden)?"[^>]*>(.*?)</div>'
$divMatches = [regex]::Matches($html, $divPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
$models = @()
foreach ($divMatch in $divMatches) {
$divContent = $divMatch.Groups[1].Value
# Find all tables within the div
$tablePattern = '<table[^>]*>(.*?)</table>'
$tableMatches = [regex]::Matches($divContent, $tablePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
foreach ($tableMatch in $tableMatches) {
$tableContent = $tableMatch.Groups[1].Value
# Find all rows in the table
$rowPattern = '<tr[^>]*>(.*?)</tr>'
$rowMatches = [regex]::Matches($tableContent, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
foreach ($rowMatch in $rowMatches) {
$rowContent = $rowMatch.Groups[1].Value
# Extract cells from the row
$cellPattern = '<td[^>]*>\s*(?:<p[^>]*>)?(.*?)(?:</p>)?\s*</td>'
$cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
if ($cellMatches.Count -ge 2) {
# Model name in the first TD
$modelName = ($cellMatches[0].Groups[1].Value).Trim()
# # Remove <p> and </p> tags if present
# $modelName = $modelName -replace '<p[^>]*>', '' -replace '</p>', ''
# $modelName = $modelName.Trim()
# The second TD might contain a link or just text
$secondTdContent = $cellMatches[1].Groups[1].Value.Trim()
# Look for a link in the second TD
$linkPattern = '<a[^>]+href="([^"]+)"[^>]*>'
$linkMatch = [regex]::Match($secondTdContent, $linkPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($linkMatch.Success) {
$modelLink = $linkMatch.Groups[1].Value
}
else {
# No link, just text instructions
$modelLink = $secondTdContent
}
$models += [PSCustomObject]@{ Model = $modelName; Link = $modelLink }
}
}
}
}
WriteLog "Parsing complete"
### FIND THE MODEL IN THE LIST OF MODELS ### FIND THE MODEL IN THE LIST OF MODELS
$selectedModel = $models | Where-Object { $_.Model -eq $Model } $selectedModel = $models | Where-Object { $_.Model -eq $Model }
@@ -164,9 +164,178 @@ function ConvertTo-SurfaceComparableName {
$value = $value -replace '\s+', ' ' $value = $value -replace '\s+', ' '
return $value.Trim().ToUpperInvariant() return $value.Trim().ToUpperInvariant()
} }
function Get-SurfaceSystemSkuReferenceIndex { function ConvertTo-SurfaceHtmlText {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$HtmlFragment
)
# Normalize HTML fragments from the Learn table into plain text values.
$textValue = $HtmlFragment -replace '<br\s*/?>', ' '
$textValue = $textValue -replace '<[^>]+>', ' '
$textValue = [System.Net.WebUtility]::HtmlDecode($textValue)
$textValue = $textValue -replace '\s+', ' '
return $textValue.Trim()
}
function ConvertTo-SurfaceDownloadCenterLink {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$LinkValue
)
# Normalize Learn links down to the canonical Download Center details URL.
$decodedLink = [System.Net.WebUtility]::HtmlDecode($LinkValue).Trim()
if ([string]::IsNullOrWhiteSpace($decodedLink)) {
return $null
}
if ($decodedLink.StartsWith('/')) {
$decodedLink = "https://www.microsoft.com$decodedLink"
}
$downloadCenterMatch = [regex]::Match(
$decodedLink,
'https://www\.microsoft\.com(?:/en-us)?/download/details\.aspx\?id=\d+',
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase
)
if (-not $downloadCenterMatch.Success) {
return $null
}
return ($downloadCenterMatch.Value -replace '/en-us/', '/')
}
function Get-SurfaceDriverModelIndex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$DriversFolder
)
$url = 'https://learn.microsoft.com/en-us/surface/manage-surface-driver-and-firmware-updates'
$minimumExpectedModelCount = 10
# Load the cached model list first to keep Microsoft model discovery fast.
try {
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
if (@($cache.ModelIndex).Count -gt 0) {
WriteLog "Surface cache: Using cached Microsoft model list ($(@($cache.ModelIndex).Count) models)."
return @($cache.ModelIndex)
}
}
catch {
WriteLog "Surface cache: Failed to load cached Microsoft model list. Falling back to the Learn source. Error: $($_.Exception.Message)"
}
try {
# Download the Learn article that now contains the authoritative Surface package table.
WriteLog "Surface cache: Downloading Microsoft model index from $url"
$headers = @{
'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
}
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $headers
$html = $webContent.Content
# Parse each table row and keep only Download Center package links.
$rowMatches = [regex]::Matches($html, '<tr[^>]*>(.*?)</tr>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
$models = [System.Collections.Generic.List[pscustomobject]]::new()
$seenModelKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($rowMatch in $rowMatches) {
$rowContent = $rowMatch.Groups[1].Value
$cellMatches = [regex]::Matches($rowContent, '<td[^>]*>\s*(.*?)\s*</td>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
if ($cellMatches.Count -lt 2) {
continue
}
$rowLabel = ConvertTo-SurfaceHtmlText -HtmlFragment $cellMatches[0].Groups[1].Value
if ([string]::IsNullOrWhiteSpace($rowLabel) -or $rowLabel -notmatch '(?i)^Surface') {
continue
}
$downloadCellContent = $cellMatches[1].Groups[1].Value
$linkMatches = [regex]::Matches(
$downloadCellContent,
'<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>',
[System.Text.RegularExpressions.RegexOptions]::Singleline -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
)
foreach ($linkMatch in $linkMatches) {
$modelName = ConvertTo-SurfaceHtmlText -HtmlFragment $linkMatch.Groups[2].Value
$modelLink = ConvertTo-SurfaceDownloadCenterLink -LinkValue $linkMatch.Groups[1].Value
if ([string]::IsNullOrWhiteSpace($modelName) -or [string]::IsNullOrWhiteSpace($modelLink)) {
continue
}
$modelKey = "$modelName`n$modelLink"
if (-not $seenModelKeys.Add($modelKey)) {
continue
}
$models.Add([pscustomobject]@{
Make = 'Microsoft'
Model = $modelName
Link = $modelLink
})
}
}
if ($models.Count -eq 0) {
throw "No Microsoft driver models were found in the Learn table."
}
if ($models.Count -lt $minimumExpectedModelCount) {
WriteLog "Surface cache: Warning - Learn parsing returned only $($models.Count) Microsoft model entries."
}
else {
WriteLog "Surface cache: Parsed $($models.Count) Microsoft model entries from Learn."
}
# Save the refreshed model list into the shared cache for both UI and CLI use.
try {
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
$cache.ModelIndex = @($models)
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
WriteLog "Surface cache: Saved Microsoft model list to cache."
}
catch {
WriteLog "Surface cache: Failed to save Microsoft model list. Error: $($_.Exception.Message)"
}
return @($models)
}
catch {
WriteLog "Surface cache: Failed to build Microsoft model list from Learn. Error: $($_.Exception.Message)"
# Fall back to the last cached model list even if it is stale when the live request fails.
try {
$cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder
if (Test-Path -Path $cachePath -PathType Leaf) {
$staleCache = Get-Content -Path $cachePath -Raw | ConvertFrom-Json -ErrorAction Stop
if (@($staleCache.ModelIndex).Count -gt 0) {
WriteLog "Surface cache: Using stale Microsoft model list ($(@($staleCache.ModelIndex).Count) models) because the live Learn request failed."
return @($staleCache.ModelIndex)
}
}
}
catch {
WriteLog "Surface cache: Failed to load stale Microsoft model list fallback. Error: $($_.Exception.Message)"
}
throw "Failed to retrieve Microsoft Surface models."
}
}
function Get-SurfaceSystemSkuReferenceIndex {
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@@ -586,6 +755,9 @@ Export-ModuleMember -Function `
Import-SurfaceDriverIndexCache, ` Import-SurfaceDriverIndexCache, `
Save-SurfaceDriverIndexCache, ` Save-SurfaceDriverIndexCache, `
ConvertTo-SurfaceComparableName, ` ConvertTo-SurfaceComparableName, `
ConvertTo-SurfaceHtmlText, `
ConvertTo-SurfaceDownloadCenterLink, `
Get-SurfaceDriverModelIndex, `
Get-SurfaceSystemSkuReferenceIndex, ` Get-SurfaceSystemSkuReferenceIndex, `
Get-SurfaceDownloadCenterDetails, ` Get-SurfaceDownloadCenterDetails, `
Get-SurfaceSystemSkuListForMicrosoftDriver Get-SurfaceSystemSkuListForMicrosoftDriver
@@ -15,100 +15,8 @@ function Get-MicrosoftDriversModelList {
[string]$DriversFolder [string]$DriversFolder
) )
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120" # Keep the UI signature unchanged while using the shared Learn-based source.
$models = @() return @(Get-SurfaceDriverModelIndex -DriversFolder $DriversFolder)
# Load cached model list first (Source B) to keep the UI fast.
# The cache is refreshed automatically when missing or invalid.
try {
$cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder
if (Test-Path -Path $cachePath -PathType Leaf) {
$cacheAgeDays = ((Get-Date) - (Get-Item -Path $cachePath).LastWriteTime).TotalDays
if ($cacheAgeDays -lt 7) {
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
if ($cache.ModelIndex -and $cache.ModelIndex.Count -gt 0) {
WriteLog "Surface cache: Using cached Microsoft model list ($($cache.ModelIndex.Count) models)."
return @($cache.ModelIndex)
}
}
}
}
catch {
WriteLog "Surface cache: Failed to load cached Microsoft model list. Falling back to online parse. Error: $($_.Exception.Message)"
}
try {
WriteLog "Getting Surface driver information from $url"
$OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
# Use passed-in UserAgent and Headers
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent
$VerbosePreference = $OriginalVerbosePreference
WriteLog "Complete"
WriteLog "Parsing web content for models and download links"
$html = $webContent.Content
$divPattern = '<div[^>]*class="selectable-content-options__option-content(?: ocHidden)?"[^>]*>(.*?)</div>'
$divMatches = [regex]::Matches($html, $divPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
foreach ($divMatch in $divMatches) {
$divContent = $divMatch.Groups[1].Value
$tablePattern = '<table[^>]*>(.*?)</table>'
$tableMatches = [regex]::Matches($divContent, $tablePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
foreach ($tableMatch in $tableMatches) {
$tableContent = $tableMatch.Groups[1].Value
$rowPattern = '<tr[^>]*>(.*?)</tr>'
$rowMatches = [regex]::Matches($tableContent, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
foreach ($rowMatch in $rowMatches) {
$rowContent = $rowMatch.Groups[1].Value
$cellPattern = '<td[^>]*>\s*(?:<p[^>]*>)?(.*?)(?:</p>)?\s*</td>'
$cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
if ($cellMatches.Count -ge 2) {
$modelName = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[0].Groups[1].Value).Trim()))
$secondTdContent = $cellMatches[1].Groups[1].Value.Trim()
# $linkPattern = '<a[^>]+href="([^"]+)"[^>]*>'
# Change linkPattern to match https://www.microsoft.com/download/details.aspx?id=
$linkPattern = '<a[^>]+href="(https://www\.microsoft\.com/download/details\.aspx\?id=\d+)"[^>]*>'
$linkMatch = [regex]::Match($secondTdContent, $linkPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($linkMatch.Success) {
$modelLink = $linkMatch.Groups[1].Value
}
else {
continue
}
$models += [PSCustomObject]@{
Make = 'Microsoft'
Model = $modelName
Link = $modelLink
}
}
}
}
}
WriteLog "Parsing complete. Found $($models.Count) models."
# Persist model list (Source B) into the local cache for fast UI population.
try {
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
$cache.ModelIndex = @($models)
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
WriteLog "Surface cache: Saved Microsoft model list to cache."
}
catch {
WriteLog "Surface cache: Failed to save Microsoft model list. Error: $($_.Exception.Message)"
}
return $models
}
catch {
WriteLog "Error getting Microsoft models: $($_.Exception.Message)"
throw "Failed to retrieve Microsoft Surface models."
}
} }
# Function to download and extract drivers for a specific Microsoft model (Modified for ForEach-Object -Parallel) # Function to download and extract drivers for a specific Microsoft model (Modified for ForEach-Object -Parallel)
function Save-MicrosoftDriversTask { function Save-MicrosoftDriversTask {