<# .SYNOPSIS Provides functions for discovering, downloading, and processing Microsoft Surface device drivers. .DESCRIPTION This module contains the logic specific to handling Microsoft Surface drivers for the FFU UI. It includes a function to scrape the official Microsoft support website to build a list of available Surface models and their driver download pages. It also provides a robust, parallel-capable function to download the correct driver package (MSI or ZIP) based on the selected Windows release, extract its contents, and optionally compress them into a WIM archive. The download process includes logic to handle MSI installer mutexes to prevent conflicts during parallel execution. #> # Function to get the list of Microsoft Surface models function Get-MicrosoftDriversModelList { [CmdletBinding()] param( [hashtable]$Headers, # Pass necessary headers [string]$UserAgent, # Pass UserAgent [Parameter(Mandatory = $true)] [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 = ']*class="selectable-content-options__option-content(?: ocHidden)?"[^>]*>(.*?)' $divMatches = [regex]::Matches($html, $divPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) foreach ($divMatch in $divMatches) { $divContent = $divMatch.Groups[1].Value $tablePattern = ']*>(.*?)' $tableMatches = [regex]::Matches($divContent, $tablePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) foreach ($tableMatch in $tableMatches) { $tableContent = $tableMatch.Groups[1].Value $rowPattern = ']*>(.*?)' $rowMatches = [regex]::Matches($tableContent, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) foreach ($rowMatch in $rowMatches) { $rowContent = $rowMatch.Groups[1].Value $cellPattern = ']*>\s*(?:]*>)?(.*?)(?:

)?\s*' $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 = ']+href="([^"]+)"[^>]*>' # Change linkPattern to match https://www.microsoft.com/download/details.aspx?id= $linkPattern = ']+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 Save-MicrosoftDriversTask { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [PSCustomObject]$DriverItemData, # Pass data, not the UI object [Parameter(Mandatory = $true)] [string]$DriversFolder, [Parameter(Mandatory = $true)] [int]$WindowsRelease, [Parameter(Mandatory = $true)] [hashtable]$Headers, # Pass necessary headers [Parameter(Mandatory = $true)] [string]$UserAgent, # Pass UserAgent [Parameter()] # Made optional [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null [Parameter()] [bool]$CompressToWim = $false, # New parameter for compression [Parameter()] [bool]$PreserveSourceOnCompress = $false # REMOVED: UI-related parameters ) $modelName = $DriverItemData.Model $modelLink = $DriverItemData.Link $make = $DriverItemData.Make $driverRelativePath = Join-Path -Path $make -ChildPath $modelName # Relative path for the driver folder $status = "Getting download link..." # Initial local status $success = $false $sanitizedModelName = ConvertTo-SafeName -Name $modelName if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" } $makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make $modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName # Initial status update if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." } try { # Check for existing drivers $existingDriver = Test-ExistingDriver -Make $make -Model $modelName -DriversFolder $DriversFolder -Identifier $modelName -ProgressQueue $ProgressQueue if ($null -ne $existingDriver) { # Add the 'Model' property to the return object for consistency if it's not there if (-not $existingDriver.PSObject.Properties['Model']) { $existingDriver | Add-Member -MemberType NoteProperty -Name 'Model' -Value $modelName } # Special handling for existing folders that need compression if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') { $makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make $wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($modelName).wim" $wimRelativePath = Join-Path -Path $make -ChildPath "$($modelName).wim" $sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $modelName WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'." if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." } try { $null = Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop $existingDriver.Status = "Compression successful" $existingDriver.DriverPath = $wimRelativePath $existingDriver.Success = $true WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath." } catch { WriteLog "Error compressing existing drivers for $($modelName): $($_.Exception.Message)" $existingDriver.Status = "Already downloaded (Compression failed)" $existingDriver.Success = $false } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $existingDriver.Status } } return $existingDriver } ### GET THE DOWNLOAD LINK $status = "Getting download link..." if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } # Initialize Win10/Win11 link variables $win10Link = $null $win10FileName = $null $win11Link = $null $win11FileName = $null # Prefer cached Download Center details (Source C) to avoid unnecessary internet calls and cache rewrites $useCachedDownloadCenterDetails = $false try { $cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder $cachedDetails = @($cache.DownloadCenterDetails | Where-Object { $_.Link -eq $modelLink } | Select-Object -First 1) if ($cachedDetails.Count -gt 0 -and $cachedDetails[0].Files -and $cachedDetails[0].Files.Count -gt 0) { $useCachedDownloadCenterDetails = $true WriteLog "Surface cache: Using cached Download Center details for $modelName from $modelLink" foreach ($downloadFile in @($cachedDetails[0].Files)) { if ($null -eq $downloadFile) { continue } $currentFileName = $downloadFile.Name $fileUrl = $downloadFile.Url if ([string]::IsNullOrWhiteSpace($currentFileName) -or [string]::IsNullOrWhiteSpace($fileUrl)) { continue } if ($currentFileName -match "Win10") { $win10Link = $fileUrl $win10FileName = $currentFileName WriteLog "Found Win10 link (cached): $win10FileName" } elseif ($currentFileName -match "Win11") { $win11Link = $fileUrl $win11FileName = $currentFileName WriteLog "Found Win11 link (cached): $win11FileName" } } } } catch { WriteLog "Surface cache: Failed loading cached Download Center details for '$modelName'. Error: $($_.Exception.Message)" } # Cache miss: download and parse the model's Download Center page (Source C), then backfill the cache if (-not $useCachedDownloadCenterDetails) { WriteLog "Getting download page content for $modelName from $modelLink" $OriginalVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' # Use passed-in UserAgent and Headers $downloadPageContent = Invoke-WebRequest -Uri $modelLink -UseBasicParsing -Headers $Headers -UserAgent $UserAgent $VerbosePreference = $OriginalVerbosePreference WriteLog "Complete" $status = "Parsing download page..." if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } WriteLog "Parsing download page for file" $scriptPattern = '