mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
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:
@@ -816,78 +816,11 @@ function Get-MicrosoftDrivers {
|
||||
[int]$WindowsRelease
|
||||
)
|
||||
|
||||
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120"
|
||||
|
||||
### DOWNLOAD DRIVER PAGE CONTENT
|
||||
WriteLog "Getting Surface driver information from $url"
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
# 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"
|
||||
$models = @(Get-SurfaceDriverModelIndex -DriversFolder $DriversFolder | Select-Object Model, Link)
|
||||
WriteLog "Complete"
|
||||
|
||||
### 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"
|
||||
WriteLog "Loaded $($models.Count) Surface models from the shared Microsoft model index."
|
||||
|
||||
### FIND THE MODEL IN THE LIST OF MODELS
|
||||
$selectedModel = $models | Where-Object { $_.Model -eq $Model }
|
||||
|
||||
@@ -161,12 +161,181 @@ function ConvertTo-SurfaceComparableName {
|
||||
|
||||
# Cleanup: remove orphaned "with" left behind by earlier removals (e.g., "Surface Pro 9 with Intel Processor")
|
||||
$value = $value -replace '(?i)\bwith\b', ''
|
||||
$value = $value -replace '\s+', ' '
|
||||
|
||||
return $value.Trim().ToUpperInvariant()
|
||||
}
|
||||
|
||||
function Get-SurfaceSystemSkuReferenceIndex {
|
||||
$value = $value -replace '\s+', ' '
|
||||
|
||||
return $value.Trim().ToUpperInvariant()
|
||||
}
|
||||
|
||||
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()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -586,6 +755,9 @@ Export-ModuleMember -Function `
|
||||
Import-SurfaceDriverIndexCache, `
|
||||
Save-SurfaceDriverIndexCache, `
|
||||
ConvertTo-SurfaceComparableName, `
|
||||
ConvertTo-SurfaceHtmlText, `
|
||||
ConvertTo-SurfaceDownloadCenterLink, `
|
||||
Get-SurfaceDriverModelIndex, `
|
||||
Get-SurfaceSystemSkuReferenceIndex, `
|
||||
Get-SurfaceDownloadCenterDetails, `
|
||||
Get-SurfaceSystemSkuListForMicrosoftDriver
|
||||
@@ -15,100 +15,8 @@ function Get-MicrosoftDriversModelList {
|
||||
[string]$DriversFolder
|
||||
)
|
||||
|
||||
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120"
|
||||
$models = @()
|
||||
|
||||
# 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."
|
||||
}
|
||||
# Keep the UI signature unchanged while using the shared Learn-based source.
|
||||
return @(Get-SurfaceDriverModelIndex -DriversFolder $DriversFolder)
|
||||
}
|
||||
# Function to download and extract drivers for a specific Microsoft model (Modified for ForEach-Object -Parallel)
|
||||
function Save-MicrosoftDriversTask {
|
||||
|
||||
Reference in New Issue
Block a user