diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1 index 9d81f6f..f178c19 100644 --- a/FFUDevelopment/BuildFFUVM_UI.ps1 +++ b/FFUDevelopment/BuildFFUVM_UI.ps1 @@ -16,22 +16,6 @@ $FFUDevelopmentPath = 'C:\FFUDevelopment' # hard coded for testing $AppsPath = "$FFUDevelopmentPath\Apps" $AppListJsonPath = "$AppsPath\AppList.json" $UserAppListJsonPath = "$AppsPath\UserAppList.json" # Define path for UserAppList.json -#Microsoft sites will intermittently fail on downloads. These headers are to help with that. -$Headers = @{ - "Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" - "Accept-Encoding" = "gzip, deflate, br, zstd" - "Accept-Language" = "en-US,en;q=0.9" - "Priority" = "u=0, i" - "Sec-Ch-Ua" = "`"Microsoft Edge`";v=`"125`", `"Chromium`";v=`"125`", `"Not.A/Brand`";v=`"24`"" - "Sec-Ch-Ua-Mobile" = "?0" - "Sec-Ch-Ua-Platform" = "`"Windows`"" - "Sec-Fetch-Dest" = "document" - "Sec-Fetch-Mode" = "navigate" - "Sec-Fetch-Site" = "none" - "Sec-Fetch-User" = "?1" - "Upgrade-Insecure-Requests" = "1" -} -$UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0' # --- NEW: Central State Object --- $script:uiState = [PSCustomObject]@{ @@ -216,398 +200,6 @@ function Set-UIValue { } } -# -------------------------------------------------------------------------- -# SECTION: Driver Download Functions -# -------------------------------------------------------------------------- - -# 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 - } -} - -# 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 - } - - # $Headers and $UserAgent are available from script scope - - 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() -} - - - -# 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." - } -} - #Remove old log file if found if (Test-Path -Path $script:uiState.LogFilePath) { Remove-item -Path $script:uiState.LogFilePath -Force @@ -1365,14 +957,6 @@ $window.Add_Loaded({ $script:uiState.Controls.pbOverallProgress.Value = 0 $script:uiState.Controls.txtStatus.Text = "Preparing driver downloads..." - # Define common necessary task-specific variables locally - $localDriversFolder = $script:uiState.Controls.txtDriversFolder.Text - $localWindowsRelease = $script:uiState.Controls.cmbWindowsRelease.SelectedItem.Value - $localWindowsArch = $script:uiState.Controls.cmbWindowsArch.SelectedItem - $localHeaders = $Headers # Use script-level variable - $localUserAgent = $UserAgent # Use script-level variable - $compressDrivers = $script:uiState.Controls.chkCompressDriversToWIM.IsChecked - # Define common necessary task-specific variables locally # Ensure required selections are made if ($null -eq $script:uiState.Controls.cmbWindowsRelease.SelectedItem) { @@ -1401,8 +985,9 @@ $window.Add_Loaded({ $localWindowsRelease = $script:uiState.Controls.cmbWindowsRelease.SelectedItem.Value $localWindowsArch = $script:uiState.Controls.cmbWindowsArch.SelectedItem $localWindowsVersion = if ($null -ne $script:uiState.Controls.cmbWindowsVersion -and $null -ne $script:uiState.Controls.cmbWindowsVersion.SelectedItem) { $script:uiState.Controls.cmbWindowsVersion.SelectedItem } else { $null } - $localHeaders = $Headers # Use script-level variable - $localUserAgent = $UserAgent # Use script-level variable + $coreStaticVars = Get-CoreStaticVariables + $localHeaders = $coreStaticVars.Headers + $localUserAgent = $coreStaticVars.UserAgent $compressDrivers = $script:uiState.Controls.chkCompressDriversToWIM.IsChecked # --- Dell Catalog Handling (once, if Dell drivers are selected) --- diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Dell.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Dell.psm1 new file mode 100644 index 0000000..1d8b3bb --- /dev/null +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Dell.psm1 @@ -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 * \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.HP.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.HP.psm1 new file mode 100644 index 0000000..d7e3269 --- /dev/null +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.HP.psm1 @@ -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 * \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1 new file mode 100644 index 0000000..33883f4 --- /dev/null +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1 @@ -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 * \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 new file mode 100644 index 0000000..aec591a --- /dev/null +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 @@ -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 = '
]*>)?(.*?)(?:
)?\s*