mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 10:19:36 -06:00
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:
@@ -16,22 +16,6 @@ $FFUDevelopmentPath = 'C:\FFUDevelopment' # hard coded for testing
|
|||||||
$AppsPath = "$FFUDevelopmentPath\Apps"
|
$AppsPath = "$FFUDevelopmentPath\Apps"
|
||||||
$AppListJsonPath = "$AppsPath\AppList.json"
|
$AppListJsonPath = "$AppsPath\AppList.json"
|
||||||
$UserAppListJsonPath = "$AppsPath\UserAppList.json" # Define path for UserAppList.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 ---
|
# --- NEW: Central State Object ---
|
||||||
$script:uiState = [PSCustomObject]@{
|
$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
|
#Remove old log file if found
|
||||||
if (Test-Path -Path $script:uiState.LogFilePath) {
|
if (Test-Path -Path $script:uiState.LogFilePath) {
|
||||||
Remove-item -Path $script:uiState.LogFilePath -Force
|
Remove-item -Path $script:uiState.LogFilePath -Force
|
||||||
@@ -1365,14 +957,6 @@ $window.Add_Loaded({
|
|||||||
$script:uiState.Controls.pbOverallProgress.Value = 0
|
$script:uiState.Controls.pbOverallProgress.Value = 0
|
||||||
$script:uiState.Controls.txtStatus.Text = "Preparing driver downloads..."
|
$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
|
# Define common necessary task-specific variables locally
|
||||||
# Ensure required selections are made
|
# Ensure required selections are made
|
||||||
if ($null -eq $script:uiState.Controls.cmbWindowsRelease.SelectedItem) {
|
if ($null -eq $script:uiState.Controls.cmbWindowsRelease.SelectedItem) {
|
||||||
@@ -1401,8 +985,9 @@ $window.Add_Loaded({
|
|||||||
$localWindowsRelease = $script:uiState.Controls.cmbWindowsRelease.SelectedItem.Value
|
$localWindowsRelease = $script:uiState.Controls.cmbWindowsRelease.SelectedItem.Value
|
||||||
$localWindowsArch = $script:uiState.Controls.cmbWindowsArch.SelectedItem
|
$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 }
|
$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
|
$coreStaticVars = Get-CoreStaticVariables
|
||||||
$localUserAgent = $UserAgent # Use script-level variable
|
$localHeaders = $coreStaticVars.Headers
|
||||||
|
$localUserAgent = $coreStaticVars.UserAgent
|
||||||
$compressDrivers = $script:uiState.Controls.chkCompressDriversToWIM.IsChecked
|
$compressDrivers = $script:uiState.Controls.chkCompressDriversToWIM.IsChecked
|
||||||
|
|
||||||
# --- Dell Catalog Handling (once, if Dell drivers are selected) ---
|
# --- Dell Catalog Handling (once, if Dell drivers are selected) ---
|
||||||
|
|||||||
@@ -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 *
|
||||||
@@ -66,7 +66,12 @@ RequiredModules = @('..\FFU.Common\FFU.Common.psd1')
|
|||||||
# FormatsToProcess = @()
|
# FormatsToProcess = @()
|
||||||
|
|
||||||
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
|
# 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.
|
# 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 = '*'
|
FunctionsToExport = '*'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user