diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index f19a81e..66334e0 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1428,128 +1428,121 @@ function Get-LenovoDrivers { } function Get-DellDrivers { - param ( + param( [Parameter(Mandatory = $true)] [string]$Model, [Parameter(Mandatory = $true)] - [ValidateSet("x64", "x86", "ARM64")] + [ValidateSet('x64','x86','ARM64')] [string]$WindowsArch, [Parameter(Mandatory = $true)] [int]$WindowsRelease ) if (-not (Test-Path -Path $DriversFolder)) { - WriteLog "Creating Drivers folder: $DriversFolder" New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null - WriteLog "Drivers folder created" + } + $DriversFolder = Join-Path $DriversFolder $Make + if (-not (Test-Path $DriversFolder)) { + New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null } - $DriversFolder = "$DriversFolder\$Make" - WriteLog "Creating Dell Drivers folder: $DriversFolder" - New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null - WriteLog "Dell Drivers folder created" - - #CatalogPC.cab is the catalog for Windows client PCs, Catalog.cab is the catalog for Windows Server + # Client pathway (<=11): use CatalogIndexPC + per-model cab. if ($WindowsRelease -le 11) { - $catalogUrl = "http://downloads.dell.com/catalog/CatalogPC.cab" - $DellCabFile = "$DriversFolder\CatalogPC.cab" - $DellCatalogXML = "$DriversFolder\CatalogPC.XML" - } - else { - $catalogUrl = "https://downloads.dell.com/catalog/Catalog.cab" - $DellCabFile = "$DriversFolder\Catalog.cab" - $DellCatalogXML = "$DriversFolder\Catalog.xml" - } - - if (-not (Test-Url -Url $catalogUrl)) { - WriteLog "Dell Catalog cab URL is not accessible: $catalogUrl Exiting" - if ($VerbosePreference -ne 'Continue') { - Write-Host "Dell Catalog cab URL is not accessible: $catalogUrl Exiting" + $indexXml = Get-DellCatalogIndex -DriversFolder (Split-Path $DriversFolder -Parent) + $allModels = Get-DellClientModels -CatalogIndexXmlPath $indexXml + $target = $allModels | Where-Object { $_.ModelDisplay -eq $Model } + if (-not $target) { throw "Requested Dell model '$Model' not found in index." } + + $cabUrl = $target.CabUrl + $modelCabName = [IO.Path]::GetFileName($cabUrl) + $modelCabPath = Join-Path $DriversFolder $modelCabName + $modelXmlPath = Join-Path $DriversFolder ($modelCabName -replace '\.cab$','.xml') + + if (Test-Path $modelCabPath) { Remove-Item $modelCabPath -Force -ErrorAction SilentlyContinue } + if (Test-Path $modelXmlPath) { Remove-Item $modelXmlPath -Force -ErrorAction SilentlyContinue } + + 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 "Failed to extract model cab XML: $modelXmlPath" } + + $pkgs = Get-DellLatestDriverPackages -ModelXmlPath $modelXmlPath -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease + if (-not $pkgs) { + WriteLog "No drivers found for '$Model'." + return } - exit + + foreach ($pkg in $pkgs) { + $categorySafe = ($pkg.Category -replace '[\\\/\:\*\?\"\<\>\| ]','_') + $downloadFolder = Join-Path $DriversFolder (Join-Path $Model $categorySafe) + if (-not (Test-Path $downloadFolder)) { New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null } + $driverFilePath = Join-Path $downloadFolder $pkg.DriverFileName + $extractFolder = Join-Path $downloadFolder ($pkg.DriverFileName.TrimEnd($pkg.DriverFileName[-4..-1])) + + 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)) { + try { Start-BitsTransferWithRetry -Source $pkg.DownloadUrl -Destination $driverFilePath } + catch { WriteLog "Download failed: $($pkg.DownloadUrl) $($_.Exception.Message)"; continue } + } + + 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-Item $extractFolder -Recurse -Force -ErrorAction SilentlyContinue + 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 } + } + return } - WriteLog "Downloading Dell Catalog cab file: $catalogUrl to $DellCabFile" - Start-BitsTransferWithRetry -Source $catalogUrl -Destination $DellCabFile - WriteLog "Dell Catalog cab file downloaded" + # Server pathway (unchanged legacy) + $catalogUrl = "https://downloads.dell.com/catalog/Catalog.cab" + $DellCabFile = Join-Path $DriversFolder 'Catalog.cab' + $DellCatalogXML = Join-Path $DriversFolder 'Catalog.xml' - WriteLog "Extracting Dell Catalog cab file to $DellCatalogXML" + Start-BitsTransferWithRetry -Source $catalogUrl -Destination $DellCabFile Invoke-Process -FilePath Expand.exe -ArgumentList "$DellCabFile $DellCatalogXML" | Out-Null - WriteLog "Dell Catalog cab file extracted" $xmlContent = [xml](Get-Content -Path $DellCatalogXML) $baseLocation = "https://" + $xmlContent.manifest.baseLocation + "/" $latestDrivers = @{} + $softwareComponents = $xmlContent.Manifest.SoftwareComponent | Where-Object { $_.ComponentType.value -eq 'DRVR' } - $softwareComponents = $xmlContent.Manifest.SoftwareComponent | Where-Object { $_.ComponentType.value -eq "DRVR" } foreach ($component in $softwareComponents) { $models = $component.SupportedSystems.Brand.Model foreach ($item in $models) { if ($item.Display.'#cdata-section' -match $Model) { - - if ($WindowsRelease -le 11) { - $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { $_.osArch -eq $WindowsArch } - } - elseif ($WindowsRelease -eq 2016) { - $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W14") } - } - elseif ($WindowsRelease -eq 2019) { - $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W19") } - } - elseif ($WindowsRelease -eq 2022) { - $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W22") } - } - elseif ($WindowsRelease -eq 2025) { - $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W25") } - } - else { - $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match "W22") } - } - - if ($validOS) { - $driverPath = $component.path - $downloadUrl = $baseLocation + $driverPath - $driverFileName = [System.IO.Path]::GetFileName($driverPath) - $name = $component.Name.Display.'#cdata-section' - $name = $name -replace '[\\\/\:\*\?\"\<\>\| ]', '_' - $name = $name -replace '[\,]', '-' - $category = $component.Category.Display.'#cdata-section' - $category = $category -replace '[\\\/\:\*\?\"\<\>\| ]', '_' - $version = [version]$component.vendorVersion - $namePrefix = ($name -split '-')[0] - - # Use hash table to store the latest driver for each category to prevent downloading older driver versions - if ($latestDrivers[$category]) { - if ($latestDrivers[$category][$namePrefix]) { - if ($latestDrivers[$category][$namePrefix].Version -lt $version) { - $latestDrivers[$category][$namePrefix] = [PSCustomObject]@{ - Name = $name; - DownloadUrl = $downloadUrl; - DriverFileName = $driverFileName; - Version = $version; - Category = $category - } - } - } - else { - $latestDrivers[$category][$namePrefix] = [PSCustomObject]@{ - Name = $name; - DownloadUrl = $downloadUrl; - DriverFileName = $driverFileName; - Version = $version; - Category = $category - } - } - } - else { - $latestDrivers[$category] = @{} - $latestDrivers[$category][$namePrefix] = [PSCustomObject]@{ - Name = $name; - DownloadUrl = $downloadUrl; - DriverFileName = $driverFileName; - Version = $version; - Category = $category - } + $validOS = $component.SupportedOperatingSystems.OperatingSystem | Where-Object { $_.osArch -eq $WindowsArch } + if (-not $validOS) { continue } + $driverPath = $component.path + $downloadUrl = $baseLocation + $driverPath + $driverFileName = [System.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 = $driverFileName; Version = $version; Category = $category } } } @@ -1558,102 +1551,15 @@ function Get-DellDrivers { foreach ($category in $latestDrivers.Keys) { foreach ($driver in $latestDrivers[$category].Values) { - $downloadFolder = "$DriversFolder\$Model\$($driver.Category)" - $driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName - - if (Test-Path -Path $driverFilePath) { - WriteLog "Driver already downloaded: $driverFilePath skipping" - continue - } - - WriteLog "Downloading driver: $($driver.Name)" - if (-not (Test-Path -Path $downloadFolder)) { - WriteLog "Creating download folder: $downloadFolder" - New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null - WriteLog "Download folder created" - } - - WriteLog "Downloading driver: $($driver.DownloadUrl) to $driverFilePath" - try { - Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $driverFilePath - Start-BitsTransferWithRetry -Source $driver.DownloadUrl -Destination $driverFilePath - Clear-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $driverFilePath - WriteLog "Driver downloaded" - } - catch { - WriteLog "Failed to download driver: $($driver.DownloadUrl) to $driverFilePath" - continue - } - - - $extractFolder = $downloadFolder + "\" + $driver.DriverFileName.TrimEnd($driver.DriverFileName[-4..-1]) - # WriteLog "Creating extraction folder: $extractFolder" - # New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null - # WriteLog "Extraction folder created" - - # $arguments = "/s /e /f `"$extractFolder`"" + $downloadFolder = Join-Path $DriversFolder (Join-Path $Model $driver.Category) + if (-not (Test-Path $downloadFolder)) { New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null } + $driverFilePath = Join-Path $downloadFolder $driver.DriverFileName + if (Test-Path $driverFilePath) { continue } + Start-BitsTransferWithRetry -Source $driver.DownloadUrl -Destination $driverFilePath + $extractFolder = Join-Path $downloadFolder ($driver.DriverFileName.TrimEnd($driver.DriverFileName[-4..-1])) $arguments = "/s /drivers=`"$extractFolder`"" - WriteLog "Extracting driver: $driverFilePath $arguments" - try { - #If Category is Chipset, must add -wait $false to the Invoke-Process command line to prevent the script from hanging on the Intel chipset driver which leaves a Window open - if ($driver.Category -eq "Chipset") { - $process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false - - #Wait 5 seconds to allow for the extraction process to finish - Start-Sleep -Seconds 5 - - $childProcesses = Get-ChildProcesses $process.Id - - # Find and stop the last created child process - if ($childProcesses) { - $latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1 - Stop-Process -Id $latestProcess.ProcessId -Force - # Sleep 1 second to let process finish exiting so its installer can be removed - Start-Sleep -Seconds 1 - } - #If Category is Network and $isServer is $false, must add -wait $false to the Invoke-Process command line to prevent the script from hanging on the Intel network driver which leaves a Window open - } - elseif ($driver.Category -eq "Network" -and $isServer -eq $false) { - - $process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false - - #Sometimes the network drivers will extract on client OS, wait 5 seconds and check if the process is still running - Start-Sleep -Seconds 5 - if ($process.HasExited -eq $false) { - $childProcesses = Get-ChildProcesses $process.Id - - # Find and stop the last created child process - if ($childProcesses) { - $latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1 - Stop-Process -Id $latestProcess.ProcessId -Force - #Move on to the next driver and skip this one - it won't extract on a client OS even with /s /e switches - continue - } - } - } - else { - Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments | Out-Null - } - # If $extractFolder is empty, try alternative extraction method - if (!(Get-ChildItem -Path $extractFolder -Recurse | Where-Object { -not $_.PSIsContainer })) { - WriteLog 'Extraction with /drivers= switch failed. Removing folder and retrying with /s /e switches' - Remove-Item -Path $extractFolder -Force -Recurse -ErrorAction SilentlyContinue - $arguments = "/s /e=`"$extractFolder`"" - WriteLog "Extracting driver: $driverFilePath $arguments" - Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments | Out-Null - } - } - catch { - WriteLog 'Extraction with /drivers= switch failed. Retrying with /s /e switches' - $arguments = "/s /e=`"$extractFolder`"" - WriteLog "Extracting driver: $driverFilePath $arguments" - Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments | Out-Null - } - WriteLog "Driver extracted" - - WriteLog "Deleting driver file: $driverFilePath" - Remove-Item -Path $driverFilePath -Force - WriteLog "Driver file deleted" + try { Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments | Out-Null } catch {} + Remove-Item $driverFilePath -Force -ErrorAction SilentlyContinue } } } diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Core.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Core.psm1 index 099e720..abb499e 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Core.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Core.psm1 @@ -156,7 +156,7 @@ function Start-BitsTransferWithRetry { $VerbosePreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue' - Start-BitsTransfer -Source $Source -Destination $Destination -ErrorAction Stop + Start-BitsTransfer -Source $Source -Destination $Destination -Priority Normal -ErrorAction Stop $ProgressPreference = $OriginalProgressPreference $VerbosePreference = $OriginalVerbosePreference diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Dell.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Dell.psm1 new file mode 100644 index 0000000..75b8c99 --- /dev/null +++ b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Dell.psm1 @@ -0,0 +1,221 @@ +<# +.SYNOPSIS + Common Dell driver helpers (catalog index, model listing, latest package selection). +#> + +function Convert-DellVendorVersion { + param([Parameter(Mandatory=$true)][string]$VendorVersion) + $segments = $VendorVersion.Split('.') | ForEach-Object { + if ($_ -match '^\d+$') { [int]$_ } else { 0 } + } + return ,$segments +} + +function Compare-DellVendorVersion { + param( + [int[]]$Left, + [int[]]$Right + ) + $len = [Math]::Max($Left.Length,$Right.Length) + for ($i=0; $i -lt $len; $i++) { + $l = if ($i -lt $Left.Length) { $Left[$i] } else { 0 } + $r = if ($i -lt $Right.Length) { $Right[$i] } else { 0 } + if ($l -gt $r) { return 1 } + if ($l -lt $r) { return -1 } + } + return 0 +} + +function Get-DellCatalogIndex { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$DriversFolder + ) + + $dellFolder = Join-Path $DriversFolder 'Dell' + if (-not (Test-Path $dellFolder)) { New-Item -Path $dellFolder -ItemType Directory -Force | Out-Null } + $cabPath = Join-Path $dellFolder 'CatalogIndexPC.cab' + $xmlPath = Join-Path $dellFolder 'CatalogIndexPC.xml' + $url = 'https://downloads.dell.com/catalog/CatalogIndexPC.cab' + + $need = $true + if (Test-Path $xmlPath) { + $ageDays = ((Get-Date) - (Get-Item $xmlPath).CreationTime).TotalDays + if ($ageDays -lt 7) { $need = $false } + } + + if ($need) { + if (Test-Path $cabPath) { Remove-Item $cabPath -Force -ErrorAction SilentlyContinue } + if (Test-Path $xmlPath) { Remove-Item $xmlPath -Force -ErrorAction SilentlyContinue } + Start-BitsTransferWithRetry -Source $url -Destination $cabPath + Invoke-Process -FilePath Expand.exe -ArgumentList """$cabPath"" ""$xmlPath""" | Out-Null + Remove-Item $cabPath -Force -ErrorAction SilentlyContinue + } + + if (-not (Test-Path $xmlPath)) { throw "Dell CatalogIndexPC XML missing: $xmlPath" } + return $xmlPath +} + +function Get-DellClientModels { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$CatalogIndexXmlPath + ) + + $settings = New-Object System.Xml.XmlReaderSettings + $settings.IgnoreWhitespace = $true + $settings.IgnoreComments = $true + $reader = [System.Xml.XmlReader]::Create($CatalogIndexXmlPath,$settings) + + $models = [System.Collections.Generic.List[pscustomobject]]::new() + try { + while ($reader.Read()) { + if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'GroupManifest') { + # Read subtree to pick out brand/model/systemID + path + $sub = $reader.ReadSubtree() + $doc = New-Object System.Xml.XmlDocument + $doc.Load($sub) + $sub.Dispose() + + # Use local-name() to ignore namespaces + $brandNode = $doc.SelectSingleNode("//*[local-name()='SupportedSystems']/*[local-name()='Brand']") + if (-not $brandNode) { continue } + $brandDisplay = ($brandNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim() + $modelNode = $brandNode.SelectSingleNode("*[local-name()='Model']") + if (-not $modelNode) { continue } + $modelNumber = ($modelNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim() + $systemId = $modelNode.GetAttribute('systemID') + $manifestInfo = $doc.SelectSingleNode("//*[local-name()='ManifestInformation']") + if (-not $manifestInfo) { continue } + $pathAttr = $manifestInfo.GetAttribute('path') + if (-not $pathAttr) { continue } + $cabUrl = 'https://downloads.dell.com/' + $pathAttr + $modelDisplay = "$brandDisplay $modelNumber ($systemId)" + $models.Add([pscustomobject]@{ + Brand = $brandDisplay + ModelNumber = $modelNumber + SystemId = $systemId + CabRelativePath = $pathAttr + CabUrl = $cabUrl + ModelDisplay = $modelDisplay + }) + } + } + } + finally { + $reader.Dispose() + } + return $models +} + +function Get-DellLatestDriverPackages { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$ModelXmlPath, + [Parameter(Mandatory=$true)][string]$WindowsArch, + [Parameter(Mandatory=$true)][int]$WindowsRelease + ) + + if (-not (Test-Path $ModelXmlPath)) { throw "Model XML not found: $ModelXmlPath" } + + $xml = [xml](Get-Content -Path $ModelXmlPath -Raw) + + # Collect all SoftwareComponent nodes + $components = $xml.SelectNodes("//*[local-name()='SoftwareComponent']") + if (-not $components) { return @() } + + $rawPackages = [System.Collections.Generic.List[pscustomobject]]::new() + + foreach ($comp in $components) { + $ctype = $comp.SelectSingleNode("*[local-name()='ComponentType']") + if (-not $ctype) { continue } + if ($ctype.GetAttribute('value') -ne 'DRVR') { continue } + + # OS filtering (arch only – release filtering intentionally minimal for now) + $osNodes = @($comp.SelectNodes("*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']")) + if (-not $osNodes) { continue } + $validOS = $osNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1 + if (-not $validOS) { continue } + + $path = $comp.GetAttribute('path') + if (-not $path) { continue } + + $downloadUrl = "https://downloads.dell.com/$path" + $fileName = [IO.Path]::GetFileName($path) + $vendorVersion = $comp.GetAttribute('vendorVersion') + $versionArr = if ($vendorVersion) { Convert-DellVendorVersion $vendorVersion } else { @(0) } + $dateTimeAttr = $comp.GetAttribute('dateTime') + $dt = Get-Date + if ($dateTimeAttr) { + try { $dt = [DateTime]::Parse($dateTimeAttr) } catch { } + } + + $categoryNode = $comp.SelectSingleNode("*[local-name()='Category']/*[local-name()='Display']") + $category = if ($categoryNode) { $categoryNode.InnerText.Trim() } else { 'Uncategorized' } + + # Collect componentIDs (SupportedDevices + SupportedDCHDevices) + $compIds = [System.Collections.Generic.List[string]]::new() + $devNodes = @($comp.SelectNodes(".//*[local-name()='Device']")) + foreach ($dn in $devNodes) { + $id = $dn.GetAttribute('componentID') + if ($id) { [void]$compIds.Add($id) } + } + if ($compIds.Count -eq 0) { continue } + + # Build a deterministic sortable key: zero-pad each numeric segment to 6 digits + $versionSortable = ($versionArr | ForEach-Object { $_.ToString('D6') }) -join '-' + + $rawPackages.Add([pscustomobject]@{ + Path = $path + DownloadUrl = $downloadUrl + FileName = $fileName + Category = $category + VendorVersion = $vendorVersion + VersionArray = $versionArr + VersionSortable = $versionSortable + DateTime = $dt + ComponentIds = $compIds + }) + } + + if ($rawPackages.Count -eq 0) { return @() } + + # Sort newest first by VersionSortable (lexicographic works due to zero padding) then DateTime + $sorted = $rawPackages | Sort-Object -Property @{ Expression = { $_.VersionSortable }; Descending = $true }, @{ Expression = { $_.DateTime }; Descending = $true } + + $chosen = [System.Collections.Generic.List[pscustomobject]]::new() + $assignedIds = [System.Collections.Generic.HashSet[string]]::new() + + foreach ($pkg in $sorted) { + $hasOverlap = $false + foreach ($cid in $pkg.ComponentIds) { + if ($assignedIds.Contains($cid)) { $hasOverlap = $true; break } + } + if ($hasOverlap) { + WriteLog "Get-DellLatestDriverPackages: Skipping superseded package $($pkg.FileName) (shared componentID with newer package)." + continue + } + + foreach ($cid in $pkg.ComponentIds) { [void]$assignedIds.Add($cid) } + + $chosen.Add([pscustomobject]@{ + Path = $pkg.Path + DownloadUrl = $pkg.DownloadUrl + DriverFileName = $pkg.FileName + Category = $pkg.Category + VendorVersion = $pkg.VendorVersion + DateTime = $pkg.DateTime + ComponentIds = $pkg.ComponentIds + }) + } + + if ($chosen.Count -eq 0) { + WriteLog "Get-DellLatestDriverPackages: No qualifying driver packages after supersedence." + return @() + } + + WriteLog ("Get-DellLatestDriverPackages: Selected {0} package(s) after supersedence." -f $chosen.Count) + return $chosen +} + +Export-ModuleMember -Function Convert-DellVendorVersion,Compare-DellVendorVersion,Get-DellCatalogIndex,Get-DellClientModels,Get-DellLatestDriverPackages \ No newline at end of file diff --git a/FFUDevelopment/FFU.Common/FFU.Common.psd1 b/FFUDevelopment/FFU.Common/FFU.Common.psd1 index ee155fb..71d11c5 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.psd1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.psd1 @@ -67,6 +67,7 @@ Description = 'Common functions shared between FFU Builder UI and the BuildFFUVM # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess NestedModules = @('FFU.Common.Drivers.psm1', + 'FFU.Common.Drivers.Dell.psm1', 'FFU.Common.Winget.psm1', 'FFU.Common.Parallel.psm1', 'FFU.Common.Cleanup.psm1') diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Dell.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Dell.psm1 index 271f6ac..0b055e3 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Dell.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Dell.psm1 @@ -12,147 +12,98 @@ function Get-DellDriversModelList { [Parameter(Mandatory = $true)] [int]$WindowsRelease, [Parameter(Mandatory = $true)] - [string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers) + [string]$DriversFolder, [Parameter(Mandatory = $true)] - [string]$Make # Should be 'Dell' + [string]$Make ) - # Define Dell specific drivers folder and catalog file names + # 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 = if ($WindowsRelease -le 11) { "CatalogPC" } else { "Catalog" } + $catalogBaseName = "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" } + $catalogUrl = "https://downloads.dell.com/catalog/Catalog.cab" - $uniqueModelNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) - $reader = $null + 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 { - # 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 } } + '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 ($isModelElement -and $isDriverComponent) { - $modelName = $reader.Value.Trim() - if (-not [string]::IsNullOrWhiteSpace($modelName)) { $uniqueModelNames.Add($modelName) | Out-Null } - $isModelElement = $false # Reset after reading CDATA + if ($inDriver -and $inModel) { + $val = $reader.Value.Trim() + if ($val) { $modelsHash.Add($val) | Out-Null } + $inModel = $false } } ([System.Xml.XmlNodeType]::EndElement) { - switch ($reader.Name) { - 'SoftwareComponent' { $isDriverComponent = $false; $isModelElement = $false; $modelDepth = -1 } - 'Model' { if ($reader.Depth -eq $modelDepth) { $isModelElement = $false; $modelDepth = -1 } } - } + if ($reader.Name -eq 'SoftwareComponent') { $inDriver = $false; $inModel = $false } + elseif ($reader.Name -eq 'Model' -and $reader.Depth -eq $depthModel) { $inModel = $false; $depthModel = -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 - } + $reader.Dispose() } - # 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 - }) + $out = [System.Collections.Generic.List[pscustomobject]]::new() + foreach ($nm in ($modelsHash | Sort-Object)) { + $out.Add([pscustomobject]@{ Make = $Make; Model = $nm }) } - - return $models + return $out } # Function to download and extract drivers for a specific Dell model (Modified for ForEach-Object -Parallel) @@ -160,552 +111,281 @@ function Save-DellDriversTask { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [PSCustomObject]$DriverItemData, # Contains Model property + [pscustomobject]$DriverItemData, [Parameter(Mandatory = $true)] - [string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers) + [string]$DriversFolder, [Parameter(Mandatory = $true)] [string]$WindowsArch, [Parameter(Mandatory = $true)] [int]$WindowsRelease, [Parameter()] - [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null + [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, [Parameter()] - [bool]$CompressToWim = $false, # New parameter for compression + [bool]$CompressToWim = $false, [Parameter()] [bool]$PreserveSourceOnCompress = $false ) - - $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..." } - - $sanitizedModelName = ConvertTo-SafeName -Name $modelName - if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" } - $makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make + + $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 # Relative path for the driver folder + $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 { - # Check for existing drivers - $existingDriver = Test-ExistingDriver -Make $make -Model $sanitizedModelName -DriversFolder $DriversFolder -Identifier $modelName -ProgressQueue $ProgressQueue - if ($null -ne $existingDriver) { - # Add the 'Model' property to the return object for consistency if it's not there - if (-not $existingDriver.PSObject.Properties['Model']) { - $existingDriver | Add-Member -MemberType NoteProperty -Name 'Model' -Value $modelName + # 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 } - - # Special handling for existing folders that need compression - if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') { - $wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($sanitizedModelName).wim" - $sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName - WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'." - if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." } + 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 $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop - $existingDriver.Status = "Already downloaded & Compressed" - $existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim" - $existingDriver.Success = $true - WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath." + 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 "Error compressing existing drivers for $($modelName): $($_.Exception.Message)" - $existingDriver.Status = "Already downloaded (Compression failed)" - $existingDriver.Success = $false + WriteLog "Compression failed for $($modelDisplay): $($_.Exception.Message)" + $existing.Status = 'Already downloaded (Compression failed)' } - if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $existingDriver.Status } + if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $existing.Status } } - - return $existingDriver + return $existing } - # Define paths for Dell catalog. The catalog is assumed to be prepared by the calling function. - $dellDriversFolder = Join-Path -Path $DriversFolder -ChildPath "Dell" - $catalogBaseName = if ($WindowsRelease -le 11) { "CatalogPC" } else { "Catalog" } - $dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml" + 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 } - # 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 } + $packages = @() - # Check if the provided XML path exists - if (-not (Test-Path -Path $dellCatalogXML -PathType Leaf)) { - throw "Dell Catalog XML file not found at specified path: $dellCatalogXML" - } - - WriteLog "Parsing existing Dell Catalog XML for model '$modelName' from: $dellCatalogXML" - - # Initialize variables - $baseLocation = $null - $latestDrivers = @{} # Hashtable to store latest drivers for this model - $modelSpecificDriversFound = $false - - # Create XML reader settings - $settings = New-Object System.Xml.XmlReaderSettings - $settings.IgnoreWhitespace = $true - $settings.IgnoreComments = $true - - # Create XML reader - $reader = $null - try { - $reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings) - - # First pass - get baseLocation from manifest - while ($reader.Read()) { - if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "Manifest") { - $baseLocationAttr = $reader.GetAttribute("baseLocation") - if ($null -ne $baseLocationAttr) { - $baseLocation = "https://" + $baseLocationAttr + "/" - break - } + if ($WindowsRelease -le 11) { + $cabUrl = $DriverItemData.CabUrl + if ([string]::IsNullOrWhiteSpace($cabUrl)) { + WriteLog "CabUrl missing for '$modelDisplay' – falling back to legacy CatalogPC parsing." + # Fallback legacy client method + $catalogCab = Join-Path $makeDriversPath 'CatalogPC.cab' + $catalogXml = Join-Path $makeDriversPath 'CatalogPC.xml' + $catalogUrl = 'http://downloads.dell.com/catalog/CatalogPC.cab' + $need = $true + if (Test-Path $catalogXml) { + if (((Get-Date) - (Get-Item $catalogXml).CreationTime).TotalDays -lt 7) { $need = $false } } - } - - if ($null -eq $baseLocation) { - throw "Invalid Dell Catalog XML format: Missing 'baseLocation' attribute in Manifest element." - } - - # Reset reader for second pass - $reader.Dispose() - $reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings) - - # Process SoftwareComponents - while ($reader.Read()) { - if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "SoftwareComponent") { - # Read the entire SoftwareComponent subtree - $componentXml = $reader.ReadSubtree() - $component = New-Object System.Xml.XmlDocument - $component.Load($componentXml) - $componentXml.Dispose() - - # Check if it's a driver component - $componentTypeNode = $component.SelectSingleNode("//ComponentType[@value='DRVR']") - if ($null -eq $componentTypeNode) { - continue - } - - # Check if component supports the model - $modelNodes = $component.SelectNodes("//SupportedSystems/Brand/Model") - $modelMatch = $false - - foreach ($modelNode in $modelNodes) { - $displayNode = $modelNode.SelectSingleNode("Display") - if ($null -ne $displayNode -and $displayNode.InnerText.Trim() -eq $modelName) { - $modelMatch = $true - break - } - } - - if ($modelMatch) { - # Check OS compatibility - $validOS = $null - $osNodes = $component.SelectNodes("//SupportedOperatingSystems/OperatingSystem") - - if ($null -ne $osNodes) { - foreach ($osNode in $osNodes) { - $osArch = $osNode.GetAttribute("osArch") - - if ($WindowsRelease -le 11) { - # Client OS check - if ($osArch -eq $WindowsArch) { - $validOS = $osNode - break - } - } - else { - # Server OS check - $osCode = $osNode.GetAttribute("osCode") - $osCodePattern = switch ($WindowsRelease) { - 2016 { "W14" } - 2019 { "W19" } - 2022 { "W22" } - 2025 { "W25" } - default { "W22" } - } - if ($osArch -eq $WindowsArch -and $osCode -match $osCodePattern) { - $validOS = $osNode - break - } - } - } - } - - if ($validOS) { - $modelSpecificDriversFound = $true - - # Extract driver information - $driverPath = $component.SoftwareComponent.GetAttribute("path") + if ($need) { + if (Test-Path $catalogCab) { Remove-SafeFolder $catalogCab } + if (Test-Path $catalogXml) { Remove-SafeFolder $catalogXml } + 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 "Legacy fallback failed; 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 - $driverFileName = [System.IO.Path]::GetFileName($driverPath) - - # Get name - $nameNode = $component.SelectSingleNode("//Name/Display") - $name = if ($null -ne $nameNode) { $nameNode.InnerText } else { "UnknownDriver" } - $name = $name -replace '[\\\/\:\*\?\"\<\>\| ]', '_' -replace '[\,]', '-' - - # Get category - $categoryNode = $component.SelectSingleNode("//Category/Display") - $category = if ($null -ne $categoryNode) { $categoryNode.InnerText } else { "Uncategorized" } - $category = $category -replace '[\\\/\:\*\?\"\<\>\| ]', '_' - - # Get version - $version = [version]"0.0" - $vendorVersion = $component.SoftwareComponent.GetAttribute("vendorVersion") - if ($null -ne $vendorVersion) { - try { $version = [version]$vendorVersion } catch { WriteLog "Warning: Could not parse version '$vendorVersion' for driver '$name'. Using 0.0." } - } - + $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] - - # 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 + 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 } } } - } - finally { - if ($null -ne $reader) { - $reader.Dispose() - } - } + else { + # Normal new model-based workflow + $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') - WriteLog "Searching $($softwareComponents.Count) DRVR components in '$dellCatalogXML' for model '$modelName'..." + 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 } - 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 } + 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" } - $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 '$dellCatalogXML'." - 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 - $driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file - WriteLog "Compressing '$modelPath' to '$destinationWimPath'..." - try { - $compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -PreserveSource:$PreserveSourceOnCompress -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)" + if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Selecting latest drivers...' } + $packages = Get-DellLatestDriverPackages -ModelXmlPath $modelXmlPath -WindowsArch $WindowsArch -WindowsRelease $WindowsRelease } } else { - $status = "Completed" # Final status if not compressing + # 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 } + 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 } } } - # --- End Compression --- - - $success = $true # Mark success as download/extract was okay - + + 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++ + $status = "Downloading $idx/$total" + 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)) { + try { Start-BitsTransferWithRetry -Source $pkg.DownloadUrl -Destination $driverFilePath } + catch { WriteLog "Download failed: $($pkg.DownloadUrl) $($_.Exception.Message)"; continue } + } + + 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 { - $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; DriverPath = $null } + $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 } } - - # 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; DriverPath = $driverRelativePath } } Export-ModuleMember -Function * \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 index c9ca0b3..52833f2 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.psm1 @@ -84,8 +84,8 @@ function ConvertTo-StandardizedDriverModel { [psobject]$State ) - $modelDisplay = $RawDriverObject.Model # Default - $id = $RawDriverObject.Model # Default + $modelDisplay = $RawDriverObject.Model + $id = $RawDriverObject.Model $link = $null $productName = $null $machineType = $null @@ -96,26 +96,51 @@ function ConvertTo-StandardizedDriverModel { # Lenovo specific handling if ($Make -eq 'Lenovo') { - $modelDisplay = $RawDriverObject.Model + $modelDisplay = $RawDriverObject.Model $productName = $RawDriverObject.ProductName $machineType = $RawDriverObject.MachineType - $id = $RawDriverObject.MachineType + $id = $RawDriverObject.MachineType } - return [PSCustomObject]@{ + # 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 + Model = $modelDisplay Link = $link - Id = $id - ProductName = $productName - MachineType = $machineType - Version = "" # Placeholder - Type = "" # Placeholder - Size = "" # Placeholder - Arch = "" # Placeholder - DownloadStatus = "" # Initial download status + 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