Refactor driver management into dedicated modules

Relocates driver-specific download, parsing, and management logic from the main UI script and the FFUUI.Core module into new, dedicated modules for each manufacturer (Dell, HP, Lenovo, Microsoft). This improves modularity and code organization.

Additionally, centralizes common HTTP headers and user agent strings in the FFUUI.Core module, accessible via a new helper function.
This commit is contained in:
rbalsleyMSFT
2025-06-12 15:47:46 -07:00
parent 9282b4231e
commit 7babad8262
9 changed files with 2190 additions and 2199 deletions
@@ -0,0 +1,548 @@
# Function to get the list of Dell models from the catalog using XML streaming
function Get-DellDriversModelList {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[int]$WindowsRelease,
[Parameter(Mandatory = $true)]
[string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers)
[Parameter(Mandatory = $true)]
[string]$Make # Should be 'Dell'
)
# Define Dell specific drivers folder and catalog file names
$dellDriversFolder = Join-Path -Path $DriversFolder -ChildPath "Dell"
$catalogBaseName = if ($WindowsRelease -le 11) { "CatalogPC" } else { "Catalog" }
$dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab"
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
$catalogUrl = if ($WindowsRelease -le 11) { "http://downloads.dell.com/catalog/CatalogPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" }
$uniqueModelNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
$reader = $null
try {
# Check if the Dell catalog XML exists and is recent
$downloadCatalog = $true
if (Test-Path -Path $dellCatalogXML -PathType Leaf) {
WriteLog "Dell Catalog XML found: $dellCatalogXML"
$dellCatalogCreationTime = (Get-Item $dellCatalogXML).CreationTime
WriteLog "Dell Catalog XML Creation time: $dellCatalogCreationTime"
# Check if the XML file is less than 7 days old
if (((Get-Date) - $dellCatalogCreationTime).TotalDays -lt 7) {
WriteLog "Using existing Dell Catalog XML (less than 7 days old): $dellCatalogXML"
$downloadCatalog = $false
}
else {
WriteLog "Existing Dell Catalog XML is older than 7 days: $dellCatalogXML"
}
}
else {
WriteLog "Dell Catalog XML not found: $dellCatalogXML"
}
if ($downloadCatalog) {
WriteLog "Attempting to download and extract Dell Catalog for Get-DellDriversModelList..."
# Ensure Dell drivers folder exists
if (-not (Test-Path -Path $dellDriversFolder -PathType Container)) {
WriteLog "Creating Dell drivers folder: $dellDriversFolder"
New-Item -Path $dellDriversFolder -ItemType Directory -Force | Out-Null
}
# Check URL accessibility
try {
$request = [System.Net.WebRequest]::Create($catalogUrl)
$request.Method = 'HEAD'; $response = $request.GetResponse(); $response.Close()
}
catch { throw "Dell Catalog URL '$catalogUrl' not accessible: $($_.Exception.Message)" }
# Remove existing files before download if they exist
if (Test-Path -Path $dellCabFile) { Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue }
if (Test-Path -Path $dellCatalogXML) { Remove-Item -Path $dellCatalogXML -Force -ErrorAction SilentlyContinue }
WriteLog "Downloading Dell Catalog cab file: $catalogUrl to $dellCabFile"
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $dellCabFile
WriteLog "Dell Catalog cab file downloaded to $dellCabFile"
WriteLog "Extracting Dell Catalog cab file '$dellCabFile' to '$dellCatalogXML'"
Invoke-Process -FilePath "Expand.exe" -ArgumentList """$dellCabFile"" ""$dellCatalogXML""" | Out-Null
WriteLog "Dell Catalog cab file extracted to $dellCatalogXML"
# Delete the CAB file after extraction
WriteLog "Deleting Dell Catalog CAB file: $dellCabFile"
Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue
}
# Ensure the XML file exists before trying to read it
if (-not (Test-Path -Path $dellCatalogXML -PathType Leaf)) {
throw "Dell Catalog XML file '$dellCatalogXML' not found after download/check attempt."
}
# Use XmlReader for streaming from the XML file
$settings = New-Object System.Xml.XmlReaderSettings
$settings.IgnoreWhitespace = $true
$settings.IgnoreComments = $true
# $settings.DtdProcessing = [System.Xml.DtdProcessing]::Ignore # Optional
$reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings)
WriteLog "Starting XML stream parsing for Dell models from '$dellCatalogXML'..."
$isDriverComponent = $false
$isModelElement = $false
$modelDepth = -1 # Track depth to handle nested elements if needed
# Read through the XML stream node by node
while ($reader.Read()) {
switch ($reader.NodeType) {
([System.Xml.XmlNodeType]::Element) {
switch ($reader.Name) {
'SoftwareComponent' { $isDriverComponent = $false } # Reset flag
'ComponentType' { if ($reader.GetAttribute('value') -eq 'DRVR') { $isDriverComponent = $true } }
'Model' { if ($isDriverComponent) { $isModelElement = $true; $modelDepth = $reader.Depth } }
}
}
([System.Xml.XmlNodeType]::CDATA) {
if ($isModelElement -and $isDriverComponent) {
$modelName = $reader.Value.Trim()
if (-not [string]::IsNullOrWhiteSpace($modelName)) { $uniqueModelNames.Add($modelName) | Out-Null }
$isModelElement = $false # Reset after reading CDATA
}
}
([System.Xml.XmlNodeType]::EndElement) {
switch ($reader.Name) {
'SoftwareComponent' { $isDriverComponent = $false; $isModelElement = $false; $modelDepth = -1 }
'Model' { if ($reader.Depth -eq $modelDepth) { $isModelElement = $false; $modelDepth = -1 } }
}
}
}
} # End while ($reader.Read())
WriteLog "Finished XML stream parsing. Found $($uniqueModelNames.Count) unique Dell models."
}
catch {
WriteLog "Error getting Dell models: $($_.Exception.ToString())" # Log full exception
throw "Failed to retrieve Dell models. Check log for details." # Re-throw for UI handling
}
finally {
# Ensure the reader is closed and disposed
if ($null -ne $reader) {
$reader.Dispose()
}
# Ensure CAB file is deleted even if extraction failed but download succeeded
if (Test-Path -Path $dellCabFile) {
WriteLog "Cleaning up downloaded Dell CAB file: $dellCabFile"
Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue
}
}
# Convert HashSet to sorted list of PSCustomObjects
$models = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($modelName in ($uniqueModelNames | Sort-Object)) {
$models.Add([PSCustomObject]@{
Make = $Make
Model = $modelName
# Link is not applicable here like for Microsoft
})
}
return $models
}
# Function to download and extract drivers for a specific Dell model (Modified for ForEach-Object -Parallel)
function Save-DellDriversTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$DriverItemData, # Contains Model property
[Parameter(Mandatory = $true)]
[string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers)
[Parameter(Mandatory = $true)]
[string]$WindowsArch,
[Parameter(Mandatory = $true)]
[int]$WindowsRelease,
[Parameter(Mandatory = $true)]
[string]$DellCatalogXmlPath, # Path to the *existing* central XML catalog file
[Parameter()] # Made optional
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
[Parameter()]
[bool]$CompressToWim = $false # New parameter for compression
# REMOVED: UI-related parameters, Catalog download/extract params
)
$modelName = $DriverItemData.Model
$make = "Dell" # Hardcoded for this task
$status = "Starting..." # Initial local status
$success = $false
# Initial status update
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
try {
# 1. Check if drivers already exist for this model (final destination)
if (Test-Path -Path $modelPath -PathType Container) {
$folderSize = (Get-ChildItem -Path $modelPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$status = "Already downloaded"
WriteLog "Drivers for '$modelName' already exist in '$modelPath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $true }
}
else {
WriteLog "Driver folder '$modelPath' for '$modelName' exists but is empty/small. Re-downloading."
}
}
# 2. REMOVED: Download and Extract Catalog - This is now done centrally in the UI script
# 3. Parse the *EXISTING* XML and Find Drivers for *this specific model*
$status = "Finding drivers..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Check if the provided XML path exists
if (-not (Test-Path -Path $DellCatalogXmlPath -PathType Leaf)) {
throw "Dell Catalog XML file not found at specified path: $DellCatalogXmlPath"
}
WriteLog "Parsing existing Dell Catalog XML for model '$modelName' from: $DellCatalogXmlPath"
[xml]$xmlContent = Get-Content -Path $DellCatalogXmlPath
# Check if manifest and baseLocation exist before accessing
if ($null -eq $xmlContent.manifest -or $null -eq $xmlContent.manifest.baseLocation) {
throw "Invalid Dell Catalog XML format: Missing 'manifest' or 'baseLocation' element in '$DellCatalogXmlPath'."
}
$baseLocation = "https://" + $xmlContent.manifest.baseLocation + "/"
$latestDrivers = @{} # Hashtable to store latest drivers for this model
# Ensure SoftwareComponent is iterable
$softwareComponents = @($xmlContent.Manifest.SoftwareComponent | Where-Object { $_.ComponentType.value -eq "DRVR" })
$modelSpecificDriversFound = $false
WriteLog "Searching $($softwareComponents.Count) DRVR components in '$DellCatalogXmlPath' for model '$modelName'..."
foreach ($component in $softwareComponents) {
# Check if SupportedSystems and Brand exist
if ($null -eq $component.SupportedSystems -or $null -eq $component.SupportedSystems.Brand) { continue }
# Ensure Model is iterable
$componentModels = @($component.SupportedSystems.Brand.Model)
if ($null -eq $componentModels) { continue }
$modelMatch = $false
foreach ($item in $componentModels) {
# Check if Display and its CDATA section exist before accessing
if ($null -ne $item.Display -and $null -ne $item.Display.'#cdata-section' -and $item.Display.'#cdata-section'.Trim() -eq $modelName) {
$modelMatch = $true
break
}
}
if ($modelMatch) {
# Model matches, now check OS compatibility
$validOS = $null
if ($null -ne $component.SupportedOperatingSystems) {
# Ensure OperatingSystem is always an array/collection
$osList = @($component.SupportedOperatingSystems.OperatingSystem)
if ($null -ne $osList) {
if ($WindowsRelease -le 11) {
# Client OS check
$validOS = $osList | Where-Object { $_.osArch -eq $WindowsArch } | Select-Object -First 1
}
else {
# Server OS check
$osCodePattern = switch ($WindowsRelease) {
2016 { "W14" } # Note: Dell uses W14 for Server 2016
2019 { "W19" }
2022 { "W22" }
2025 { "W25" }
default { "W22" } # Fallback, adjust as needed
}
$validOS = $osList | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match $osCodePattern) } | Select-Object -First 1
}
}
}
if ($validOS) {
$modelSpecificDriversFound = $true # Mark that we found at least one relevant driver component
$driverPath = $component.path
$downloadUrl = $baseLocation + $driverPath
$driverFileName = [System.IO.Path]::GetFileName($driverPath)
# Check if Name, Display, and CDATA exist
$name = "UnknownDriver" # Default name
if ($null -ne $component.Name -and $null -ne $component.Name.Display -and $null -ne $component.Name.Display.'#cdata-section') {
$name = $component.Name.Display.'#cdata-section'
$name = $name -replace '[\\\/\:\*\?\"\<\>\| ]', '_' -replace '[\,]', '-'
}
# Check if Category, Display, and CDATA exist
$category = "Uncategorized" # Default category
if ($null -ne $component.Category -and $null -ne $component.Category.Display -and $null -ne $component.Category.Display.'#cdata-section') {
$category = $component.Category.Display.'#cdata-section'
$category = $category -replace '[\\\/\:\*\?\"\<\>\| ]', '_'
}
$version = [version]"0.0" # Default version
if ($null -ne $component.vendorVersion) {
try { $version = [version]$component.vendorVersion } catch { WriteLog "Warning: Could not parse version '$($component.vendorVersion)' for driver '$name'. Using 0.0." }
}
$namePrefix = ($name -split '-')[0] # Group by prefix within category
# Store the latest version for each category/prefix combination
if (-not $latestDrivers.ContainsKey($category)) { $latestDrivers[$category] = @{} }
if (-not $latestDrivers[$category].ContainsKey($namePrefix) -or $latestDrivers[$category][$namePrefix].Version -lt $version) {
$latestDrivers[$category][$namePrefix] = [PSCustomObject]@{
Name = $name
DownloadUrl = $downloadUrl
DriverFileName = $driverFileName
Version = $version
Category = $category
}
}
}
} # End if ($modelMatch)
} # End foreach ($component in $softwareComponents)
if (-not $modelSpecificDriversFound) {
$status = "No drivers found for OS"
WriteLog "No drivers found for model '$modelName' matching Windows Release '$WindowsRelease' and Arch '$WindowsArch' in '$DellCatalogXmlPath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Consider this success as the process completed, just no drivers to download
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $true }
}
# 4. Download and Extract Found Drivers (Logic remains largely the same)
$totalDriversToProcess = ($latestDrivers.Values | ForEach-Object { $_.Values.Count } | Measure-Object -Sum).Sum
$driversProcessed = 0
WriteLog "Found $totalDriversToProcess latest driver packages to download for $modelName."
# Ensure base directories exist before loop
if (-not (Test-Path -Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
if (-not (Test-Path -Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
foreach ($category in $latestDrivers.Keys) {
foreach ($driver in $latestDrivers[$category].Values) {
$driversProcessed++
$status = "Downloading $($driversProcessed)/$($totalDriversToProcess): $($driver.Name)"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$downloadFolder = Join-Path -Path $modelPath -ChildPath $driver.Category
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName
$extractFolder = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName.TrimEnd($driver.DriverFileName[-4..-1])
# Check if already extracted (more robust check)
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
WriteLog "Driver already extracted: $($driver.Name) in $extractFolder. Skipping."
continue # Skip to next driver
}
}
# Check if download file exists but extraction folder doesn't or is empty
if (Test-Path -Path $driverFilePath -PathType Leaf) {
WriteLog "Download file $($driver.DriverFileName) exists, but extraction folder '$extractFolder' is missing or empty. Will attempt extraction."
# Proceed to extraction logic below
}
else {
# Download the driver
WriteLog "Downloading driver: $($driver.Name) ($($driver.DriverFileName))"
if (-not (Test-Path -Path $downloadFolder)) {
WriteLog "Creating download folder: $downloadFolder"
New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null
}
WriteLog "Downloading from: $($driver.DownloadUrl) to $driverFilePath"
try {
Start-BitsTransferWithRetry -Source $driver.DownloadUrl -Destination $driverFilePath
WriteLog "Driver downloaded: $($driver.DriverFileName)"
}
catch {
WriteLog "Failed to download driver: $($driver.DownloadUrl). Error: $($_.Exception.Message). Skipping."
# Update status for this specific driver failure? Maybe too granular.
continue # Skip to next driver
}
}
# Extract the driver
$status = "Extracting $($driversProcessed)/$($totalDriversToProcess): $($driver.Name)"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Ensure extraction folder exists before attempting extraction
if (-not (Test-Path -Path $extractFolder)) {
WriteLog "Creating extraction folder: $extractFolder"
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
}
# Dell uses /e to extact the entire DUP while /drivers to extract only the drivers
# In many cases /drivers will extract drivers for mutliple OS versions
# Which can cause many duplicate files and bloat your driver folder
# /e seems to be better and only extracts what is necessary and has less issues
# We will default to using /e, but will fall back to /drivers if content cannot be found
$arguments = "/s /e=`"$extractFolder`" /l=`"$extractFolder\log.log`""
$altArguments = "/s /drivers=`"$extractFolder`" /l=`"$extractFolder\log.log`""
$extractionSuccess = $false
try {
# Handle special cases (Chipset/Network) - Check if OS is Server
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem # Get OS info within the task scope
$isServer = $osInfo.Caption -match 'server'
# Chipset drivers may require killing child processes in some cases
if ($driver.Category -eq "Chipset") {
WriteLog "Extracting Chipset driver: $driverFilePath $arguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false
Start-Sleep -Seconds 5 # Allow time for extraction
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
# Attempt to gracefully close child process if needed (logic from original script)
$childProcesses = Get-CimInstance Win32_Process -Filter "ParentProcessId = $($process.Id)"
if ($childProcesses) {
$latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1
WriteLog "Stopping child process for Chipset driver: $($latestProcess.Name) (PID: $($latestProcess.ProcessId))"
Stop-Process -Id $latestProcess.ProcessId -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
}
}
# Network drivers on client OS may require killing child processes
elseif ($driver.Category -eq "Network" -and -not $isServer) {
WriteLog "Extracting Network driver: $driverFilePath $arguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false
Start-Sleep -Seconds 5
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
if (-not $process.HasExited) {
$childProcesses = Get-CimInstance Win32_Process -Filter "ParentProcessId = $($process.Id)"
if ($childProcesses) {
$latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1
WriteLog "Stopping child process for Network driver: $($latestProcess.Name) (PID: $($latestProcess.ProcessId))"
Stop-Process -Id $latestProcess.ProcessId -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
}
}
}
else {
WriteLog "Extracting driver: $driverFilePath $arguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
}
# Verify extraction (check if folder has content)
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
$extractionSuccess = $true
WriteLog "Extraction successful (Method 1) for $driverFilePath $arguments"
}
}
# If primary extraction failed or folder is empty, try alternative
if (-not $extractionSuccess) {
# $arguments = "/s /e=`"$extractFolder`""
# $altArguments = "/s /drivers=`"$extractFolder`""
WriteLog "Extraction with $arguments failed or resulted in empty folder for $driverFilePath. Retrying with $altArguments"
# Clean up potentially empty folder before retrying
Remove-Item -Path $extractFolder -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null # Recreate empty folder
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $altArguments
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
# Verify extraction again
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
$extractionSuccess = $true
WriteLog "Extraction successful (Method 2) for $driverFilePath $altArguments"
}
}
}
}
catch {
WriteLog "Error during extraction process for $($driver.DriverFileName): $($_.Exception.Message). Trying alternative method."
# Try alternative method on any error during the first attempt block
try {
if (Test-Path -Path $extractFolder) {
# Clean up before retry if needed
Remove-Item -Path $extractFolder -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
# $arguments = "/s /e=`"$extractFolder`""
# $altArguments = "/s /drivers=`"$extractFolder`""
WriteLog "Extracting driver (Method 2): $driverFilePath $altArguments"
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $altArguments
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
# Verify extraction again
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
$extractionSuccess = $true
WriteLog "Extraction successful (Method 2) for $driverFilePath."
}
}
}
catch {
WriteLog "Alternative extraction method also failed for $($driver.DriverFileName): $($_.Exception.Message)."
# Extraction failed completely
}
}
# Cleanup downloaded file only if extraction was successful
if ($extractionSuccess) {
WriteLog "Deleting driver file: $driverFilePath"
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
WriteLog "Driver file deleted: $driverFilePath"
}
else {
WriteLog "Extraction failed for $($driver.DriverFileName). Downloaded file kept at $driverFilePath for inspection."
# Update status to indicate partial failure?
}
} # End foreach ($driver in $latestDrivers)
} # End foreach ($category in $latestDrivers)
# --- Compress to WIM if requested (after all drivers processed) ---
if ($CompressToWim) {
$status = "Compressing..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$wimFileName = "$($modelName).wim"
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
try {
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop
if ($compressResult) {
WriteLog "Compression successful for '$modelName'."
$status = "Completed & Compressed"
}
else {
WriteLog "Compression failed for '$modelName'. Check verbose/error output from Compress-DriverFolderToWim."
$status = "Completed (Compression Failed)"
}
}
catch {
WriteLog "Error during compression for '$modelName': $($_.Exception.Message)"
$status = "Completed (Compression Error)"
}
}
else {
$status = "Completed" # Final status if not compressing
}
# --- End Compression ---
$success = $true # Mark success as download/extract was okay
}
catch {
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
WriteLog "Error saving Dell drivers for $($modelName): $($_.Exception.ToString())" # Log full exception string
$success = $false
# Enqueue the error status before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Ensure return object is created even on error
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
}
# REMOVED: Finally block that cleaned up temp catalog files
# Enqueue the final status (success or error) before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Return the final status
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
}
Export-ModuleMember -Function *
@@ -0,0 +1,402 @@
# Function to get the list of HP models from the PlatformList.xml
function Get-HPDriversModelList {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$DriversFolder,
[Parameter(Mandatory = $true)]
[string]$Make # Expected to be 'HP'
)
WriteLog "Getting HP driver model list..."
$hpDriversFolder = Join-Path -Path $DriversFolder -ChildPath $Make
$platformListUrl = 'https://hpia.hpcloud.hp.com/ref/platformList.cab'
$platformListCab = Join-Path -Path $hpDriversFolder -ChildPath "platformList.cab"
$platformListXml = Join-Path -Path $hpDriversFolder -ChildPath "PlatformList.xml"
$modelList = [System.Collections.Generic.List[PSCustomObject]]::new()
try {
# Ensure HP drivers folder exists
if (-not (Test-Path -Path $hpDriversFolder)) {
WriteLog "Creating HP Drivers folder: $hpDriversFolder"
New-Item -Path $hpDriversFolder -ItemType Directory -Force | Out-Null
}
# Download PlatformList.cab if it doesn't exist or is outdated (e.g., older than 7 days)
if (-not (Test-Path -Path $platformListCab) -or ((Get-Date) - (Get-Item $platformListCab).LastWriteTime).TotalDays -gt 7) {
WriteLog "Downloading $platformListUrl to $platformListCab"
# Use the private helper function for download with retry
Start-BitsTransferWithRetry -Source $platformListUrl -Destination $platformListCab -ErrorAction Stop
WriteLog "PlatformList.cab download complete."
# Force extraction if downloaded
if (Test-Path -Path $platformListXml) {
Remove-Item -Path $platformListXml -Force
}
}
else {
WriteLog "Using existing PlatformList.cab found at $platformListCab"
}
# Extract PlatformList.xml if it doesn't exist
if (-not (Test-Path -Path $platformListXml)) {
WriteLog "Expanding $platformListCab to $platformListXml"
# Use the private helper function for process invocation
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$platformListCab`"", "`"$platformListXml`"") -ErrorAction Stop | Out-Null
WriteLog "PlatformList.xml extraction complete."
}
else {
WriteLog "Using existing PlatformList.xml found at $platformListXml"
}
# Parse the PlatformList.xml using XmlReader for efficiency
WriteLog "Parsing PlatformList.xml to extract HP models..."
$settings = New-Object System.Xml.XmlReaderSettings
$settings.Async = $false # Ensure synchronous reading
$reader = [System.Xml.XmlReader]::Create($platformListXml, $settings)
$uniqueModels = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
while ($reader.Read()) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'Platform') {
# Read the inner content of the Platform node
$platformReader = $reader.ReadSubtree()
while ($platformReader.Read()) {
if ($platformReader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $platformReader.Name -eq 'ProductName') {
$modelName = $platformReader.ReadElementContentAsString()
if (-not [string]::IsNullOrWhiteSpace($modelName) -and $uniqueModels.Add($modelName)) {
# Add to list only if it's a new unique model
$modelList.Add([PSCustomObject]@{
Make = $Make
Model = $modelName
})
}
}
}
$platformReader.Close()
}
}
$reader.Close()
WriteLog "Successfully parsed $($modelList.Count) unique HP models from PlatformList.xml."
}
catch {
WriteLog "Error getting HP driver model list: $($_.Exception.Message)"
}
# Sort the list alphabetically by Model name before returning
return $modelList | Sort-Object -Property Model
}
# Function to download and extract drivers for a specific HP model (Designed for ForEach-Object -Parallel)
function Save-HPDriversTask {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[PSCustomObject]$DriverItemData, # Contains Make, Model
[Parameter(Mandatory = $true)]
[string]$DriversFolder,
[Parameter(Mandatory = $true)]
[ValidateSet("x64", "x86", "ARM64")]
[string]$WindowsArch,
[Parameter(Mandatory = $true)]
[ValidateSet(10, 11)]
[int]$WindowsRelease,
[Parameter(Mandatory = $true)]
[string]$WindowsVersion, # e.g., 22H2, 23H2, etc.
[Parameter()] # Made optional
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
[Parameter()]
[bool]$CompressToWim = $false # New parameter for compression
)
$modelName = $DriverItemData.Model
$make = $DriverItemData.Make # Should be 'HP'
$identifier = $modelName # Unique identifier for progress updates
$hpDriversBaseFolder = Join-Path -Path $DriversFolder -ChildPath $make # Changed variable name for clarity
$platformListXml = Join-Path -Path $hpDriversBaseFolder -ChildPath "PlatformList.xml"
$modelSpecificFolder = Join-Path -Path $hpDriversBaseFolder -ChildPath ($modelName -replace '[\\/:"*?<>|]', '_') # Sanitize model name for folder path
$finalStatus = "" # Initialize final status
$successState = $true # Assume success unless an operation fails
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking HP drivers for $modelName..." }
# Ensure the base HP folder exists
if (-not (Test-Path -Path $hpDriversBaseFolder -PathType Container)) {
try {
New-Item -Path $hpDriversBaseFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
WriteLog "Created base HP driver folder: $hpDriversBaseFolder"
}
catch {
$errMsg = "Failed to create base HP driver folder '$hpDriversBaseFolder': $($_.Exception.Message)"
WriteLog $errMsg
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Error: Create HP dir failed" }
return [PSCustomObject]@{ Identifier = $identifier; Status = "Error: Create HP dir failed"; Success = $false }
}
}
# Check if drivers already exist for this model
if (Test-Path -Path $modelSpecificFolder -PathType Container) {
WriteLog "HP drivers for '$identifier' already exist in '$modelSpecificFolder'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Found existing HP drivers for $identifier. Verifying..." }
if ($CompressToWim) {
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim" # WIM in base HP folder, next to model folder
if (Test-Path -Path $wimFilePath -PathType Leaf) {
$finalStatus = "Already downloaded (WIM exists)"
WriteLog "WIM file $wimFilePath already exists for $identifier."
}
else {
WriteLog "WIM file $wimFilePath not found for $identifier. Attempting compression of existing folder '$modelSpecificFolder'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing HP drivers for $identifier..." }
try {
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
$finalStatus = "Already downloaded & Compressed"
WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath."
}
catch {
$errMsgForLog = "Error compressing existing drivers for $($identifier): $($_.Exception.Message)"
WriteLog $errMsgForLog
$finalStatus = "Already downloaded (Compression failed: $($_.Exception.Message.Split([Environment]::NewLine)[0]))"
# $successState = false # Keep true if folder exists, compression is secondary
}
}
}
else {
# Not compressing
$finalStatus = "Already downloaded"
}
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $finalStatus }
return [PSCustomObject]@{
Identifier = $identifier
Status = $finalStatus
Success = $successState
}
}
# If folder does not exist, proceed with download and extraction
WriteLog "HP drivers for '$identifier' not found locally. Starting download process..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Downloading HP drivers for $identifier..." }
try {
# Ensure PlatformList.xml exists (it should have been downloaded by Get-HPDriversModelList)
if (-not (Test-Path -Path $platformListXml)) {
# Attempt to download/extract it again if missing
WriteLog "PlatformList.xml not found for HP task, attempting download/extract..."
$platformListUrl = 'https://hpia.hpcloud.hp.com/ref/platformList.cab'
$platformListCab = Join-Path -Path $hpDriversBaseFolder -ChildPath "platformList.cab"
# Base folder already checked/created
Start-BitsTransferWithRetry -Source $platformListUrl -Destination $platformListCab -ErrorAction Stop
if (Test-Path -Path $platformListXml) { Remove-Item -Path $platformListXml -Force }
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$platformListCab`"", "`"$platformListXml`"") -ErrorAction Stop | Out-Null
WriteLog "PlatformList.xml download/extract complete for HP task."
if (-not (Test-Path -Path $platformListXml)) {
throw "Failed to obtain PlatformList.xml for HP driver task."
}
}
# Parse PlatformList.xml to find SystemID and OSReleaseID for the specific model
WriteLog "Parsing $platformListXml for model '$modelName' details..."
[xml]$platformListContent = Get-Content -Path $platformListXml -Raw -Encoding UTF8 -ErrorAction Stop
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.ProductName.'#text' -match "^$([regex]::Escape($modelName))$" } | Select-Object -First 1
if ($null -eq $platformNode) {
throw "Model '$modelName' not found in PlatformList.xml."
}
$systemID = $platformNode.SystemID
# --- OS Node Selection with Fallback Logic ---
$selectedOSNode = $null
$selectedOSVersion = $null
$selectedOSRelease = $WindowsRelease # Start with the requested release
# Complete list of Windows 11 feature-update versions (newest to oldest)
$win11Versions = @(
"24H2", "23H2", "22H2", "21H2"
)
# Complete list of Windows 10 feature-update versions (newest to oldest)
$win10Versions = @(
"22H2", "21H2", "21H1", "20H2", "2004", "1909", "1903", "1809", "1803", "1709", "1703", "1607", "1511", "1507"
)
# Helper function to find a matching OS node for a given release and version list
function Find-MatchingOSNode {
param(
[int]$ReleaseToSearch,
[array]$VersionsToSearch
)
$osNodesForRelease = $platformNode.OS | Where-Object {
($ReleaseToSearch -eq 11 -and $_.IsWindows11 -contains 'true') -or
($ReleaseToSearch -eq 10 -and ($null -eq $_.IsWindows11 -or $_.IsWindows11 -notcontains 'true'))
}
if ($null -eq $osNodesForRelease) { return $null }
foreach ($version in $VersionsToSearch) {
foreach ($osNode in $osNodesForRelease) {
$releaseIDs = $osNode.OSReleaseIdFileName -replace 'H', 'h' -split ' '
if ($releaseIDs -contains $version.ToLower()) {
return @{ Node = $osNode; Version = $version }
}
}
}
return $null
}
# 1. Attempt Exact Match (Requested Release and Version)
WriteLog "Attempting to find exact match for Win$($WindowsRelease) ($($WindowsVersion))..."
$exactMatchResult = Find-MatchingOSNode -ReleaseToSearch $WindowsRelease -VersionsToSearch @($WindowsVersion)
if ($null -ne $exactMatchResult) {
$selectedOSNode = $exactMatchResult.Node
$selectedOSVersion = $exactMatchResult.Version
WriteLog "Exact match found: Win$($selectedOSRelease) ($($selectedOSVersion))."
}
else {
WriteLog "Exact match not found for Win$($WindowsRelease) ($($WindowsVersion))."
# 2. Fallback: Same Release, Other Versions (Newest First)
WriteLog "Attempting fallback within Win$($WindowsRelease)..."
$versionsForCurrentRelease = if ($WindowsRelease -eq 11) { $win11Versions } else { $win10Versions }
$fallbackVersions = $versionsForCurrentRelease | Where-Object { $_ -ne $WindowsVersion }
$fallbackResult = Find-MatchingOSNode -ReleaseToSearch $WindowsRelease -VersionsToSearch $fallbackVersions
if ($null -ne $fallbackResult) {
$selectedOSNode = $fallbackResult.Node
$selectedOSVersion = $fallbackResult.Version
WriteLog "Fallback successful within Win$($selectedOSRelease). Using version: $($selectedOSVersion)."
}
else {
WriteLog "Fallback within Win$($WindowsRelease) unsuccessful."
# 3. Fallback: Other Release, Versions (Newest First)
$otherRelease = if ($WindowsRelease -eq 11) { 10 } else { 11 }
WriteLog "Attempting fallback to Win$($otherRelease)..."
$versionsForOtherRelease = if ($otherRelease -eq 11) { $win11Versions } else { $win10Versions }
$otherFallbackResult = Find-MatchingOSNode -ReleaseToSearch $otherRelease -VersionsToSearch $versionsForOtherRelease
if ($null -ne $otherFallbackResult) {
$selectedOSNode = $otherFallbackResult.Node
$selectedOSVersion = $otherFallbackResult.Version
$selectedOSRelease = $otherRelease
WriteLog "Fallback successful to Win$($selectedOSRelease). Using version: $($selectedOSVersion)."
}
else {
WriteLog "Fallback to Win$($otherRelease) also failed."
}
}
}
if ($null -eq $selectedOSNode) {
$allAvailableVersions = @()
if ($platformNode.OS) {
foreach ($osNode in $platformNode.OS) {
$osRel = if ($osNode.IsWindows11 -contains 'true') { 11 } else { 10 }
$relIDs = $osNode.OSReleaseIdFileName -replace 'H', 'h' -split ' '
foreach ($id in $relIDs) { $allAvailableVersions += "Win$($osRel) $($id)" }
}
}
$availableVersionsString = ($allAvailableVersions | Select-Object -Unique) -join ', '
if ([string]::IsNullOrWhiteSpace($availableVersionsString)) { $availableVersionsString = "None" }
throw "Could not find any suitable OS driver pack for model '$modelName' matching requested or fallback versions (Win$($WindowsRelease) $WindowsVersion). Available: $availableVersionsString"
}
$osReleaseIdFileName = $selectedOSNode.OSReleaseIdFileName -replace 'H', 'h'
WriteLog "Using SystemID: $systemID and OS Info: Win$($selectedOSRelease) ($($selectedOSVersion)) for '$modelName'"
$archSuffix = $WindowsArch -replace "^x", ""
$modelRelease = "$($systemID)_$($archSuffix)_$($selectedOSRelease).0.$($selectedOSVersion.ToLower())"
$driverCabUrl = "https://hpia.hpcloud.hp.com/ref/$systemID/$modelRelease.cab"
$driverCabFile = Join-Path -Path $hpDriversBaseFolder -ChildPath "$modelRelease.cab" # Store in base HP folder
$driverXmlFile = Join-Path -Path $hpDriversBaseFolder -ChildPath "$modelRelease.xml" # Store in base HP folder
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Downloading driver index..." }
WriteLog "Downloading HP Driver cab from $driverCabUrl to $driverCabFile"
Start-BitsTransferWithRetry -Source $driverCabUrl -Destination $driverCabFile -ErrorAction Stop
WriteLog "Expanding HP Driver cab $driverCabFile to $driverXmlFile"
if (Test-Path -Path $driverXmlFile) { Remove-Item -Path $driverXmlFile -Force }
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$driverCabFile`"", "`"$driverXmlFile`"") -ErrorAction Stop | Out-Null
WriteLog "Parsing driver XML $driverXmlFile"
[xml]$driverXmlContent = Get-Content -Path $driverXmlFile -Raw -Encoding UTF8 -ErrorAction Stop
$updates = $driverXmlContent.ImagePal.Solutions.UpdateInfo | Where-Object { $_.Category -match '^Driver' }
$totalDrivers = ($updates | Measure-Object).Count
$downloadedCount = 0
WriteLog "Found $totalDrivers driver updates for $modelName."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Found $totalDrivers drivers. Downloading..." }
if (-not (Test-Path -Path $modelSpecificFolder)) {
New-Item -Path $modelSpecificFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
foreach ($update in $updates) {
$driverName = $update.Name -replace '[\\/:"*?<>|]', '_'
$category = $update.Category -replace '[\\/:"*?<>|]', '_'
$version = $update.Version -replace '[\\/:"*?<>|]', '_'
$driverUrl = "https://$($update.URL)"
$driverFileName = Split-Path -Path $driverUrl -Leaf
$downloadFolder = Join-Path -Path $modelSpecificFolder -ChildPath $category
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driverFileName
$extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverName + "_" + $version + "_" + ($driverFileName -replace '\.exe$', ''))
$downloadedCount++
$progressMsg = "($downloadedCount/$totalDrivers) Downloading $driverName..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $progressMsg }
WriteLog "$progressMsg URL: $driverUrl"
if (Test-Path -Path $extractFolder) {
WriteLog "Driver already extracted to $extractFolder, skipping download."
continue
}
if (-not (Test-Path -Path $downloadFolder)) {
New-Item -Path $downloadFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
WriteLog "Downloading driver to: $driverFilePath"
Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath -ErrorAction Stop
WriteLog "Driver downloaded: $driverFilePath"
WriteLog "Creating extraction folder: $extractFolder"
New-Item -Path $extractFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
$arguments = "/s /e /f `"$extractFolder`""
WriteLog "Extracting driver $driverFilePath with args: $arguments"
#DEBUG
# wrap $driverFilePath in quotes to handle spaces
# $driverFilePath = "`"$driverFilePath`""
WriteLog "Running HP Driver Extraction Command: $driverFilePath $arguments"
Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -ErrorAction Stop | Out-Null
# Start-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait -NoNewWindow -ErrorAction Stop | Out-Null
WriteLog "Driver extracted to: $extractFolder"
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
WriteLog "Deleted driver installer: $driverFilePath"
}
Remove-Item -Path $driverCabFile, $driverXmlFile -Force -ErrorAction SilentlyContinue
WriteLog "Cleaned up driver cab and xml files for $modelName"
$finalStatus = "Completed"
if ($CompressToWim) {
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing..." }
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim"
WriteLog "Compressing '$modelSpecificFolder' to '$wimFilePath'..."
try {
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
WriteLog "Compression successful for '$identifier'."
$finalStatus = "Completed & Compressed"
}
catch {
WriteLog "Error during compression for '$identifier': $($_.Exception.Message)"
$finalStatus = "Completed (Compression Failed)"
}
}
$successState = $true
}
catch {
$errorMessage = "Error saving HP drivers for $($modelName): $($_.Exception.Message)"
WriteLog $errorMessage
$finalStatus = "Error: $($_.Exception.Message.Split([Environment]::NewLine)[0])"
$successState = $false
if (Test-Path -Path $modelSpecificFolder -PathType Container) {
WriteLog "Attempting to remove partially created folder $modelSpecificFolder due to error."
Remove-Item -Path $modelSpecificFolder -Recurse -Force -ErrorAction SilentlyContinue
}
}
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $finalStatus }
return [PSCustomObject]@{ Identifier = $identifier; Status = $finalStatus; Success = $successState }
}
Export-ModuleMember -Function *
@@ -0,0 +1,430 @@
# Function to get the list of Lenovo models using the PSREF API
function Get-LenovoDriversModelList {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ModelSearchTerm, # User input for model/machine type
[Parameter(Mandatory = $true)]
[hashtable]$Headers,
[Parameter(Mandatory = $true)]
[string]$UserAgent
)
WriteLog "Querying Lenovo PSREF API for model/machine type: $ModelSearchTerm"
$url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$([uri]::EscapeDataString($ModelSearchTerm))"
$models = [System.Collections.Generic.List[PSCustomObject]]::new()
try {
$OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent -ErrorAction Stop
$VerbosePreference = $OriginalVerbosePreference
WriteLog "PSREF API query complete."
$jsonResponse = $response.Content | ConvertFrom-Json
if ($null -ne $jsonResponse.data -and $jsonResponse.data.Count -gt 0) {
foreach ($item in $jsonResponse.data) {
$productName = $item.ProductName
$machineTypes = $item.MachineType -split " / " # Split if multiple machine types are listed
foreach ($machineTypeRaw in $machineTypes) {
$machineType = $machineTypeRaw.Trim()
# Only add if machine type is not empty
if (-not [string]::IsNullOrWhiteSpace($machineType)) {
# Create the combined display string
$displayModel = "$productName ($machineType)"
# Add each combination as a separate entry
$models.Add([PSCustomObject]@{
Make = 'Lenovo'
Model = $displayModel
ProductName = $productName
MachineType = $machineType
})
}
else {
WriteLog "Skipping entry for product '$productName' due to missing machine type."
}
}
}
WriteLog "Found $($models.Count) potential model/machine type combinations for '$ModelSearchTerm'."
}
else {
WriteLog "No models found matching '$ModelSearchTerm' in Lenovo PSREF."
}
}
catch {
WriteLog "Error querying Lenovo PSREF API: $($_.Exception.Message)"
# Return empty list on error
}
return $models
}
# Function to download and extract drivers for a specific Lenovo model (Background Task)
function Save-LenovoDriversTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$DriverItemData, # Contains Model (ProductName) and MachineType
[Parameter(Mandatory = $true)]
[string]$DriversFolder,
[Parameter(Mandatory = $true)]
[int]$WindowsRelease,
[Parameter(Mandatory = $true)]
[hashtable]$Headers,
[Parameter(Mandatory = $true)]
[string]$UserAgent,
[Parameter()] # Made optional
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null,
[Parameter()]
[bool]$CompressToWim = $false
)
# The Model property from the UI already contains the combined "ProductName (MachineType)" string
$identifier = $DriverItemData.Model
$machineType = $DriverItemData.MachineType
$make = "Lenovo"
$status = "Starting..."
$success = $false
# Define paths
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
# Use the identifier (which contains the model name and machine type) and sanitize it for the path
$modelPath = Join-Path -Path $makeDriversPath -ChildPath ($identifier -replace '[\\/:"*?<>|]', '_')
$tempDownloadPath = Join-Path -Path $makeDriversPath -ChildPath "_TEMP_$($machineType)_$($PID)" # Temp folder for catalog/package XMLs
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking..." }
try {
# 1. Check if drivers already exist for this model (final destination)
if (Test-Path -Path $modelPath -PathType Container) {
$folderSize = (Get-ChildItem -Path $modelPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$status = "Already downloaded"
WriteLog "Drivers for '$identifier' already exist in '$modelPath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $true }
}
else {
WriteLog "Driver folder '$modelPath' for '$identifier' exists but is empty/small. Re-downloading."
}
}
# Ensure base directories exist
if (-not (Test-Path -Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
if (-not (Test-Path -Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
if (-not (Test-Path -Path $tempDownloadPath)) { New-Item -Path $tempDownloadPath -ItemType Directory -Force | Out-Null }
# 2. Construct and Download Catalog URL
$modelRelease = $machineType + "_Win" + $WindowsRelease
$catalogUrl = "https://download.lenovo.com/catalog/$modelRelease.xml"
$lenovoCatalogXML = Join-Path -Path $tempDownloadPath -ChildPath "$modelRelease.xml"
$status = "Downloading Catalog..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
WriteLog "Downloading Lenovo Driver catalog for '$identifier' from $catalogUrl"
# Check URL accessibility first
try {
$request = [System.Net.WebRequest]::Create($catalogUrl); $request.Method = 'HEAD'; $response = $request.GetResponse(); $response.Close()
}
catch { throw "Lenovo Driver catalog URL is not accessible: $catalogUrl. Error: $($_.Exception.Message)" }
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $lenovoCatalogXML
WriteLog "Catalog download Complete: $lenovoCatalogXML"
# 3. Parse Catalog and Process Packages
$status = "Parsing Catalog..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
[xml]$xmlContent = Get-Content -Path $lenovoCatalogXML -Encoding UTF8
$packages = @($xmlContent.packages.package) # Ensure it's an array
$totalPackages = $packages.Count
$processedPackages = 0
WriteLog "Found $totalPackages packages in catalog for '$identifier'."
foreach ($package in $packages) {
$processedPackages++
$category = $package.category
$packageUrl = $package.location # URL to the package's *XML* file
# Skip BIOS/Firmware based on category
if ($category -like 'BIOS*' -or $category -like 'Firmware*') {
WriteLog "($processedPackages/$totalPackages) Skipping BIOS/Firmware package: $category"
continue
}
# Sanitize category for path
$categoryClean = $category -replace '[\\/:"*?<>|]', '_'
if ($categoryClean -eq 'Motherboard Devices Backplanes core chipset onboard video PCIe switches') {
$categoryClean = 'Motherboard Devices' # Shorten long category name
}
$packageName = [System.IO.Path]::GetFileName($packageUrl)
$packageXMLPath = Join-Path -Path $tempDownloadPath -ChildPath $packageName
$baseURL = $packageUrl -replace [regex]::Escape($packageName), "" # Base URL for the driver file
$status = "($processedPackages/$totalPackages) Getting package info..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Download the package XML
WriteLog "($processedPackages/$totalPackages) Downloading package XML: $packageUrl"
try {
Start-BitsTransferWithRetry -Source $packageUrl -Destination $packageXMLPath
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to download package XML '$packageUrl'. Skipping. Error: $($_.Exception.Message)"
continue # Skip this package
}
# Load and parse the package XML
[xml]$packageXmlContent = Get-Content -Path $packageXMLPath -Encoding UTF8
$packageType = $packageXmlContent.Package.PackageType.type
$packageTitleRaw = $packageXmlContent.Package.title.InnerText
# Filter out non-driver packages (Type 2 = Driver)
if ($packageType -ne 2) {
WriteLog "($processedPackages/$totalPackages) Skipping package '$packageTitleRaw' (Type: $packageType) - Not a driver."
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
continue
}
# Sanitize title for folder name
$packageTitle = $packageTitleRaw -replace '[\\/:"*?<>|]', '_' -replace ' - .*', ''
# Extract driver file name and extract command
$driverFileName = $null
$extractCommand = $null
try {
$driverFileName = $packageXmlContent.Package.Files.Installer.File.Name
$extractCommand = $packageXmlContent.Package.ExtractCommand
}
catch {
WriteLog "($processedPackages/$totalPackages) Error parsing package XML '$packageXMLPath' for file name/command. Skipping. Error: $($_.Exception.Message)"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
continue
}
# Skip if essential info is missing
if ([string]::IsNullOrWhiteSpace($driverFileName) -or [string]::IsNullOrWhiteSpace($extractCommand)) {
WriteLog "($processedPackages/$totalPackages) Skipping package '$packageTitleRaw' - Missing driver file name or extract command in XML."
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
continue
}
# Construct paths
$driverUrl = $baseURL + $driverFileName
$categoryPath = Join-Path -Path $modelPath -ChildPath $categoryClean
$downloadFolder = Join-Path -Path $categoryPath -ChildPath $packageTitle # Final destination subfolder
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driverFileName
$extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverFileName -replace '\.exe$', '') # Extract to subfolder named after exe
# Check if already extracted
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
WriteLog "($processedPackages/$totalPackages) Driver '$packageTitleRaw' already extracted to '$extractFolder'. Skipping."
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
continue
}
}
# Ensure download folder exists
if (-not (Test-Path -Path $downloadFolder)) {
New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null
}
# Download the driver .exe
$status = "($processedPackages/$totalPackages) Downloading $packageTitle..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
WriteLog "($processedPackages/$totalPackages) Downloading driver: $driverUrl to $driverFilePath"
try {
Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath
WriteLog "($processedPackages/$totalPackages) Driver downloaded: $driverFileName"
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to download driver '$driverUrl'. Skipping. Error: $($_.Exception.Message)"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
continue # Skip this driver
}
# --- Extraction Logic ---
$status = "($processedPackages/$totalPackages) Extracting $packageTitle..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Always use a temporary extraction path to avoid long path issues
$originalExtractFolder = $extractFolder # Store the originally intended final path
$extractionSucceeded = $false
$tempExtractBase = $null # Initialize
# Create randomized number for use with temp folder name
$randomNumber = Get-Random -Minimum 1000 -Maximum 9999
$tempExtractBase = Join-Path $env:TEMP "LenovoDriverExtract_$randomNumber"
$extractFolder = Join-Path $tempExtractBase ($driverFileName -replace '\.exe$', '') # Actual temp extraction folder
WriteLog "($processedPackages/$totalPackages) Using temporary extraction path: $extractFolder"
# Ensure the base temp directory exists
if (-not (Test-Path -Path $tempExtractBase)) {
New-Item -Path $tempExtractBase -ItemType Directory -Force | Out-Null
}
# Ensure the target temporary extraction folder exists
if (-not (Test-Path -Path $extractFolder)) {
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
}
# Modify the extract command to point to the temporary folder
$modifiedExtractCommand = $extractCommand -replace '%PACKAGEPATH%', "`"$extractFolder`""
WriteLog "($processedPackages/$totalPackages) Extracting driver: $driverFilePath using command: $modifiedExtractCommand"
try {
Invoke-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand -Wait $true | Out-Null
WriteLog "($processedPackages/$totalPackages) Driver extracted to temporary path: $extractFolder"
$extractionSucceeded = $true
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to extract driver '$driverFilePath' to temporary path. Skipping. Error: $($_.Exception.Message)"
# Don't delete the downloaded exe yet if extraction fails
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
# Clean up temp folder if extraction failed
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
}
continue # Skip further processing for this driver
}
# --- Post-Extraction Handling (Move from Temp to Final Destination) ---
if ($extractionSucceeded) {
WriteLog "($processedPackages/$totalPackages) Performing post-extraction move from temp to final destination..."
try {
# Ensure the *original* final destination folder exists and is empty
if (Test-Path -Path $originalExtractFolder) {
WriteLog "($processedPackages/$totalPackages) Clearing existing final destination folder: $originalExtractFolder"
Get-ChildItem -Path $originalExtractFolder -Recurse | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
else {
WriteLog "($processedPackages/$totalPackages) Creating final destination folder: $originalExtractFolder"
New-Item -Path $originalExtractFolder -ItemType Directory -Force | Out-Null
}
# Get all items (files and folders) directly inside the temp extraction folder
$extractedItems = Get-ChildItem -Path $extractFolder -ErrorAction Stop
foreach ($item in $extractedItems) {
$itemName = $item.Name
$finalDestinationPath = $null
# Check if it's a directory containing 'Liteon'
if ($item.PSIsContainer -and $itemName -like '*Liteon*') {
# Rename Liteon folders with a random number suffix
$randomNumber = Get-Random -Minimum 1000 -Maximum 9999
$finalFolderName = "Liteon_$randomNumber"
$finalDestinationPath = Join-Path -Path $originalExtractFolder -ChildPath $finalFolderName
WriteLog "($processedPackages/$totalPackages) Moving Liteon folder '$itemName' to '$finalDestinationPath'"
}
else {
# For other files/folders, move them directly
$finalDestinationPath = Join-Path -Path $originalExtractFolder -ChildPath $itemName
WriteLog "($processedPackages/$totalPackages) Moving item '$itemName' to '$finalDestinationPath'"
}
# Perform the move
try {
Move-Item -Path $item.FullName -Destination $finalDestinationPath -Force -ErrorAction Stop
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to move item '$($item.FullName)' to '$finalDestinationPath'. Error: $($_.Exception.Message)"
# Decide if this should stop the whole process or just skip this item
# For now, we'll log and continue, but mark overall success as false
$extractionSucceeded = $false
}
} # End foreach ($item in $extractedItems)
if ($extractionSucceeded) {
WriteLog "($processedPackages/$totalPackages) All driver contents moved successfully from temp to final destination."
}
else {
WriteLog "($processedPackages/$totalPackages) Some driver contents failed to move. Check logs."
}
}
catch {
WriteLog "($processedPackages/$totalPackages) Error during post-extraction move: $($_.Exception.Message). Files might remain in temp."
$extractionSucceeded = $false # Mark as failed for cleanup logic below
}
finally {
# Clean up the base temporary directory regardless of move success/failure
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
WriteLog "($processedPackages/$totalPackages) Cleaning up temporary extraction base: $tempExtractBase"
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
# --- Final Cleanup ---
# Delete the downloaded .exe only if extraction AND move were successful
if ($extractionSucceeded) {
WriteLog "($processedPackages/$totalPackages) Deleting driver installation file: $driverFilePath"
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
}
else {
WriteLog "($processedPackages/$totalPackages) Keeping driver installation file due to extraction/move failure: $driverFilePath"
}
# Always delete the package XML
WriteLog "($processedPackages/$totalPackages) Deleting package XML file: $packageXMLPath"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
} # End foreach package
# --- Compress to WIM if requested (after all drivers processed) ---
if ($CompressToWim) {
$status = "Compressing..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
$wimFileName = "$($identifier).wim" # Use sanitized identifier for filename
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
try {
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $identifier -WimDescription $identifier -ErrorAction Stop
if ($compressResult) {
WriteLog "Compression successful for '$identifier'."
$status = "Completed & Compressed"
}
else {
WriteLog "Compression failed for '$identifier'. Check verbose/error output from Compress-DriverFolderToWim."
$status = "Completed (Compression Failed)"
}
}
catch {
WriteLog "Error during compression for '$identifier': $($_.Exception.Message)"
$status = "Completed (Compression Error)"
}
}
else {
$status = "Completed"
}
# --- End Compression ---
$success = $true # Mark success as download/extract was okay
}
catch {
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
WriteLog "Error saving Lenovo drivers for '$identifier': $($_.Exception.ToString())" # Log full exception string
$success = $false
# Enqueue the error status before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Ensure return object is created even on error
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success }
}
finally {
# Clean up the main catalog XML and temp folder
WriteLog "Cleaning up temporary download folder: $tempDownloadPath"
Remove-Item -Path $tempDownloadPath -Recurse -Force -ErrorAction SilentlyContinue
}
# Enqueue the final status (success or error) before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Return the final status
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success }
}
Export-ModuleMember -Function *
@@ -0,0 +1,384 @@
# Function to get the list of Microsoft Surface models
function Get-MicrosoftDriversModelList {
[CmdletBinding()]
param(
[hashtable]$Headers, # Pass necessary headers
[string]$UserAgent # Pass UserAgent
)
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120"
$models = @()
try {
WriteLog "Getting Surface driver information from $url"
WriteLog "Using UserAgent: $UserAgent"
WriteLog "Using Headers: $($Headers | Out-String)"
$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 = ($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."
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
# REMOVED: UI-related parameters
)
$modelName = $DriverItemData.Model
$modelLink = $DriverItemData.Link
$make = $DriverItemData.Make
$status = "Getting download link..." # Initial local status
$success = $false
# Initial status update
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
try {
# Check if drivers already exist for this model
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
if (Test-Path -Path $modelPath -PathType Container) {
$folderSize = (Get-ChildItem -Path $modelPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($folderSize -gt 1MB) {
$status = "Already downloaded"
WriteLog "Drivers for '$modelName' already exist in '$modelPath'."
# Enqueue this status before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Return success immediately
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $true }
}
else {
# Status is not set to error here, just log and continue
WriteLog "Driver folder '$modelPath' for '$modelName' exists but is empty or very small. Re-downloading."
# Allow the process to continue to re-download
}
}
### GET THE DOWNLOAD LINK
$status = "Getting download link..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
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 = '<script>window.__DLCDetails__={(.*?)}<\/script>'
$scriptMatch = [regex]::Match($downloadPageContent.Content, $scriptPattern)
if ($scriptMatch.Success) {
$scriptContent = $scriptMatch.Groups[1].Value
# $downloadFilePattern = '"name":"(.*?)",.*?"url":"(.*?)"'
$downloadFilePattern = '"name":"([^"]+\.(?:msi|zip))",[^}]*?"url":"(.*?)"'
$downloadFileMatches = [regex]::Matches($scriptContent, $downloadFilePattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$win10Link = $null
$win10FileName = $null
$win11Link = $null
$win11FileName = $null
# Iterate through all matches to find potential Win10 and Win11 links
foreach ($downloadFile in $downloadFileMatches) {
$currentFileName = $downloadFile.Groups[1].Value
$fileUrl = $downloadFile.Groups[2].Value
if ($currentFileName -match "Win10") {
$win10Link = $fileUrl
$win10FileName = $currentFileName
WriteLog "Found Win10 link: $win10FileName"
}
elseif ($currentFileName -match "Win11") {
$win11Link = $fileUrl
$win11FileName = $currentFileName
WriteLog "Found Win11 link: $win11FileName"
}
}
# Decision logic to select the appropriate download link
$downloadLink = $null
$fileName = $null
$downloadedVersion = $null # Track which version we are actually downloading
if ($WindowsRelease -eq 10 -and $win10Link) {
$downloadLink = $win10Link
$fileName = $win10FileName
$downloadedVersion = 10
WriteLog "Exact match found for Win10."
}
elseif ($WindowsRelease -eq 11 -and $win11Link) {
$downloadLink = $win11Link
$fileName = $win11FileName
$downloadedVersion = 11
WriteLog "Exact match found for Win11."
}
elseif (-not $win10Link -and $win11Link) {
# Only Win11 available, regardless of $WindowsRelease
$downloadLink = $win11Link
$fileName = $win11FileName
$downloadedVersion = 11
WriteLog "Exact match for Win$($WindowsRelease) not found. Falling back to available Win11 driver."
}
elseif ($win10Link -and -not $win11Link) {
# Only Win10 available, regardless of $WindowsRelease
$downloadLink = $win10Link
$fileName = $win10FileName
$downloadedVersion = 10
WriteLog "Exact match for Win$($WindowsRelease) not found. Falling back to available Win10 driver."
}
# If both Win10 and Win11 links exist, but neither matches $WindowsRelease, $downloadLink remains $null.
### DOWNLOAD AND EXTRACT
if ($downloadLink) {
WriteLog "Selected Download Link for $modelName (Actual: Windows $downloadedVersion): $downloadLink"
$status = "Downloading (Win$downloadedVersion)..." # Update status message
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Create directories
if (-not (Test-Path -Path $DriversFolder)) {
WriteLog "Creating Drivers folder: $DriversFolder"
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
}
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
if (-Not (Test-Path -Path $modelPath)) {
WriteLog "Creating model folder: $modelPath"
New-Item -Path $modelPath -ItemType Directory -Force | Out-Null
}
else {
WriteLog "Model folder already exists: $modelPath"
}
### DOWNLOAD
$filePath = Join-Path -Path $makeDriversPath -ChildPath ($fileName)
WriteLog "Downloading $modelName driver file to $filePath"
# Use Start-BitsTransferWithRetry
Start-BitsTransferWithRetry -Source $downloadLink -Destination $filePath
WriteLog "Download complete"
$fileExtension = [System.IO.Path]::GetExtension($filePath).ToLower()
### EXTRACT
if ($fileExtension -eq ".msi") {
$status = "Extracting MSI..." # Set initial status
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Loop indefinitely to wait for mutex and handle MSIExec exit codes by catching errors
while ($true) {
$mutexClear = $false
# 1. Check Mutex
try {
$Mutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute")
$Mutex.Dispose()
$status = "Waiting for MSIExec..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
WriteLog "Another MSIExec installer is running (Mutex Held). Waiting 5 seconds before rechecking for $modelName..."
Start-Sleep -Seconds 5
continue # Go back to start of while loop to re-check mutex
}
catch [System.Threading.WaitHandleCannotBeOpenedException] {
# Mutex is clear, proceed to extraction attempt
WriteLog "Mutex clear. Proceeding with MSI extraction attempt for $modelName."
$status = "Extracting MSI..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$mutexClear = $true
}
catch {
# Handle other potential errors when checking the mutex
WriteLog "Warning: Error checking MSIExec mutex for $($modelName): $_. Proceeding with caution."
$status = "Extracting MSI (Mutex Error)..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$mutexClear = $true # Proceed despite mutex error
}
# 2. Attempt Extraction (only if mutex was clear or error occurred during check)
if ($mutexClear) {
WriteLog "Extracting MSI file to $modelPath"
$arguments = "/a `"$($filePath)`" /qn TARGETDIR=`"$($modelPath)`""
try {
# Use Invoke-Process. It will throw an error for any non-zero exit code.
Invoke-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait $true -ErrorAction Stop | Out-Null
# If Invoke-Process succeeded (didn't throw), extraction is complete.
WriteLog "Extraction complete for $modelName (Exit Code 0)."
break # Success, exit the while loop
}
catch {
# Catch errors thrown by Invoke-Process
$errorMessage = $_.Exception.Message
if ($errorMessage -match 'Process exited with code 1618') {
# Specific handling for MSIExec busy error (1618)
WriteLog "MSIExec collision detected (Exit Code 1618) for $modelName. Retrying after wait..."
$status = "Waiting (MSI Collision)..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
Start-Sleep -Seconds 5 # Wait before retrying
continue # Go back to start of while loop to re-check mutex/retry
}
else {
# Handle other errors from Invoke-Process (e.g., file not found, permissions, other exit codes)
WriteLog "Error during MSI extraction process for $($modelName): $errorMessage"
throw # Re-throw the original exception to be caught by the outer try/catch
}
}
} # End if ($mutexClear)
} # End while ($true) - Loop runs until break or throw
}
elseif ($fileExtension -eq ".zip") {
$status = "Extracting ZIP..." # Set status before extraction
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
WriteLog "Extracting ZIP file to $modelPath"
$ProgressPreference = 'SilentlyContinue'
Expand-Archive -Path $filePath -DestinationPath $modelPath -Force
$ProgressPreference = 'Continue'
WriteLog "Extraction complete"
}
else {
WriteLog "Unsupported file type: $fileExtension"
throw "Unsupported file type: $fileExtension"
}
# Remove downloaded file
$status = "Cleaning up..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
WriteLog "Removing $filePath"
Remove-Item -Path $filePath -Force
WriteLog "Cleanup complete." # Changed log message slightly
# --- Compress to WIM if requested ---
if ($CompressToWim) {
$status = "Compressing..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$wimFileName = "$($modelName).wim"
# Corrected WIM path: WIM file should be next to the model folder, not inside it.
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
try {
# Use the function from the imported common module
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop
if ($compressResult) {
WriteLog "Compression successful for '$modelName'."
$status = "Completed & Compressed"
}
else {
WriteLog "Compression failed for '$modelName'. Check verbose/error output from Compress-DriverFolderToWim."
$status = "Completed (Compression Failed)"
# Don't mark overall success as false, download/extract succeeded
}
}
catch {
WriteLog "Error during compression for '$modelName': $($_.Exception.Message)"
$status = "Completed (Compression Error)"
# Don't mark overall success as false
}
}
else {
$status = "Completed" # Final status if not compressing
}
# --- End Compression ---
$success = $true # Mark success as download/extract was okay
} # End if/elseif for .msi/.zip
else {
WriteLog "No suitable download link found for Windows $WindowsRelease (or fallback) for model $modelName."
$status = "Error: No Win$($WindowsRelease)/Fallback link"
$success = $false
}
}
else {
WriteLog "Failed to parse the download page for the driver file for model $modelName."
$status = "Error: Parse failed"
$success = $false
}
}
catch {
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
WriteLog "Error saving Microsoft drivers for $($modelName): $($_.Exception.Message)"
$success = $false
# Enqueue the error status before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Ensure return object is created even on error
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
}
# Enqueue the final status (success or error) before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Return the final status (this is still used by Receive-Job for final confirmation)
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
}
Export-ModuleMember -Function *
@@ -0,0 +1,390 @@
# Helper function to get models for a selected Make and standardize them
function Get-ModelsForMake {
param(
[Parameter(Mandatory = $true)]
[string]$SelectedMake,
[Parameter(Mandatory = $true)]
[psobject]$State
)
$standardizedModels = [System.Collections.Generic.List[PSCustomObject]]::new()
$rawModels = @()
# Get necessary values from UI or script scope
$localDriversFolder = $State.Controls.txtDriversFolder.Text
$localWindowsRelease = $null
if ($null -ne $State.Controls.cmbWindowsRelease.SelectedItem) {
$localWindowsRelease = $State.Controls.cmbWindowsRelease.SelectedItem.Value
}
# Get headers and user agent from Get-CoreStaticVariables
$staticVars = Get-CoreStaticVariables
$Headers = $staticVars.Headers
$UserAgent = $staticVars.UserAgent
if (-not $localWindowsRelease -and ($SelectedMake -eq 'Dell' -or $SelectedMake -eq 'Lenovo')) {
[System.Windows.MessageBox]::Show("Please select a Windows Release first for $SelectedMake.", "Missing Information", "OK", "Warning")
throw "Windows Release not selected for $SelectedMake."
}
switch ($SelectedMake) {
'Microsoft' {
$rawModels = Get-MicrosoftDriversModelList -Headers $Headers -UserAgent $UserAgent
}
'Dell' {
$rawModels = Get-DellDriversModelList -WindowsRelease $localWindowsRelease -DriversFolder $localDriversFolder -Make $SelectedMake
}
'HP' {
$rawModels = Get-HPDriversModelList -DriversFolder $localDriversFolder -Make $SelectedMake
}
'Lenovo' {
$modelSearchTerm = [Microsoft.VisualBasic.Interaction]::InputBox("Enter Lenovo Model Name or Machine Type (e.g., T480 or 20L5):", "Lenovo Model Search", "")
if ([string]::IsNullOrWhiteSpace($modelSearchTerm)) {
# User cancelled or entered nothing
return @()
}
$State.Controls.txtStatus.Text = "Searching Lenovo models for '$modelSearchTerm'..."
$rawModels = Get-LenovoDriversModelList -ModelSearchTerm $modelSearchTerm -Headers $Headers -UserAgent $UserAgent
}
default {
[System.Windows.MessageBox]::Show("Selected Make '$SelectedMake' is not supported for automatic model retrieval.", "Unsupported Make", "OK", "Warning")
return @()
}
}
if ($null -ne $rawModels) {
foreach ($rawModel in $rawModels) {
# Filter out Chromebooks for Lenovo before standardization
if ($SelectedMake -eq 'Lenovo' -and $rawModel.Model -match 'Chromebook') {
WriteLog "Get-ModelsForMake: Skipping Chromebook model: $($rawModel.Model)"
continue
}
$standardizedModels.Add((ConvertTo-StandardizedDriverModel -RawDriverObject $rawModel -Make $SelectedMake -State $State))
}
}
return $standardizedModels.ToArray()
}
# Helper function to convert raw driver objects to a standardized format
function ConvertTo-StandardizedDriverModel {
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$RawDriverObject,
[Parameter(Mandatory = $true)]
[string]$Make,
[Parameter(Mandatory = $true)]
[psobject]$State
)
$modelDisplay = $RawDriverObject.Model # Default
$id = $RawDriverObject.Model # Default
$link = $null
$productName = $null
$machineType = $null
if ($RawDriverObject.PSObject.Properties['Link']) {
$link = $RawDriverObject.Link
}
# Lenovo specific handling
if ($Make -eq 'Lenovo') {
$modelDisplay = $RawDriverObject.Model
$productName = $RawDriverObject.ProductName
$machineType = $RawDriverObject.MachineType
$id = $RawDriverObject.MachineType
}
return [PSCustomObject]@{
IsSelected = $false
Make = $Make
Model = $modelDisplay
Link = $link
Id = $id
ProductName = $productName
MachineType = $machineType
Version = "" # Placeholder
Type = "" # Placeholder
Size = "" # Placeholder
Arch = "" # Placeholder
DownloadStatus = "" # Initial download status
}
}
# Function to filter the driver model list based on text input
function Filter-DriverModels {
param(
[string]$filterText,
[Parameter(Mandatory = $true)]
[psobject]$State
)
# Check if UI elements and the full list are available
if ($null -eq $State.Controls.lstDriverModels -or $null -eq $State.Data.allDriverModels) {
WriteLog "Filter-DriverModels: ListView or full model list not available."
return
}
WriteLog "Filtering models with text: '$filterText'"
# Filter the full list based on the Model property (case-insensitive)
# Ensure the result is always an array, even if only one item matches
$filteredModels = @($State.Data.allDriverModels | Where-Object { $_.Model -like "*$filterText*" })
# Update the ListView's ItemsSource with the filtered list
# Setting ItemsSource directly should work for simple scenarios
$State.Controls.lstDriverModels.ItemsSource = $filteredModels
# Explicitly refresh the ListView's view to reflect the changes in the bound source
if ($null -ne $State.Controls.lstDriverModels.ItemsSource -and $State.Controls.lstDriverModels.Items -is [System.ComponentModel.ICollectionView]) {
$State.Controls.lstDriverModels.Items.Refresh()
}
elseif ($null -ne $State.Controls.lstDriverModels.ItemsSource) {
# Fallback refresh if not using ICollectionView (less common for direct ItemsSource binding)
$State.Controls.lstDriverModels.Items.Refresh()
}
WriteLog "Filtered list contains $($filteredModels.Count) models."
}
# Function to save selected driver models to a JSON file
function Save-DriversJson {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
WriteLog "Save-DriversJson function called."
$selectedDrivers = @($State.Controls.lstDriverModels.Items | Where-Object { $_.IsSelected })
if (-not $selectedDrivers) {
[System.Windows.MessageBox]::Show("No drivers selected to save.", "Save Drivers", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
WriteLog "No drivers selected to save."
return
}
$outputJson = @{} # Use a Hashtable for the desired structure
$selectedDrivers | Group-Object -Property Make | ForEach-Object {
$makeName = $_.Name
$modelsForThisMake = @() # Initialize an array to hold model objects
foreach ($driverItem in $_.Group) {
$modelObject = $null
switch ($makeName) {
'Microsoft' {
$modelObject = @{
Name = $driverItem.Model # Model is the display name
Link = $driverItem.Link
}
}
'Dell' {
$modelObject = @{
Name = $driverItem.Model
}
}
'HP' {
$modelObject = @{
Name = $driverItem.Model
}
}
'Lenovo' {
$modelObject = @{
Name = $driverItem.Model # This is "ProductName (MachineType)"
ProductName = $driverItem.ProductName # This is "ProductName"
MachineType = $driverItem.MachineType # This is "MachineType"
}
}
default {
WriteLog "Save-DriversJson: Unknown Make '$makeName' encountered for model '$($driverItem.Model)'. Skipping."
}
}
if ($null -ne $modelObject) {
$modelsForThisMake += $modelObject
}
}
if ($modelsForThisMake.Count -gt 0) {
# Store the array of model objects under a "Models" key
$outputJson[$makeName] = @{
"Models" = $modelsForThisMake
}
}
}
$sfd = New-Object System.Windows.Forms.SaveFileDialog
$sfd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*"
$sfd.Title = "Save Selected Drivers"
$sfd.FileName = "Drivers.json"
$sfd.InitialDirectory = $FFUDevelopmentPath
if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
try {
$outputJson | ConvertTo-Json -Depth 5 | Set-Content -Path $sfd.FileName -Encoding UTF8
[System.Windows.MessageBox]::Show("Selected drivers saved to $($sfd.FileName)", "Save Successful", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
WriteLog "Selected drivers saved to $($sfd.FileName)"
}
catch {
[System.Windows.MessageBox]::Show("Error saving drivers file: $($_.Exception.Message)", "Save Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
WriteLog "Error saving drivers file to $($sfd.FileName): $($_.Exception.Message)"
}
}
else {
WriteLog "Save drivers operation cancelled by user."
}
}
# Function to import driver models from a JSON file
function Import-DriversJson {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
WriteLog "Import-DriversJson function called."
$ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*"
$ofd.Title = "Import Drivers"
$ofd.InitialDirectory = $FFUDevelopmentPath
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
try {
$importedData = Get-Content -Path $ofd.FileName -Raw | ConvertFrom-Json
if ($null -eq $importedData -or $importedData -isnot [System.Management.Automation.PSCustomObject]) {
[System.Windows.MessageBox]::Show("Invalid JSON file format. Expected a JSON object with Makes as keys.", "Import Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
WriteLog "Import-DriversJson: Invalid JSON format in $($ofd.FileName). Expected an object."
return
}
$newModelsAdded = 0
$existingModelsUpdated = 0
if ($null -eq $State.Data.allDriverModels) {
$State.Data.allDriverModels = @()
}
$importedData.PSObject.Properties | ForEach-Object {
$makeName = $_.Name
$makeData = $_.Value # This is the object containing "Models" array
# Check if $makeData is null, not a PSCustomObject, or does not have a 'Models' property
if ($null -eq $makeData -or $makeData -isnot [System.Management.Automation.PSCustomObject] -or -not ($makeData.PSObject.Properties | Where-Object { $_.Name -eq 'Models' })) {
WriteLog "Import-DriversJson: Skipping Make '$makeName' due to invalid structure or missing 'Models' key."
return # Corresponds to 'continue' in ForEach-Object script block
}
$modelObjectArray = $makeData.Models # This is now an array of objects
if ($null -eq $modelObjectArray -or $modelObjectArray -isnot [array]) {
WriteLog "Import-DriversJson: Skipping Make '$makeName' because 'Models' value is not an array."
return
}
foreach ($importedModelObject in $modelObjectArray) {
if ($null -eq $importedModelObject -or -not $importedModelObject.PSObject.Properties['Name']) {
WriteLog "Import-DriversJson: Skipping model for Make '$makeName' due to missing 'Name' property or null object."
continue
}
$importedModelNameFromObject = $importedModelObject.Name
if ([string]::IsNullOrWhiteSpace($importedModelNameFromObject)) {
WriteLog "Import-DriversJson: Skipping empty model name for Make '$makeName'."
continue
}
$existingModel = $State.Data.allDriverModels | Where-Object { $_.Make -eq $makeName -and $_.Model -eq $importedModelNameFromObject } | Select-Object -First 1
if ($null -ne $existingModel) {
$existingModel.IsSelected = $true
$existingModel.DownloadStatus = "Imported"
if ($makeName -eq 'Microsoft' -and $importedModelObject.PSObject.Properties['Link']) {
if ($existingModel.Link -ne $importedModelObject.Link) {
$existingModel.Link = $importedModelObject.Link
WriteLog "Import-DriversJson: Updated Link for existing Microsoft model '$($existingModel.Model)'."
}
}
elseif ($makeName -eq 'Lenovo') {
$updateExistingLenovo = $false
if ($importedModelObject.PSObject.Properties['ProductName'] -and $existingModel.PSObject.Properties['ProductName'] -and $existingModel.ProductName -ne $importedModelObject.ProductName) {
$existingModel.ProductName = $importedModelObject.ProductName
$updateExistingLenovo = $true
}
if ($importedModelObject.PSObject.Properties['MachineType'] -and $existingModel.PSObject.Properties['MachineType'] -and $existingModel.MachineType -ne $importedModelObject.MachineType) {
$existingModel.MachineType = $importedModelObject.MachineType
$existingModel.Id = $importedModelObject.MachineType # Update Id as well
$updateExistingLenovo = $true
}
if ($updateExistingLenovo) {
WriteLog "Import-DriversJson: Updated ProductName/MachineType/Id for existing Lenovo model '$($existingModel.Model)'."
}
}
$existingModelsUpdated++
WriteLog "Import-DriversJson: Marked existing model '$($existingModel.Make) - $($existingModel.Model)' as imported."
}
else {
# Model does not exist, create a new one
$importedLink = if ($makeName -eq 'Microsoft' -and $importedModelObject.PSObject.Properties['Link']) { $importedModelObject.Link } else { $null }
$importedId = $importedModelNameFromObject # Default Id
$importedProductName = $null
$importedMachineType = $null
if ($makeName -eq 'Lenovo') {
$importedProductName = if ($importedModelObject.PSObject.Properties['ProductName']) { $importedModelObject.ProductName } else { $null }
$importedMachineType = if ($importedModelObject.PSObject.Properties['MachineType']) { $importedModelObject.MachineType } else { $null }
if ($null -ne $importedMachineType) {
$importedId = $importedMachineType # Override Id for Lenovo
}
# Fallback parsing if ProductName/MachineType are missing from JSON but Name has the pattern
if (($null -eq $importedProductName -or $null -eq $importedMachineType) -and $importedModelNameFromObject -match '(.+?)\s*\((.+?)\)$') {
WriteLog "Import-DriversJson: Lenovo model '$importedModelNameFromObject' missing ProductName or MachineType in JSON. Attempting to parse from Name."
if ($null -eq $importedProductName) { $importedProductName = $matches[1].Trim() }
if ($null -eq $importedMachineType) {
$importedMachineType = $matches[2].Trim()
$importedId = $importedMachineType # Update Id if MachineType was parsed here
}
}
if ($null -eq $importedProductName -or $null -eq $importedMachineType) {
WriteLog "Import-DriversJson: Warning - Lenovo model '$importedModelNameFromObject' is missing ProductName or MachineType after parsing. ID might be based on full name."
}
}
$newDriverModel = [PSCustomObject]@{
IsSelected = $true
Make = $makeName
Model = $importedModelNameFromObject # Full display name
Link = $importedLink
Id = $importedId
ProductName = $importedProductName
MachineType = $importedMachineType
Version = ""
Type = ""
Size = ""
Arch = ""
DownloadStatus = "Imported"
}
$State.Data.allDriverModels += $newDriverModel
$newModelsAdded++
WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)"
}
}
}
$State.Data.allDriverModels = $State.Data.allDriverModels | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model
Filter-DriverModels -filterText $State.Controls.txtModelFilter.Text -State $script:uiState
$message = "Driver import complete.`nNew models added: $newModelsAdded`nExisting models updated: $existingModelsUpdated"
[System.Windows.MessageBox]::Show($message, "Import Successful", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
WriteLog $message
}
catch {
[System.Windows.MessageBox]::Show("Error importing drivers file: $($_.Exception.Message)", "Import Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
WriteLog "Error importing drivers file from $($ofd.FileName): $($_.Exception.Message)"
}
}
else {
WriteLog "Import drivers operation cancelled by user."
}
}
Export-ModuleMember -Function *
+6 -1
View File
@@ -66,7 +66,12 @@ RequiredModules = @('..\FFU.Common\FFU.Common.psd1')
# FormatsToProcess = @()
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @('FFUUI.Shared.psm1')
NestedModules = @('FFUUI.Core.Shared.psm1',
'FFUUI.Core.Drivers.psm1',
'FFUUI.Core.Drivers.Dell.psm1',
'FFUUI.Core.Drivers.HP.psm1',
'FFUUI.Core.Drivers.Lenovo.psm1',
'FFUUI.Core.Drivers.Microsoft.psm1')
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = '*'
File diff suppressed because it is too large Load Diff