mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
359 lines
19 KiB
PowerShell
359 lines
19 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Provides functions for discovering, downloading, and processing Dell device drivers.
|
||
.DESCRIPTION
|
||
This module contains the logic specific to handling Dell drivers for the FFU Builder UI. It includes functions to parse Dell's large XML driver catalog to retrieve a list of supported models (Get-DellDriversModelList). It also provides a parallel-capable task function (Save-DellDriversTask) that finds, downloads, extracts, and optionally compresses all the latest driver packages for a specified Dell model and operating system.
|
||
#>
|
||
|
||
# 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,
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$Make
|
||
)
|
||
|
||
# Client pathway (<=11) uses CatalogIndexPC to build full Brand Model (SystemID) strings.
|
||
if ($WindowsRelease -le 11) {
|
||
$dellModels = Get-DellClientModels -CatalogIndexXmlPath (Get-DellCatalogIndex -DriversFolder $DriversFolder)
|
||
$final = [System.Collections.Generic.List[pscustomobject]]::new()
|
||
foreach ($m in $dellModels) {
|
||
$final.Add([pscustomobject]@{
|
||
Make = $Make
|
||
Model = $m.ModelDisplay
|
||
Brand = $m.Brand
|
||
ModelNumber = $m.ModelNumber
|
||
SystemId = $m.SystemId
|
||
CabRelativePath = $m.CabRelativePath
|
||
CabUrl = $m.CabUrl
|
||
})
|
||
}
|
||
return $final
|
||
}
|
||
|
||
# Server pathway (unchanged – still uses Catalog.cab)
|
||
$dellDriversFolder = Join-Path -Path $DriversFolder -ChildPath "Dell"
|
||
$catalogBaseName = "Catalog"
|
||
$dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab"
|
||
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
|
||
$catalogUrl = "https://downloads.dell.com/catalog/Catalog.cab"
|
||
|
||
if (-not (Test-Path -Path $dellDriversFolder)) {
|
||
New-Item -Path $dellDriversFolder -ItemType Directory -Force | Out-Null
|
||
}
|
||
|
||
$download = $true
|
||
if (Test-Path -Path $dellCatalogXML) {
|
||
if (((Get-Date) - (Get-Item $dellCatalogXML).CreationTime).TotalDays -lt 7) {
|
||
$download = $false
|
||
}
|
||
}
|
||
|
||
if ($download) {
|
||
if (Test-Path $dellCabFile) { Remove-Item $dellCabFile -Force -ErrorAction SilentlyContinue }
|
||
if (Test-Path $dellCatalogXML) { Remove-Item $dellCatalogXML -Force -ErrorAction SilentlyContinue }
|
||
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $dellCabFile
|
||
Invoke-Process -FilePath Expand.exe -ArgumentList """$dellCabFile"" ""$dellCatalogXML""" | Out-Null
|
||
Remove-Item $dellCabFile -Force -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
if (-not (Test-Path $dellCatalogXML)) { throw "Dell server catalog XML missing: $dellCatalogXML" }
|
||
|
||
$settings = New-Object System.Xml.XmlReaderSettings
|
||
$settings.IgnoreWhitespace = $true
|
||
$settings.IgnoreComments = $true
|
||
$reader = [System.Xml.XmlReader]::Create($dellCatalogXML,$settings)
|
||
$inDriver = $false
|
||
$inModel = $false
|
||
$depthModel = -1
|
||
$modelsHash = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||
try {
|
||
while ($reader.Read()) {
|
||
switch ($reader.NodeType) {
|
||
([System.Xml.XmlNodeType]::Element) {
|
||
switch ($reader.Name) {
|
||
'SoftwareComponent' { $inDriver = $false }
|
||
'ComponentType' { if ($reader.GetAttribute('value') -eq 'DRVR') { $inDriver = $true } }
|
||
'Model' { if ($inDriver) { $inModel = $true; $depthModel = $reader.Depth } }
|
||
}
|
||
}
|
||
([System.Xml.XmlNodeType]::CDATA) {
|
||
if ($inDriver -and $inModel) {
|
||
$val = $reader.Value.Trim()
|
||
if ($val) { $modelsHash.Add($val) | Out-Null }
|
||
$inModel = $false
|
||
}
|
||
}
|
||
([System.Xml.XmlNodeType]::EndElement) {
|
||
if ($reader.Name -eq 'SoftwareComponent') { $inDriver = $false; $inModel = $false }
|
||
elseif ($reader.Name -eq 'Model' -and $reader.Depth -eq $depthModel) { $inModel = $false; $depthModel = -1 }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
finally {
|
||
$reader.Dispose()
|
||
}
|
||
|
||
$out = [System.Collections.Generic.List[pscustomobject]]::new()
|
||
foreach ($nm in ($modelsHash | Sort-Object)) {
|
||
$out.Add([pscustomobject]@{ Make = $Make; Model = $nm })
|
||
}
|
||
return $out
|
||
}
|
||
|
||
# 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,
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$DriversFolder,
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$WindowsArch,
|
||
[Parameter(Mandatory = $true)]
|
||
[int]$WindowsRelease,
|
||
[Parameter()]
|
||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null,
|
||
[Parameter()]
|
||
[bool]$CompressToWim = $false,
|
||
[Parameter()]
|
||
[bool]$PreserveSourceOnCompress = $false
|
||
)
|
||
|
||
$modelDisplay = $DriverItemData.Model
|
||
$make = 'Dell'
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Checking...' }
|
||
|
||
$sanitizedModelName = ConvertTo-SafeName -Name $modelDisplay
|
||
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make
|
||
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
|
||
$driverRelativePath = Join-Path -Path $make -ChildPath $sanitizedModelName
|
||
|
||
# Helper: safe folder removal
|
||
function Remove-SafeFolder {
|
||
param([string]$Path)
|
||
if ([string]::IsNullOrWhiteSpace($Path)) { return }
|
||
# Never allow deleting the entire Dell root folder accidentally
|
||
$dellRoot = (Resolve-Path $makeDriversPath).ProviderPath
|
||
$target = (Resolve-Path $Path -ErrorAction SilentlyContinue)?.ProviderPath
|
||
if ($null -eq $target) { return }
|
||
if ($target -eq $dellRoot) { return }
|
||
if (-not ($target.StartsWith($dellRoot,[System.StringComparison]::OrdinalIgnoreCase))) { return }
|
||
Remove-Item -Path $target -Recurse -Force -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
try {
|
||
# Existing drivers short‑circuit
|
||
$existing = Test-ExistingDriver -Make $make -Model $sanitizedModelName -DriversFolder $DriversFolder -Identifier $modelDisplay -ProgressQueue $ProgressQueue
|
||
if ($existing) {
|
||
if (-not $existing.PSObject.Properties['Model']) {
|
||
$existing | Add-Member -MemberType NoteProperty -Name 'Model' -Value $modelDisplay
|
||
}
|
||
if ($CompressToWim -and $existing.Status -eq 'Already downloaded') {
|
||
$wimPath = Join-Path $makeDriversPath "$sanitizedModelName.wim"
|
||
$srcPath = Join-Path $makeDriversPath $sanitizedModelName
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Compressing existing...' }
|
||
try {
|
||
Compress-DriverFolderToWim -SourceFolderPath $srcPath -DestinationWimPath $wimPath -WimName $modelDisplay -WimDescription "Drivers for $modelDisplay" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||
$existing.Status = 'Already downloaded & Compressed'
|
||
$existing.DriverPath = Join-Path $make "$sanitizedModelName.wim"
|
||
$existing.Success = $true
|
||
}
|
||
catch {
|
||
WriteLog "Compression failed for $($modelDisplay): $($_.Exception.Message)"
|
||
$existing.Status = 'Already downloaded (Compression failed)'
|
||
}
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $existing.Status }
|
||
}
|
||
return $existing
|
||
}
|
||
|
||
if (-not (Test-Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
|
||
if (-not (Test-Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
|
||
|
||
$packages = @()
|
||
|
||
if ($WindowsRelease -le 11) {
|
||
$cabUrl = $DriverItemData.CabUrl
|
||
if ([string]::IsNullOrWhiteSpace($cabUrl)) {
|
||
WriteLog "CabUrl missing for '$modelDisplay' – resolving via CatalogIndexPC."
|
||
$resolved = Resolve-DellCabUrlFromModel -DriversFolder $DriversFolder -ModelDisplay $modelDisplay
|
||
if ($null -eq $resolved -or [string]::IsNullOrWhiteSpace($resolved.CabUrl)) {
|
||
throw "Unable to resolve CabUrl for $modelDisplay from CatalogIndexPC."
|
||
}
|
||
$cabUrl = $resolved.CabUrl
|
||
# Optionally persist back into the incoming object if property exists
|
||
if ($DriverItemData.PSObject.Properties['CabUrl']) {
|
||
$DriverItemData.CabUrl = $cabUrl
|
||
}
|
||
}
|
||
|
||
# Model-based workflow (always used for client pathway now)
|
||
$modelCabName = [IO.Path]::GetFileName($cabUrl)
|
||
if ([string]::IsNullOrWhiteSpace($modelCabName)) { throw "Derived model cab name empty for $modelDisplay" }
|
||
$modelCabPath = Join-Path $makeDriversPath $modelCabName
|
||
$modelXmlPath = Join-Path $makeDriversPath ([IO.Path]::GetFileNameWithoutExtension($modelCabName) + '.xml')
|
||
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Downloading catalog...' }
|
||
if (Test-Path $modelCabPath) { Remove-SafeFolder $modelCabPath }
|
||
if (Test-Path $modelXmlPath) { Remove-SafeFolder $modelXmlPath }
|
||
|
||
WriteLog "Downloading Dell model catalog from $cabUrl to $modelCabPath"
|
||
Start-BitsTransferWithRetry -Source $cabUrl -Destination $modelCabPath
|
||
Invoke-Process -FilePath Expand.exe -ArgumentList """$modelCabPath"" ""$modelXmlPath""" | Out-Null
|
||
Remove-Item $modelCabPath -Force -ErrorAction SilentlyContinue
|
||
if (-not (Test-Path $modelXmlPath)) { throw "Model XML not found after extraction: $modelXmlPath" }
|
||
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Selecting latest drivers...' }
|
||
$packages = Get-DellLatestDriverPackages -ModelXmlPath $modelXmlPath -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease
|
||
}
|
||
else {
|
||
# Server legacy logic unchanged (kept as before)
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Preparing server catalog...' }
|
||
$catalogCab = Join-Path $makeDriversPath 'Catalog.cab'
|
||
$catalogXml = Join-Path $makeDriversPath 'Catalog.xml'
|
||
$catalogUrl = 'https://downloads.dell.com/catalog/Catalog.cab'
|
||
$need = $true
|
||
if (Test-Path $catalogXml) {
|
||
if (((Get-Date) - (Get-Item $catalogXml).CreationTime).TotalDays -lt 7) { $need = $false }
|
||
}
|
||
if ($need) {
|
||
if (Test-Path $catalogCab) { Remove-SafeFolder $catalogCab }
|
||
if (Test-Path $catalogXml) { Remove-SafeFolder $catalogXml }
|
||
WriteLog "Downloading Dell server catalog from $catalogUrl to $catalogCab"
|
||
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $catalogCab
|
||
Invoke-Process -FilePath Expand.exe -ArgumentList """$catalogCab"" ""$catalogXml""" | Out-Null
|
||
Remove-Item $catalogCab -Force -ErrorAction SilentlyContinue
|
||
}
|
||
if (-not (Test-Path $catalogXml)) { throw "Server catalog XML missing: $catalogXml" }
|
||
|
||
[xml]$xmlContent = Get-Content -Path $catalogXml -Raw
|
||
$baseLocation = "https://$($xmlContent.manifest.baseLocation)/"
|
||
$softwareComponents = $xmlContent.Manifest.SoftwareComponent | Where-Object { $_.ComponentType.value -eq 'DRVR' }
|
||
$latestDrivers = @{}
|
||
foreach ($component in $softwareComponents) {
|
||
$models = $component.SupportedSystems.Brand.Model
|
||
foreach ($m in $models) {
|
||
if ($m.Display.'#cdata-section' -eq $modelDisplay) {
|
||
$validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { $_.osArch -eq $WindowsArch }
|
||
if (-not $validOS) { continue }
|
||
$driverPath = $component.path
|
||
$downloadUrl = $baseLocation + $driverPath
|
||
$fileName = [IO.Path]::GetFileName($driverPath)
|
||
$name = $component.Name.Display.'#cdata-section' -replace '[\\\/\:\*\?\"\<\>\| ]','_' -replace '[\,]','-'
|
||
$category = $component.Category.Display.'#cdata-section' -replace '[\\\/\:\*\?\"\<\>\| ]','_'
|
||
$version = [version]$component.vendorVersion
|
||
$namePrefix = ($name -split '-')[0]
|
||
if (-not $latestDrivers[$category]) { $latestDrivers[$category] = @{} }
|
||
if (-not $latestDrivers[$category][$namePrefix] -or $latestDrivers[$category][$namePrefix].Version -lt $version) {
|
||
$latestDrivers[$category][$namePrefix] = [pscustomobject]@{
|
||
Name = $name
|
||
DownloadUrl = $downloadUrl
|
||
DriverFileName = $fileName
|
||
Version = $version
|
||
Category = $category
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
foreach ($cat in $latestDrivers.Keys) { foreach ($drv in $latestDrivers[$cat].Values) { $packages += $drv } }
|
||
}
|
||
|
||
if (-not $packages -or $packages.Count -eq 0) {
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'No drivers found for OS' }
|
||
return [pscustomobject]@{ Model = $modelDisplay; Status = 'No drivers found for OS'; Success = $true; DriverPath = $driverRelativePath }
|
||
}
|
||
|
||
$total = $packages.Count
|
||
$idx = 0
|
||
foreach ($pkg in $packages) {
|
||
$idx++
|
||
$driverName = $pkg.Name
|
||
if ([string]::IsNullOrWhiteSpace($driverName)) { $driverName = $pkg.DriverFileName }
|
||
$status = "$idx/$total Downloading $driverName"
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $status }
|
||
|
||
$categorySafe = ($pkg.Category -replace '[\\\/\:\*\?\"\<\>\| ]','_')
|
||
$downloadFolder = Join-Path $modelPath $categorySafe
|
||
if (-not (Test-Path $downloadFolder)) { New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null }
|
||
$driverFilePath = Join-Path $downloadFolder $pkg.DriverFileName
|
||
$plainName = [IO.Path]::GetFileNameWithoutExtension($pkg.DriverFileName)
|
||
if ([string]::IsNullOrWhiteSpace($plainName)) { $plainName = "_extract" }
|
||
$extractFolder = Join-Path $downloadFolder $plainName
|
||
|
||
if (Test-Path $extractFolder) {
|
||
$sz = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||
if ($sz -gt 1KB) { continue }
|
||
}
|
||
|
||
if (-not (Test-Path $driverFilePath)) {
|
||
WriteLog "$status URL: $($pkg.DownloadUrl)"
|
||
try { Start-BitsTransferWithRetry -Source $pkg.DownloadUrl -Destination $driverFilePath }
|
||
catch { WriteLog "Download failed: $($pkg.DownloadUrl) $($_.Exception.Message)"; continue }
|
||
}
|
||
|
||
$status = "$idx/$total Extracting $driverName"
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $status }
|
||
|
||
if (-not (Test-Path $extractFolder)) { New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null }
|
||
|
||
$arg1 = "/s /e=`"$extractFolder`" /l=`"$extractFolder\log.log`""
|
||
$arg2 = "/s /drivers=`"$extractFolder`" /l=`"$extractFolder\log.log`""
|
||
$ok = $false
|
||
try {
|
||
Invoke-Process -FilePath $driverFilePath -ArgumentList $arg1 | Out-Null
|
||
$sz = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||
if ($sz -gt 1KB) { $ok = $true }
|
||
if (-not $ok) {
|
||
Remove-SafeFolder $extractFolder
|
||
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
|
||
Invoke-Process -FilePath $driverFilePath -ArgumentList $arg2 | Out-Null
|
||
$sz = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||
if ($sz -gt 1KB) { $ok = $true }
|
||
}
|
||
}
|
||
catch {
|
||
WriteLog "Extraction error: $($_.Exception.Message)"
|
||
}
|
||
|
||
if ($ok) {
|
||
Remove-Item $driverFilePath -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
if ($CompressToWim) {
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Compressing...' }
|
||
$wimPath = Join-Path $makeDriversPath "$sanitizedModelName.wim"
|
||
try {
|
||
Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $wimPath -WimName $modelDisplay -WimDescription $modelDisplay -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||
$driverRelativePath = Join-Path $make "$sanitizedModelName.wim"
|
||
$statusFinal = 'Completed & Compressed'
|
||
}
|
||
catch {
|
||
WriteLog "Compression failed for $($modelDisplay): $($_.Exception.Message)"
|
||
$statusFinal = 'Completed (Compression Failed)'
|
||
}
|
||
}
|
||
else {
|
||
$statusFinal = 'Completed'
|
||
}
|
||
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $statusFinal }
|
||
return [pscustomobject]@{ Model = $modelDisplay; Status = $statusFinal; Success = $true; DriverPath = $driverRelativePath }
|
||
}
|
||
catch {
|
||
$err = "Error: $($_.Exception.Message.Split('.')[0])"
|
||
WriteLog "Save-DellDriversTask error for $($modelDisplay): $($_.Exception.ToString())"
|
||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $err }
|
||
return [pscustomobject]@{ Model = $modelDisplay; Status = $err; Success = $false; DriverPath = $null }
|
||
}
|
||
}
|
||
|
||
Export-ModuleMember -Function * |