mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
863 lines
45 KiB
PowerShell
863 lines
45 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Provides functions for managing and downloading hardware drivers in the FFU Builder UI.
|
|
.DESCRIPTION
|
|
This module contains all the business logic for the 'Drivers' tab in the FFU Builder UI. It handles fetching driver model lists from various manufacturers (Microsoft, Dell, HP, Lenovo), displaying and filtering them in the UI, and managing the selection state. It also includes functions to import and export driver selections to a JSON file (Drivers.json) and to orchestrate the parallel download of selected driver packages using the common parallel processing module.
|
|
#>
|
|
|
|
# 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
|
|
$id = $RawDriverObject.Model
|
|
$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
|
|
}
|
|
|
|
# Dell-specific passthrough (needed for per-model cab workflow)
|
|
$dellBrand = $null
|
|
$dellModelNumber = $null
|
|
$dellSystemId = $null
|
|
$dellCabUrl = $null
|
|
$dellCabRelative = $null
|
|
if ($Make -eq 'Dell') {
|
|
if ($RawDriverObject.PSObject.Properties['Brand']) { $dellBrand = $RawDriverObject.Brand }
|
|
if ($RawDriverObject.PSObject.Properties['ModelNumber']) { $dellModelNumber = $RawDriverObject.ModelNumber }
|
|
if ($RawDriverObject.PSObject.Properties['SystemId']) { $dellSystemId = $RawDriverObject.SystemId }
|
|
if ($RawDriverObject.PSObject.Properties['CabUrl']) { $dellCabUrl = $RawDriverObject.CabUrl }
|
|
if ($RawDriverObject.PSObject.Properties['CabRelativePath']) { $dellCabRelative = $RawDriverObject.CabRelativePath }
|
|
}
|
|
|
|
$output = [PSCustomObject]@{
|
|
IsSelected = $false
|
|
Make = $Make
|
|
Model = $modelDisplay
|
|
Link = $link
|
|
Id = $id
|
|
ProductName = $productName
|
|
MachineType = $machineType
|
|
Version = ""
|
|
Type = ""
|
|
Size = ""
|
|
Arch = ""
|
|
DownloadStatus = ""
|
|
}
|
|
|
|
if ($Make -eq 'Dell') {
|
|
# Add Dell-only fields so Save-DellDriversTask can use CabUrl
|
|
$output | Add-Member -NotePropertyName Brand -NotePropertyValue $dellBrand
|
|
$output | Add-Member -NotePropertyName ModelNumber -NotePropertyValue $dellModelNumber
|
|
$output | Add-Member -NotePropertyName SystemId -NotePropertyValue $dellSystemId
|
|
$output | Add-Member -NotePropertyName CabUrl -NotePropertyValue $dellCabUrl
|
|
$output | Add-Member -NotePropertyName CabRelativePath -NotePropertyValue $dellCabRelative
|
|
}
|
|
|
|
return $output
|
|
}
|
|
|
|
# Function to filter the driver model list based on text input
|
|
function Search-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 "Search-DriverModels: ListView or full model list not available."
|
|
return
|
|
}
|
|
|
|
# Ensure the ItemsSource is always the master list. This prevents inconsistency.
|
|
if ($State.Controls.lstDriverModels.ItemsSource -ne $State.Data.allDriverModels) {
|
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
|
}
|
|
|
|
# Get the default view of the items source, which supports filtering.
|
|
$collectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($State.Controls.lstDriverModels.ItemsSource)
|
|
if ($null -eq $collectionView) {
|
|
WriteLog "Search-DriverModels: Could not get CollectionView. Filtering may not work."
|
|
return
|
|
}
|
|
|
|
WriteLog "Applying filter with text: '$filterText'"
|
|
|
|
if ([string]::IsNullOrWhiteSpace($filterText)) {
|
|
# If filter is empty, remove any existing filter
|
|
$collectionView.Filter = $null
|
|
}
|
|
else {
|
|
# Apply a filter predicate. This is the correct WPF way to filter.
|
|
$collectionView.Filter = {
|
|
param($item)
|
|
# $item is the PSCustomObject from the list
|
|
return $item.Model -like "*$filterText*"
|
|
}
|
|
}
|
|
|
|
# The view will automatically refresh. No need to call .Refresh() explicitly for filtering.
|
|
$filteredCount = 0
|
|
if ($null -ne $collectionView) {
|
|
foreach ($item in $collectionView) { $filteredCount++ }
|
|
}
|
|
WriteLog "Filter applied. View now contains $filteredCount 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
|
|
}
|
|
if ($driverItem.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driverItem.SystemId)) {
|
|
$modelObject.SystemId = $driverItem.SystemId
|
|
}
|
|
if ($driverItem.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($driverItem.CabUrl)) {
|
|
$modelObject.CabUrl = $driverItem.CabUrl
|
|
}
|
|
if ($driverItem.PSObject.Properties['CabRelativePath'] -and -not [string]::IsNullOrWhiteSpace($driverItem.CabRelativePath)) {
|
|
$modelObject.CabRelativePath = $driverItem.CabRelativePath
|
|
}
|
|
}
|
|
'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 = Join-Path -Path $State.FFUDevelopmentPath -ChildPath "Drivers"
|
|
|
|
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
|
|
$updateExistingLenovo = $true
|
|
}
|
|
if ($updateExistingLenovo) {
|
|
WriteLog "Import-DriversJson: Updated ProductName/MachineType/Id for existing Lenovo model '$($existingModel.Model)'."
|
|
}
|
|
}
|
|
elseif ($makeName -eq 'Dell') {
|
|
# Update Dell extended fields if provided
|
|
if ($importedModelObject.PSObject.Properties['SystemId'] -and $existingModel.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
|
|
if ($existingModel.SystemId -ne $importedModelObject.SystemId) {
|
|
$existingModel.SystemId = $importedModelObject.SystemId
|
|
WriteLog "Import-DriversJson: Updated SystemId for existing Dell model '$($existingModel.Model)'."
|
|
}
|
|
}
|
|
if ($importedModelObject.PSObject.Properties['CabUrl'] -and $existingModel.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.CabUrl)) {
|
|
if ($existingModel.CabUrl -ne $importedModelObject.CabUrl) {
|
|
$existingModel.CabUrl = $importedModelObject.CabUrl
|
|
WriteLog "Import-DriversJson: Updated CabUrl for existing Dell model '$($existingModel.Model)'."
|
|
}
|
|
}
|
|
if ($importedModelObject.PSObject.Properties['CabRelativePath'] -and $existingModel.PSObject.Properties['CabRelativePath'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.CabRelativePath)) {
|
|
if ($existingModel.CabRelativePath -ne $importedModelObject.CabRelativePath) {
|
|
$existingModel.CabRelativePath = $importedModelObject.CabRelativePath
|
|
WriteLog "Import-DriversJson: Updated CabRelativePath for existing Dell 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
|
|
Link = $importedLink
|
|
Id = $importedId
|
|
ProductName = $importedProductName
|
|
MachineType = $importedMachineType
|
|
Version = ""
|
|
Type = ""
|
|
Size = ""
|
|
Arch = ""
|
|
DownloadStatus = "Imported"
|
|
}
|
|
if ($makeName -eq 'Dell') {
|
|
# Attach optional Dell extended fields if present
|
|
if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
|
|
$newDriverModel | Add-Member -NotePropertyName SystemId -NotePropertyValue $importedModelObject.SystemId
|
|
}
|
|
if ($importedModelObject.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.CabUrl)) {
|
|
$newDriverModel | Add-Member -NotePropertyName CabUrl -NotePropertyValue $importedModelObject.CabUrl
|
|
}
|
|
if ($importedModelObject.PSObject.Properties['CabRelativePath'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.CabRelativePath)) {
|
|
$newDriverModel | Add-Member -NotePropertyName CabRelativePath -NotePropertyValue $importedModelObject.CabRelativePath
|
|
}
|
|
}
|
|
$State.Data.allDriverModels.Add($newDriverModel)
|
|
$newModelsAdded++
|
|
WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Sort the full list of models
|
|
$sortedModels = $State.Data.allDriverModels | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model
|
|
|
|
# Create a new list from the sorted results and assign it to the state.
|
|
# This prevents the "ItemsControl inconsistent" error by replacing the source instead of modifying it.
|
|
$newList = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
if ($null -ne $sortedModels) {
|
|
foreach ($model in @($sortedModels)) {
|
|
$newList.Add($model)
|
|
}
|
|
}
|
|
$State.Data.allDriverModels = $newList
|
|
|
|
# Update the UI and apply any existing filter
|
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
|
Search-DriverModels -filterText $State.Controls.txtModelFilter.Text -State $State
|
|
|
|
$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."
|
|
}
|
|
}
|
|
|
|
# Function to handle the 'Get Models' button click logic
|
|
function Invoke-GetModels {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[psobject]$State,
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$Button
|
|
)
|
|
|
|
$selectedMake = $State.Controls.cmbMake.SelectedItem
|
|
$State.Controls.txtStatus.Text = "Getting models for $selectedMake..."
|
|
$State.Window.Cursor = [System.Windows.Input.Cursors]::Wait
|
|
$Button.IsEnabled = $false
|
|
try {
|
|
# Get ALL previously selected models to preserve them, regardless of make.
|
|
$allPreviouslySelectedModels = @($State.Data.allDriverModels | Where-Object { $_.IsSelected })
|
|
|
|
# Get newly fetched models for the current make
|
|
$newlyFetchedStandardizedModels = Get-ModelsForMake -SelectedMake $selectedMake -State $State
|
|
|
|
$combinedModelsList = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
$modelIdentifiersInCombinedList = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
|
|
# Add all previously selected models first to preserve their 'IsSelected' state.
|
|
foreach ($item in $allPreviouslySelectedModels) {
|
|
$combinedModelsList.Add($item)
|
|
$modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)") | Out-Null
|
|
}
|
|
|
|
# Add newly fetched models, but only if they are not already in the list.
|
|
# This prevents overwriting a selected model with an unselected one.
|
|
$addedNewCount = 0
|
|
foreach ($item in $newlyFetchedStandardizedModels) {
|
|
if ($modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)")) {
|
|
$combinedModelsList.Add($item)
|
|
$addedNewCount++
|
|
}
|
|
}
|
|
|
|
# Sort the combined list
|
|
$sortedModels = $combinedModelsList | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model
|
|
|
|
# Create a new list object from the sorted results. This is safer than modifying the existing list
|
|
# that the UI is bound to, which can cause inconsistency errors.
|
|
$newList = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
if ($null -ne $sortedModels) {
|
|
# Sort-Object can return a single object or an array. Ensure it's always treated as a collection.
|
|
foreach ($model in @($sortedModels)) {
|
|
$newList.Add($model)
|
|
}
|
|
}
|
|
$State.Data.allDriverModels = $newList
|
|
|
|
# Update the UI ItemsSource to point to the new list and clear the filter
|
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
|
$State.Controls.txtModelFilter.Text = ""
|
|
|
|
if ($State.Data.allDriverModels.Count -gt 0) {
|
|
$State.Controls.spModelFilterSection.Visibility = 'Visible'
|
|
$State.Controls.lstDriverModels.Visibility = 'Visible'
|
|
$State.Controls.spDriverActionButtons.Visibility = 'Visible'
|
|
$statusText = "Displaying $($State.Data.allDriverModels.Count) models."
|
|
if ($newlyFetchedStandardizedModels.Count -gt 0 -and $addedNewCount -eq 0 -and $allPreviouslySelectedModels.Count -gt 0) {
|
|
$statusText = "Fetched $($newlyFetchedStandardizedModels.Count) models for $selectedMake; all were already in the selected list. Displaying $($State.Data.allDriverModels.Count) total selected models."
|
|
}
|
|
elseif ($addedNewCount -gt 0) {
|
|
$statusText = "Added $addedNewCount new models for $selectedMake. Displaying $($State.Data.allDriverModels.Count) total models."
|
|
}
|
|
elseif ($newlyFetchedStandardizedModels.Count -eq 0 -and $selectedMake -eq 'Lenovo' ) {
|
|
$statusText = if ($allPreviouslySelectedModels.Count -gt 0) { "No new models found for $selectedMake. Displaying $($allPreviouslySelectedModels.Count) previously selected models." } else { "No models found for $selectedMake." }
|
|
}
|
|
elseif ($newlyFetchedStandardizedModels.Count -eq 0) {
|
|
$statusText = "No new models found for $selectedMake. Displaying $($State.Data.allDriverModels.Count) previously selected models."
|
|
}
|
|
$State.Controls.txtStatus.Text = $statusText
|
|
}
|
|
else {
|
|
$State.Controls.spModelFilterSection.Visibility = 'Collapsed'
|
|
$State.Controls.lstDriverModels.Visibility = 'Collapsed'
|
|
$State.Controls.spDriverActionButtons.Visibility = 'Collapsed'
|
|
$State.Controls.txtStatus.Text = "No models to display for $selectedMake."
|
|
}
|
|
}
|
|
catch {
|
|
$State.Controls.txtStatus.Text = "Error getting models: $($_.Exception.Message)"
|
|
[System.Windows.MessageBox]::Show("Error getting models: $($_.Exception.Message)", "Error", "OK", "Error")
|
|
if ($null -eq $State.Data.allDriverModels -or $State.Data.allDriverModels.Count -eq 0) {
|
|
$State.Controls.spModelFilterSection.Visibility = 'Collapsed'
|
|
$State.Controls.lstDriverModels.Visibility = 'Collapsed'
|
|
$State.Controls.spDriverActionButtons.Visibility = 'Collapsed'
|
|
$State.Controls.lstDriverModels.ItemsSource = $null
|
|
$State.Controls.txtModelFilter.Text = ""
|
|
}
|
|
}
|
|
finally {
|
|
$State.Window.Cursor = $null
|
|
$Button.IsEnabled = $true
|
|
}
|
|
}
|
|
|
|
# Function to handle the 'Download Selected Drivers' button click logic
|
|
function Invoke-DownloadSelectedDrivers {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[psobject]$State,
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$Button
|
|
)
|
|
|
|
$selectedDrivers = @($State.Data.allDriverModels | Where-Object { $_.IsSelected })
|
|
if (-not $selectedDrivers) {
|
|
[System.Windows.MessageBox]::Show("No drivers selected to download.", "Download Drivers", "OK", "Information")
|
|
return
|
|
}
|
|
|
|
$Button.IsEnabled = $false
|
|
$State.Controls.pbOverallProgress.Visibility = 'Visible'
|
|
$State.Controls.pbOverallProgress.Value = 0
|
|
$State.Controls.txtStatus.Text = "Preparing driver downloads..."
|
|
|
|
# Define common necessary task-specific variables locally
|
|
# Ensure required selections are made
|
|
if ($null -eq $State.Controls.cmbWindowsRelease.SelectedItem) {
|
|
[System.Windows.MessageBox]::Show("Please select a Windows Release.", "Missing Information", "OK", "Warning")
|
|
$Button.IsEnabled = $true
|
|
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
|
$State.Controls.txtStatus.Text = "Driver download cancelled."
|
|
return
|
|
}
|
|
if ($null -eq $State.Controls.cmbWindowsArch.SelectedItem) {
|
|
[System.Windows.MessageBox]::Show("Please select a Windows Architecture.", "Missing Information", "OK", "Warning")
|
|
$Button.IsEnabled = $true
|
|
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
|
$State.Controls.txtStatus.Text = "Driver download cancelled."
|
|
return
|
|
}
|
|
if (($selectedDrivers | Where-Object { $_.Make -eq 'HP' }) -and $null -ne $State.Controls.cmbWindowsVersion -and $null -eq $State.Controls.cmbWindowsVersion.SelectedItem) {
|
|
[System.Windows.MessageBox]::Show("HP drivers are selected. Please select a Windows Version.", "Missing Information", "OK", "Warning")
|
|
$Button.IsEnabled = $true
|
|
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
|
$State.Controls.txtStatus.Text = "Driver download cancelled."
|
|
return
|
|
}
|
|
|
|
$localDriversFolder = $State.Controls.txtDriversFolder.Text
|
|
$localWindowsRelease = $State.Controls.cmbWindowsRelease.SelectedItem.Value
|
|
$localWindowsArch = $State.Controls.cmbWindowsArch.SelectedItem
|
|
$localWindowsVersion = if ($null -ne $State.Controls.cmbWindowsVersion -and $null -ne $State.Controls.cmbWindowsVersion.SelectedItem) { $State.Controls.cmbWindowsVersion.SelectedItem } else { $null }
|
|
$coreStaticVars = Get-CoreStaticVariables
|
|
$localHeaders = $coreStaticVars.Headers
|
|
$localUserAgent = $coreStaticVars.UserAgent
|
|
$compressDrivers = $State.Controls.chkCompressDriversToWIM.IsChecked
|
|
# Determine if we must preserve source folders (used later for PE driver harvesting)
|
|
$preserveSource = ($State.Controls.chkUseDriversAsPEDrivers.IsChecked -and $State.Controls.chkCompressDriversToWIM.IsChecked)
|
|
|
|
$State.Controls.txtStatus.Text = "Processing all selected drivers..."
|
|
WriteLog "Processing all selected drivers: $($selectedDrivers.Model -join ', ')"
|
|
|
|
# Pre-process Dell Catalog if needed, so it's not done in parallel
|
|
if ($selectedDrivers | Where-Object { $_.Make -eq 'Dell' }) {
|
|
WriteLog "Dell drivers selected. Ensuring Dell Catalog is up-to-date..."
|
|
try {
|
|
$dellDriversFolder = Join-Path -Path $localDriversFolder -ChildPath "Dell"
|
|
$catalogBaseName = if ($localWindowsRelease -le 11) { "CatalogPC" } else { "Catalog" }
|
|
$dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab"
|
|
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
|
|
$catalogUrl = if ($localWindowsRelease -le 11) { "http://downloads.dell.com/catalog/CatalogPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" }
|
|
|
|
$downloadCatalog = $true
|
|
if (Test-Path -Path $dellCatalogXML -PathType Leaf) {
|
|
if (((Get-Date) - (Get-Item $dellCatalogXML).CreationTime).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 "Downloading and extracting Dell Catalog for driver download process..."
|
|
if (-not (Test-Path -Path $dellDriversFolder -PathType Container)) {
|
|
New-Item -Path $dellDriversFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
if (Test-Path -Path $dellCabFile) { Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue }
|
|
if (Test-Path -Path $dellCatalogXML) { Remove-Item -Path $dellCatalogXML -Force -ErrorAction SilentlyContinue }
|
|
|
|
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $dellCabFile
|
|
Invoke-Process -FilePath "Expand.exe" -ArgumentList """$dellCabFile"" ""$dellCatalogXML""" | Out-Null
|
|
Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue
|
|
WriteLog "Dell Catalog prepared successfully."
|
|
}
|
|
}
|
|
catch {
|
|
$errorMessage = "Failed to prepare Dell Catalog: $($_.Exception.Message)"
|
|
WriteLog $errorMessage
|
|
[System.Windows.MessageBox]::Show($errorMessage, "Dell Catalog Error", "OK", "Error")
|
|
$Button.IsEnabled = $true
|
|
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
|
$State.Controls.txtStatus.Text = "Driver download cancelled due to Dell Catalog error."
|
|
return
|
|
}
|
|
}
|
|
$preserveSource = ($State.Controls.chkUseDriversAsPEDrivers.IsChecked -and $State.Controls.chkCompressDriversToWIM.IsChecked)
|
|
$taskArguments = @{
|
|
DriversFolder = $localDriversFolder
|
|
WindowsRelease = $localWindowsRelease
|
|
WindowsArch = $localWindowsArch
|
|
WindowsVersion = $localWindowsVersion
|
|
Headers = $localHeaders
|
|
UserAgent = $localUserAgent
|
|
CompressToWim = $compressDrivers
|
|
PreserveSourceOnCompress = $preserveSource
|
|
}
|
|
|
|
$parallelResults = Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers `
|
|
-ListViewControl $State.Controls.lstDriverModels `
|
|
-IdentifierProperty 'Model' `
|
|
-StatusProperty 'DownloadStatus' `
|
|
-TaskType 'DownloadDriverByMake' `
|
|
-TaskArguments $taskArguments `
|
|
-CompletedStatusText 'Completed' `
|
|
-ErrorStatusPrefix 'Error: ' `
|
|
-WindowObject $State.Window `
|
|
-MainThreadLogPath $State.LogFilePath `
|
|
-ThrottleLimit $State.Controls.txtThreads.Text
|
|
|
|
$overallSuccess = $true
|
|
$successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
|
|
# Check the results from the parallel processing tasks
|
|
if ($null -ne $parallelResults) {
|
|
# Create a lookup from the original selected drivers to get the 'Make' property,
|
|
# as the result object might only have 'Identifier' or 'Model'.
|
|
$makeLookup = @{}
|
|
$selectedDrivers | ForEach-Object { $makeLookup[$_.Model] = $_.Make }
|
|
|
|
# Filter for objects that could be results, avoiding stray log strings
|
|
foreach ($result in ($parallelResults | Where-Object { $_ -is [hashtable] })) {
|
|
if ($null -eq $result) { continue }
|
|
|
|
# The result from Invoke-ParallelProcessing is a hashtable.
|
|
# Access properties using their keys.
|
|
$modelName = $result['Identifier']
|
|
$resultCode = $result['ResultCode']
|
|
$driverPath = $result['DriverPath']
|
|
|
|
if ([string]::IsNullOrWhiteSpace($modelName)) {
|
|
WriteLog "Could not determine model name from result object: $($result | ConvertTo-Json -Compress -Depth 3)"
|
|
$overallSuccess = $false
|
|
continue
|
|
}
|
|
|
|
if ($resultCode -ne 0) {
|
|
$overallSuccess = $false
|
|
WriteLog "Error detected for model $modelName."
|
|
}
|
|
elseif (-not [string]::IsNullOrWhiteSpace($driverPath)) {
|
|
# The task was successful and returned a driver path.
|
|
$make = $makeLookup[$modelName]
|
|
if ($make) {
|
|
$successfullyDownloaded.Add([PSCustomObject]@{
|
|
Make = $make
|
|
Model = $modelName
|
|
DriverPath = $driverPath
|
|
})
|
|
}
|
|
else {
|
|
WriteLog "Warning: Could not find 'Make' for successful download of model '$modelName'. Skipping from DriverMapping.json."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Update the driver mapping JSON if there are any successful downloads
|
|
if ($successfullyDownloaded.Count -gt 0) {
|
|
try {
|
|
WriteLog "Updating DriverMapping.json with $($successfullyDownloaded.Count) successfully downloaded drivers."
|
|
Update-DriverMappingJson -DownloadedDrivers $successfullyDownloaded -DriversFolder $localDriversFolder
|
|
}
|
|
catch {
|
|
WriteLog "Failed to update DriverMapping.json: $($_.Exception.Message)"
|
|
# This is not a fatal error for the download process itself, so just show a warning.
|
|
[System.Windows.MessageBox]::Show("The driver download process completed, but failed to update the DriverMapping.json file. Please check the log for details.", "Driver Mapping Error", "OK", "Warning")
|
|
}
|
|
}
|
|
|
|
# Automatically save the selected drivers to the specified Drivers.json path
|
|
$driversJsonPath = $State.Controls.txtDriversJsonPath.Text
|
|
if (-not [string]::IsNullOrWhiteSpace($driversJsonPath) -and $selectedDrivers.Count -gt 0) {
|
|
WriteLog "Attempting to automatically save selected drivers list to $driversJsonPath"
|
|
try {
|
|
$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
|
|
}
|
|
if ($driverItem.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driverItem.SystemId)) {
|
|
$modelObject.SystemId = $driverItem.SystemId
|
|
}
|
|
if ($driverItem.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($driverItem.CabUrl)) {
|
|
$modelObject.CabUrl = $driverItem.CabUrl
|
|
}
|
|
if ($driverItem.PSObject.Properties['CabRelativePath'] -and -not [string]::IsNullOrWhiteSpace($driverItem.CabRelativePath)) {
|
|
$modelObject.CabRelativePath = $driverItem.CabRelativePath
|
|
}
|
|
}
|
|
'HP' {
|
|
$modelObject = @{
|
|
Name = $driverItem.Model
|
|
}
|
|
}
|
|
'Lenovo' {
|
|
$modelObject = @{
|
|
Name = $driverItem.Model
|
|
ProductName = $driverItem.ProductName
|
|
MachineType = $driverItem.MachineType
|
|
}
|
|
}
|
|
default {
|
|
WriteLog "Auto-Save Drivers.json: Unrecognized Make '$makeName' for driver '$($driverItem.Model)'. Skipping."
|
|
}
|
|
}
|
|
if ($null -ne $modelObject) {
|
|
$modelsForThisMake += $modelObject
|
|
}
|
|
}
|
|
# Add the models array to the make-specific object
|
|
$outputJson[$makeName] = @{ Models = $modelsForThisMake }
|
|
}
|
|
|
|
# Ensure directory exists
|
|
$parentDir = Split-Path -Path $driversJsonPath -Parent
|
|
if (-not (Test-Path -Path $parentDir -PathType Container)) {
|
|
WriteLog "Creating directory for Drivers.json: $parentDir"
|
|
New-Item -Path $parentDir -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
$outputJson | ConvertTo-Json -Depth 5 | Set-Content -Path $driversJsonPath -Encoding UTF8
|
|
WriteLog "Successfully auto-saved selected drivers to $driversJsonPath"
|
|
}
|
|
catch {
|
|
WriteLog "Failed to automatically save selected drivers to $driversJsonPath. Error: $($_.Exception.Message)"
|
|
# This is a best-effort operation, so we only log the error and don't bother the user with a popup.
|
|
}
|
|
}
|
|
|
|
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
|
$Button.IsEnabled = $true
|
|
if ($overallSuccess) {
|
|
$State.Controls.txtStatus.Text = "All selected driver downloads processed."
|
|
[System.Windows.MessageBox]::Show("All selected driver downloads processed. Check status column for details.", "Download Process Finished", "OK", "Information")
|
|
}
|
|
else {
|
|
$State.Controls.txtStatus.Text = "Driver downloads processed with some errors. Check status column and log."
|
|
[System.Windows.MessageBox]::Show("Driver downloads processed, but some errors occurred. Please check the status column for each driver and the log file for details.", "Download Process Finished with Errors", "OK", "Warning")
|
|
}
|
|
}
|
|
|
|
Export-ModuleMember -Function * |