mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3524d02047 | |||
| 7948201e18 | |||
| 8d84137a27 | |||
| 2273cffbc2 | |||
| 63ef35a005 | |||
| 417be73b23 | |||
| 18367219c8 | |||
| 2a77cf1a02 | |||
| 41b0f7d742 | |||
| 24c81c234f | |||
| 4833d9f00d | |||
| 37e3497522 | |||
| 8229aa73fe | |||
| e67590d0a1 | |||
| 33f0608d84 | |||
| 3d1a586c73 | |||
| 7d36253668 | |||
| e076e9f4ca | |||
| 44aa4d3a32 | |||
| a1d08b6fa4 | |||
| fc4a71f7e1 | |||
| 9a59b9fea4 | |||
| 19081a2e1f | |||
| 3cb4003bcd | |||
| beb48e500e | |||
| 93c4679c46 | |||
| d6688def9d | |||
| 489d53f55c | |||
| 3deb8fb8d2 | |||
| 1af3a0f092 | |||
| de80ac551b | |||
| 89601efde0 | |||
| 235065322c | |||
| 11b3e120e2 | |||
| 667edf3724 | |||
| 4a10e27ddf | |||
| b4305a1edb | |||
| 7598ee96da | |||
| 6de7c861ed | |||
| 658c57e22c | |||
| 60cf1dab18 | |||
| 4ce9183bd3 | |||
| 7dd002396f | |||
| 1130a830c7 | |||
| 66a9026b8f | |||
| 458f1e517c | |||
| a13f9b481a | |||
| de70a22c42 | |||
| f3d3506e02 | |||
| 1daa14584a |
@@ -2,16 +2,6 @@
|
||||
<Add SourcePath="C:\FFUDevelopment\Apps\Office" OfficeClientEdition="64" Channel="Current">
|
||||
<Product ID="O365ProPlusRetail">
|
||||
<Language ID="MatchOS" />
|
||||
<ExcludeApp ID="Access" />
|
||||
<ExcludeApp ID="Lync" />
|
||||
<ExcludeApp ID="Publisher" />
|
||||
<ExcludeApp ID="Bing" />
|
||||
</Product>
|
||||
</Add>
|
||||
<Property Name="SharedComputerLicensing" Value="0" />
|
||||
<Property Name="FORCEAPPSHUTDOWN" Value="FALSE" />
|
||||
<Property Name="DeviceBasedLicensing" Value="0" />
|
||||
<Property Name="SCLCacheOverride" Value="0" />
|
||||
<Updates Enabled="TRUE" />
|
||||
<Display Level="None" AcceptEULA="TRUE" />
|
||||
</Configuration>
|
||||
@@ -92,6 +92,49 @@ function Invoke-Process {
|
||||
}
|
||||
}
|
||||
|
||||
function Format-MsiArguments {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ensures MSI file paths in msiexec arguments are properly quoted.
|
||||
.DESCRIPTION
|
||||
Detects /i arguments followed by an unquoted path ending in .msi
|
||||
and wraps the path in double quotes to handle paths with spaces.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$CommandLine,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Arguments
|
||||
)
|
||||
|
||||
# Only process if the command is msiexec
|
||||
if ($CommandLine -notmatch '^msiexec(\.exe)?$') {
|
||||
return $Arguments
|
||||
}
|
||||
|
||||
# Regex pattern explanation:
|
||||
# (?i) - Case-insensitive matching
|
||||
# (/i)\s+ - Match /i followed by whitespace
|
||||
# (?!") - Negative lookahead: not already quoted
|
||||
# (.+?\.msi) - Capture path ending in .msi (lazy match to stop at first .msi)
|
||||
# (?=\s+/|\s*$) - Followed by another switch or end of string
|
||||
|
||||
# Pattern to match /i followed by an unquoted MSI path
|
||||
$pattern = '(?i)(/i)\s+(?!")(.+?\.msi)(?=\s+/|\s*$)'
|
||||
|
||||
if ($Arguments -match $pattern) {
|
||||
$originalArgs = $Arguments
|
||||
# Replace with quoted path
|
||||
$Arguments = $Arguments -replace $pattern, '$1 "$2"'
|
||||
Write-Host "Detected unquoted MSI path in msiexec arguments. Adjusted arguments:"
|
||||
Write-Host "Original: $originalArgs"
|
||||
Write-Host "Modified: $Arguments"
|
||||
}
|
||||
|
||||
return $Arguments
|
||||
}
|
||||
|
||||
function Install-Applications {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -177,6 +220,15 @@ function Install-Applications {
|
||||
$ignoreNonZeroExitCodes = $app.IgnoreNonZeroExitCodes
|
||||
}
|
||||
|
||||
# Auto-quote MSI paths if using msiexec and path contains spaces but no quotes
|
||||
if ($null -ne $argumentsToPass -and $argumentsToPass.Count -gt 0) {
|
||||
$joinedArgs = $argumentsToPass -join ' '
|
||||
$formattedArgs = Format-MsiArguments -CommandLine $app.CommandLine -Arguments $joinedArgs
|
||||
if ($formattedArgs -ne $joinedArgs) {
|
||||
$argumentsToPass = @($formattedArgs)
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $argumentsToPass -or $argumentsToPass.Count -eq 0) {
|
||||
Write-Host "Running command: $($app.CommandLine) (no arguments)"
|
||||
$result = Invoke-Process -FilePath $app.CommandLine -AdditionalSuccessCodes $additionalSuccessCodes -IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes
|
||||
|
||||
+434
-302
@@ -202,6 +202,9 @@ Example: @{ "SanDisk Ultra" = "1234567890"; "Kingston DataTraveler" = "098765432
|
||||
.PARAMETER MaxUSBDrives
|
||||
Maximum number of USB drives to build in parallel. Default is 5. Set to 0 to process all discovered drives (or all selected drives when USBDriveList or selection is used). Actual throttle will never exceed the number of drives discovered.
|
||||
|
||||
.PARAMETER Threads
|
||||
Controls the throttle applied to parallel tasks inside the script. Default is 5, matching the UI Threads field, and applies to driver downloads invoked through Invoke-ParallelProcessing.
|
||||
|
||||
.PARAMETER UserAgent
|
||||
User agent string to use when downloading files.
|
||||
|
||||
@@ -230,7 +233,7 @@ Integer value of 10 or 11. This is used to identify which release of Windows to
|
||||
Edition of Windows 10/11 to be installed. Accepted values are: 'Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N'.
|
||||
|
||||
.PARAMETER WindowsVersion
|
||||
String value of the Windows version to download. This is used to identify which version of Windows to download. Default is '24h2'.
|
||||
String value of the Windows version to download. This is used to identify which version of Windows to download. Default is '25h2'.
|
||||
|
||||
.EXAMPLE
|
||||
Command line for most people who want to download the latest Windows 11 Pro x64 media in English (US) with the latest Windows Cumulative Update, .NET Framework, Defender platform and definition updates, Edge, OneDrive, and Office/M365 Apps. It will also copy drivers to the FFU. This can take about 40 minutes to create the FFU due to the time it takes to download and install the updates.
|
||||
@@ -354,11 +357,15 @@ param(
|
||||
[bool]$BuildUSBDrive,
|
||||
[hashtable]$USBDriveList,
|
||||
[int]$MaxUSBDrives = 5,
|
||||
[ValidateRange(1, 64)]
|
||||
[int]$Threads = 5,
|
||||
[ValidateSet('Foreground', 'High', 'Normal', 'Low')]
|
||||
[string]$BitsPriority = 'Normal',
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet(10, 11, 2016, 2019, 2021, 2022, 2024, 2025)]
|
||||
[int]$WindowsRelease = 11,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$WindowsVersion = '24h2',
|
||||
[string]$WindowsVersion = '25h2',
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet('x86', 'x64', 'arm64')]
|
||||
[string]$WindowsArch = 'x64',
|
||||
@@ -436,7 +443,7 @@ param(
|
||||
[switch]$Cleanup
|
||||
)
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$version = '2509.1Preview'
|
||||
$version = '2511.1Preview'
|
||||
|
||||
# Remove any existing modules to avoid conflicts
|
||||
if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) {
|
||||
@@ -659,6 +666,7 @@ if ($WindowsSKU -like "*LTS*") {
|
||||
|
||||
# Set the log path for the common logger
|
||||
Set-CommonCoreLogPath -Path $LogFile
|
||||
Set-BitsTransferPriority -Priority $BitsPriority
|
||||
|
||||
#FUNCTIONS
|
||||
|
||||
@@ -1432,124 +1440,126 @@ function Get-DellDrivers {
|
||||
[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"
|
||||
$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
|
||||
if ([string]::IsNullOrWhiteSpace($cabUrl)) {
|
||||
WriteLog "CabUrl missing for '$($target.Model)'; resolving via CatalogIndexPC."
|
||||
$resolved = Resolve-DellCabUrlFromModel -DriversFolder $DriversFolder -ModelDisplay $target.Model
|
||||
if ($null -eq $resolved -or [string]::IsNullOrWhiteSpace($resolved.CabUrl)) {
|
||||
throw "Unable to resolve CabUrl for $($target.Model) from CatalogIndexPC."
|
||||
}
|
||||
else {
|
||||
$cabUrl = $resolved.CabUrl
|
||||
$target.CabUrl = $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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Server pathway (unchanged legacy)
|
||||
$catalogUrl = "https://downloads.dell.com/catalog/Catalog.cab"
|
||||
$DellCabFile = "$DriversFolder\Catalog.cab"
|
||||
$DellCatalogXML = "$DriversFolder\Catalog.xml"
|
||||
}
|
||||
$DellCabFile = Join-Path $DriversFolder 'Catalog.cab'
|
||||
$DellCatalogXML = Join-Path $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"
|
||||
}
|
||||
exit
|
||||
}
|
||||
|
||||
WriteLog "Downloading Dell Catalog cab file: $catalogUrl to $DellCabFile"
|
||||
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $DellCabFile
|
||||
WriteLog "Dell Catalog cab file downloaded"
|
||||
|
||||
WriteLog "Extracting Dell Catalog cab file to $DellCatalogXML"
|
||||
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) {
|
||||
if (-not $validOS) { continue }
|
||||
$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 '[\\\/\:\*\?\"\<\>\| ]', '_'
|
||||
$name = $component.Name.Display.'#cdata-section' -replace '[\\\/\:\*\?\"\<\>\| ]', '_' -replace '[\,]', '-'
|
||||
$category = $component.Category.Display.'#cdata-section' -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
|
||||
}
|
||||
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 +1568,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
|
||||
$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
|
||||
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`""
|
||||
$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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1929,6 +1852,96 @@ function Get-ADK {
|
||||
}
|
||||
return $adkPath
|
||||
}
|
||||
function Get-ProductsCab {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutFile,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('x64', 'arm64')]
|
||||
[string]$Architecture,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BuildVersion
|
||||
)
|
||||
|
||||
$productsArchitecture = if ($Architecture -eq 'arm64') { 'arm64' } else { 'amd64' }
|
||||
$productsParam = "PN=Windows.Products.Cab.$productsArchitecture&V=$BuildVersion"
|
||||
$deviceAttributes = "DUScan=1;OSVersion=10.0.26100.1"
|
||||
|
||||
$bodyObj = [ordered]@{
|
||||
Products = $productsParam
|
||||
DeviceAttributes = $deviceAttributes
|
||||
}
|
||||
$bodyJson = $bodyObj | ConvertTo-Json -Compress
|
||||
|
||||
$searchUri = 'https://fe3.delivery.mp.microsoft.com/UpdateMetadataService/updates/search/v1/bydeviceinfo'
|
||||
|
||||
WriteLog "Requesting products.cab location from Windows Update service..."
|
||||
try {
|
||||
$searchResponse = Invoke-RestMethod -Uri $searchUri -Method Post -ContentType 'application/json' -Headers @{ Accept = '*/*' } -Body $bodyJson
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to retrieve products.cab metadata: $($_.Exception.Message)"
|
||||
throw
|
||||
}
|
||||
|
||||
if ($searchResponse -is [System.Array]) { $searchResponse = $searchResponse[0] }
|
||||
if (-not $searchResponse.FileLocations) { throw "Search response did not include FileLocations." }
|
||||
|
||||
$fileRec = $searchResponse.FileLocations | Where-Object { $_.FileName -eq 'products.cab' } | Select-Object -First 1
|
||||
if (-not $fileRec) { throw "products.cab entry not found in FileLocations." }
|
||||
|
||||
$downloadUrl = $fileRec.Url
|
||||
$serverDigestB64 = $fileRec.Digest
|
||||
$serverSize = [int64]$fileRec.Size
|
||||
$updateId = $searchResponse.UpdateIds[0]
|
||||
|
||||
try {
|
||||
$metaUri = "https://fe3.delivery.mp.microsoft.com/UpdateMetadataService/updates/v1/$updateId"
|
||||
$meta = Invoke-RestMethod -Uri $metaUri -Method Get -Headers @{ Accept = '*/*' }
|
||||
if ($meta.LocalizedProperties.Count -gt 0) {
|
||||
$title = $meta.LocalizedProperties[0].Title
|
||||
WriteLog "Resolved update: $title"
|
||||
}
|
||||
else {
|
||||
WriteLog "Resolved update id: $updateId"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Resolved update id: $updateId"
|
||||
}
|
||||
|
||||
$destDir = Split-Path -Path $OutFile -Parent
|
||||
if ($destDir -and -not (Test-Path $destDir)) {
|
||||
[void](New-Item -ItemType Directory -Path $destDir)
|
||||
}
|
||||
|
||||
WriteLog "Downloading products.cab to $OutFile ..."
|
||||
$downloadHeaders = @{ Accept = '*/*' }
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $OutFile -Headers $downloadHeaders -UserAgent $UserAgent
|
||||
|
||||
$actualSize = (Get-Item $OutFile).Length
|
||||
if ($actualSize -ne $serverSize) {
|
||||
throw "Size check failed. Expected $serverSize bytes. Got $actualSize bytes."
|
||||
}
|
||||
|
||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||
$fs = [System.IO.File]::OpenRead($OutFile)
|
||||
try {
|
||||
$hashBytes = $sha256.ComputeHash($fs)
|
||||
}
|
||||
finally {
|
||||
$fs.Dispose()
|
||||
}
|
||||
$actualDigestB64 = [Convert]::ToBase64String($hashBytes)
|
||||
|
||||
if ($actualDigestB64 -ne $serverDigestB64) {
|
||||
throw "Digest check failed. Expected $serverDigestB64. Got $actualDigestB64."
|
||||
}
|
||||
|
||||
WriteLog "products.cab downloaded and verified successfully."
|
||||
return $OutFile
|
||||
}
|
||||
|
||||
function Get-WindowsESD {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
@@ -1951,25 +1964,42 @@ function Get-WindowsESD {
|
||||
WriteLog "Windows Language: $WindowsLang"
|
||||
WriteLog "Windows Media Type: $MediaType"
|
||||
|
||||
# Select cab file URL based on Windows Release
|
||||
$cabFileUrl = if ($WindowsRelease -eq 10) {
|
||||
'https://go.microsoft.com/fwlink/?LinkId=841361'
|
||||
$cabFilePath = Join-Path $PSScriptRoot "tempCabFile.cab"
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
try {
|
||||
if ($WindowsRelease -eq 10) {
|
||||
WriteLog "Downloading Cab file"
|
||||
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/?LinkId=841361' -OutFile $cabFilePath -Headers $Headers -UserAgent $UserAgent
|
||||
}
|
||||
elseif ($WindowsRelease -eq 11) {
|
||||
'https://go.microsoft.com/fwlink/?LinkId=2156292'
|
||||
WriteLog "Downloading Cab file"
|
||||
$buildVersionMap = @{
|
||||
'22H2' = '22621.0.0.0'
|
||||
'23H2' = '22631.0.0.0'
|
||||
'24H2' = '26100.0.0.0'
|
||||
'25H2' = '26100.0.0.0'
|
||||
}
|
||||
$normalizedVersion = $WindowsVersion.ToUpper()
|
||||
if ($buildVersionMap.ContainsKey($normalizedVersion)) {
|
||||
$buildVersion = $buildVersionMap[$normalizedVersion]
|
||||
}
|
||||
else {
|
||||
WriteLog "No explicit build mapping found for Windows 11 version '$WindowsVersion'. Defaulting products.cab build token to 26100.0.0.0."
|
||||
$buildVersion = '26100.0.0.0'
|
||||
}
|
||||
|
||||
$cabArchitecture = if ($WindowsArch -eq 'ARM64') { 'arm64' } else { 'x64' }
|
||||
Get-ProductsCab -OutFile $cabFilePath -Architecture $cabArchitecture -BuildVersion $buildVersion | Out-Null
|
||||
}
|
||||
else {
|
||||
throw "Downloading Windows $WindowsRelease is not supported. Please use the -ISOPath parameter to specify the path to the Windows $WindowsRelease ISO file."
|
||||
}
|
||||
|
||||
# Download cab file
|
||||
WriteLog "Downloading Cab file"
|
||||
$cabFilePath = Join-Path $PSScriptRoot "tempCabFile.cab"
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
Invoke-WebRequest -Uri $cabFileUrl -OutFile $cabFilePath -Headers $Headers -UserAgent $UserAgent
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
WriteLog "Download succeeded"
|
||||
}
|
||||
finally {
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
}
|
||||
|
||||
# Extract XML from cab file
|
||||
WriteLog "Extracting Products XML from cab"
|
||||
@@ -1989,18 +2019,14 @@ function Get-WindowsESD {
|
||||
$esdFilePath = Join-Path $PSScriptRoot (Split-Path $file.FilePath -Leaf)
|
||||
#Download if ESD file doesn't already exist
|
||||
If (-not (Test-Path $esdFilePath)) {
|
||||
#Required to fix slow downloads
|
||||
# $ProgressPreference = 'SilentlyContinue'
|
||||
WriteLog "Downloading $($file.filePath) to $esdFIlePath"
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
Mark-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
|
||||
Invoke-WebRequest -Uri $file.FilePath -OutFile $esdFilePath -Headers $Headers -UserAgent $UserAgent
|
||||
Start-BitsTransferWithRetry -Source $file.FilePath -Destination $esdFilePath
|
||||
Clear-DownloadInProgress -FFUDevelopmentPath $FFUDevelopmentPath -TargetPath $esdFilePath
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
WriteLog "Download succeeded"
|
||||
#Set back to show progress
|
||||
# $ProgressPreference = 'Continue'
|
||||
WriteLog "Cleanup cab and xml file"
|
||||
Remove-Item -Path $cabFilePath -Force
|
||||
Remove-Item -Path $xmlFilePath -Force
|
||||
@@ -2011,8 +2037,6 @@ function Get-WindowsESD {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Get-ODTURL {
|
||||
try {
|
||||
[String]$ODTPage = Invoke-WebRequest 'https://www.microsoft.com/en-us/download/details.aspx?id=49117' -Headers $Headers -UserAgent $UserAgent -ErrorAction Stop
|
||||
@@ -2864,7 +2888,7 @@ function New-PEMedia {
|
||||
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver $PEDriversFolder -Recurse -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-null
|
||||
}
|
||||
catch {
|
||||
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
|
||||
WriteLog 'Some drivers failed to be added. This can be expected. Continuing.'
|
||||
}
|
||||
WriteLog "Adding drivers complete"
|
||||
}
|
||||
@@ -3428,18 +3452,38 @@ Function Get-USBDrive {
|
||||
# Log the count of specified USB drives
|
||||
$USBDriveListCount = $USBDriveList.Count
|
||||
WriteLog "Looking for $USBDriveListCount USB drives from USB Drive List"
|
||||
# Get only the specified USB drives based on both model and serial number
|
||||
# Get only the specified USB drives based on model and UniqueId
|
||||
$USBDrives = @()
|
||||
foreach ($model in $USBDriveList.Keys) {
|
||||
$serialNumber = $USBDriveList[$model]
|
||||
Writelog "Looking for USB drive model $model with serial number $serialNumber"
|
||||
$USBDrive = Get-CimInstance -ClassName Win32_DiskDrive -Filter "Model LIKE '%$model%' AND SerialNumber LIKE '$serialNumber%' AND (MediaType='Removable Media' OR MediaType='External hard disk media')"
|
||||
if ($USBDrive) {
|
||||
WriteLog "Found USB drive model $($USBDrive.model) with serial number $($USBDrive.serialNumber)"
|
||||
$USBDrives += $USBDrive
|
||||
$configUniqueId = $USBDriveList[$model]
|
||||
WriteLog "Looking for USB drive model $model with UniqueId $configUniqueId"
|
||||
# First get candidate drives by model and media type
|
||||
$candidateDrives = Get-CimInstance -ClassName Win32_DiskDrive -Filter "Model LIKE '%$model%' AND (MediaType='Removable Media' OR MediaType='External hard disk media')"
|
||||
$foundDrive = $null
|
||||
foreach ($candidate in $candidateDrives) {
|
||||
# Get the disk to retrieve UniqueId
|
||||
$disk = Get-Disk -Number $candidate.Index -ErrorAction SilentlyContinue
|
||||
if ($disk -and $disk.UniqueId) {
|
||||
# Trim the machine name suffix (everything after the colon) from UniqueId
|
||||
$diskUniqueId = if ($disk.UniqueId -match ':') {
|
||||
$disk.UniqueId.Split(':')[0]
|
||||
}
|
||||
else {
|
||||
WriteLog "USB drive model $model with serial number $serialNumber not found"
|
||||
$disk.UniqueId
|
||||
}
|
||||
# Match on the trimmed UniqueId
|
||||
if ($diskUniqueId -eq $configUniqueId) {
|
||||
$foundDrive = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($foundDrive) {
|
||||
WriteLog "Found USB drive model $($foundDrive.Model) with UniqueId $configUniqueId"
|
||||
$USBDrives += $foundDrive
|
||||
}
|
||||
else {
|
||||
WriteLog "USB drive model $model with UniqueId $configUniqueId not found"
|
||||
}
|
||||
}
|
||||
$USBDrivesCount = $USBDrives.Count
|
||||
@@ -3453,7 +3497,7 @@ Function Get-USBDrive {
|
||||
}
|
||||
|
||||
# Check if any USB drives were found
|
||||
if ($null -eq $USBDrives) {
|
||||
if ($USBDrives.Count -eq 0) {
|
||||
WriteLog "No USB drive found. Exiting"
|
||||
Write-Error "No USB drive found. Exiting"
|
||||
exit 1
|
||||
@@ -4808,15 +4852,15 @@ if (($WindowsArch -eq 'ARM64') -and ($UpdateLatestMSRT -eq $true)) {
|
||||
$UpdateLatestMSRT = $false
|
||||
WriteLog 'Windows Malicious Software Removal Tool is not available for the ARM64 architecture.'
|
||||
}
|
||||
#If downloading ESD from MCT, hardcode WindowsVersion to 22H2 for Windows 10 and 24H2 for Windows 11
|
||||
#MCT media only provides 22H2 and 24H2 media
|
||||
#If downloading ESD from MCT, hardcode WindowsVersion to 22H2 for Windows 10 and 25H2 for Windows 11
|
||||
#MCT media only provides 22H2 and 25H2 media
|
||||
#This prevents issues with VHDX Caching unecessarily and with searching for CUs
|
||||
if ($ISOPath -eq '') {
|
||||
if ($WindowsRelease -eq '10') {
|
||||
$WindowsVersion = '22H2'
|
||||
}
|
||||
if ($WindowsRelease -eq '11') {
|
||||
$WindowsVersion = '24H2'
|
||||
$WindowsVersion = '25H2'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4889,19 +4933,96 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
|
||||
$makeName = $makeEntry.Name
|
||||
if ($makeEntry.Value.PSObject.Properties['Models']) {
|
||||
foreach ($modelEntry in $makeEntry.Value.Models) {
|
||||
# Construct the PSCustomObject exactly as the Save-*DriversTask functions expect $DriverItemData
|
||||
if ($null -eq $modelEntry -or -not $modelEntry.PSObject.Properties['Name']) {
|
||||
WriteLog "Skipping model entry for Make '$makeName' due to missing Name."
|
||||
continue
|
||||
}
|
||||
|
||||
$modelName = $modelEntry.Name
|
||||
if ([string]::IsNullOrWhiteSpace($modelName)) {
|
||||
WriteLog "Skipping model entry for Make '$makeName' because Name is empty."
|
||||
continue
|
||||
}
|
||||
$modelName = $modelName.Trim()
|
||||
|
||||
$driverItem = $null
|
||||
switch ($makeName) {
|
||||
'Microsoft' {
|
||||
$driverItem = [PSCustomObject]@{
|
||||
Make = $makeName
|
||||
Model = $modelEntry.Name # This is the display name, e.g., "Surface Book 3" or "Lenovo 500w (83LH)"
|
||||
Model = $modelName
|
||||
Link = if ($modelEntry.PSObject.Properties['Link']) { $modelEntry.Link } else { $null }
|
||||
ProductName = if ($modelEntry.PSObject.Properties['ProductName']) { $modelEntry.ProductName } else { $null } # Specifically for Lenovo
|
||||
MachineType = if ($modelEntry.PSObject.Properties['MachineType']) { $modelEntry.MachineType } else { $null } # Specifically for Lenovo
|
||||
# Ensure all properties potentially accessed by any Save-*DriversTask via $DriverItemData are present
|
||||
}
|
||||
}
|
||||
'HP' {
|
||||
$systemId = if ($modelEntry.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($modelEntry.SystemId)) { $modelEntry.SystemId.Trim() } else { $null }
|
||||
$baseName = if ($modelEntry.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($modelEntry.ProductName)) { $modelEntry.ProductName } else { $modelName }
|
||||
if ($modelName -match '(.+?)\s*\((.+?)\)$') {
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $matches[1].Trim() }
|
||||
if ([string]::IsNullOrWhiteSpace($systemId)) { $systemId = $matches[2].Trim() }
|
||||
}
|
||||
if ($baseName -match '(.+?)\s*\((.+?)\)$') {
|
||||
$baseName = $matches[1].Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($systemId)) { $systemId = $matches[2].Trim() }
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $modelName }
|
||||
$displayModel = if ([string]::IsNullOrWhiteSpace($systemId)) { $baseName.Trim() } else { "$($baseName.Trim()) ($($systemId.Trim()))" }
|
||||
$driverItem = [PSCustomObject]@{
|
||||
Make = $makeName
|
||||
Model = $displayModel
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($baseName)) {
|
||||
$driverItem | Add-Member -NotePropertyName ProductName -NotePropertyValue $baseName.Trim()
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($systemId)) {
|
||||
$driverItem | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemId
|
||||
}
|
||||
}
|
||||
'Lenovo' {
|
||||
$machineType = if ($modelEntry.PSObject.Properties['MachineType']) { $modelEntry.MachineType } else { $null }
|
||||
$productName = $modelName
|
||||
if ([string]::IsNullOrWhiteSpace($machineType) -and $modelName -match '(.+?)\s*\((.+?)\)$') {
|
||||
$productName = $matches[1].Trim()
|
||||
$machineType = $matches[2].Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($machineType)) {
|
||||
WriteLog "Skipping Lenovo model '$modelName' because MachineType is missing."
|
||||
continue
|
||||
}
|
||||
$displayModel = "$productName ($machineType)"
|
||||
$driverItem = [PSCustomObject]@{
|
||||
Make = $makeName
|
||||
Model = $displayModel
|
||||
ProductName = $productName
|
||||
MachineType = $machineType
|
||||
}
|
||||
}
|
||||
'Dell' {
|
||||
$systemId = if ($modelEntry.PSObject.Properties['SystemId']) { $modelEntry.SystemId } else { $null }
|
||||
$baseName = $modelName
|
||||
if ([string]::IsNullOrWhiteSpace($systemId) -and $modelName -match '(.+?)\s*\((.+?)\)$') {
|
||||
$baseName = $matches[1].Trim()
|
||||
$systemId = $matches[2].Trim()
|
||||
}
|
||||
$displayModel = if ([string]::IsNullOrWhiteSpace($systemId)) { $baseName } else { "$baseName ($systemId)" }
|
||||
$driverItem = [PSCustomObject]@{
|
||||
Make = $makeName
|
||||
Model = $displayModel
|
||||
SystemId = $systemId
|
||||
CabUrl = if ($modelEntry.PSObject.Properties['CabUrl']) { $modelEntry.CabUrl } else { $null }
|
||||
}
|
||||
}
|
||||
default {
|
||||
WriteLog "Skipping unsupported Make '$makeName' in Drivers.json."
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $driverItem) {
|
||||
$driversToProcess += $driverItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($driversToProcess.Count -eq 0) {
|
||||
WriteLog "No drivers found to process in $driversJsonPath."
|
||||
@@ -4922,54 +5043,102 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
|
||||
}
|
||||
|
||||
WriteLog "Starting parallel driver processing using Invoke-ParallelProcessing..."
|
||||
# Use the configured Threads value to control driver download concurrency
|
||||
$parallelResults = Invoke-ParallelProcessing -ItemsToProcess $driversToProcess `
|
||||
-TaskType 'DownloadDriverByMake' `
|
||||
-TaskArguments $taskArguments `
|
||||
-IdentifierProperty 'Model' `
|
||||
-WindowObject $null `
|
||||
-ListViewControl $null `
|
||||
-MainThreadLogPath $LogFile
|
||||
-MainThreadLogPath $LogFile `
|
||||
-ThrottleLimit $Threads
|
||||
|
||||
# After processing, update the driver mapping file
|
||||
# After processing, update the driver mapping file and detect failures
|
||||
$successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
if ($null -ne $parallelResults) {
|
||||
# Create a lookup table from the original items to get the 'Make'
|
||||
$makeLookup = @{}
|
||||
$driversToProcess | ForEach-Object { $makeLookup[$_.Model] = $_.Make }
|
||||
$failedDownloads = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
|
||||
# Filter for objects that could be results, avoiding stray log strings
|
||||
foreach ($result in ($parallelResults | Where-Object { $_ -is [hashtable] })) {
|
||||
if ($null -ne $parallelResults) {
|
||||
# Create a lookup table from the original items to retain full metadata for mapping.
|
||||
$driverLookup = @{}
|
||||
foreach ($driver in $driversToProcess) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($driver.Model)) {
|
||||
$driverLookup[$driver.Model] = $driver
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($result in $parallelResults) {
|
||||
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']
|
||||
$lookupModelName = $null
|
||||
$resultStatus = $null
|
||||
$resultDriverPath = $null
|
||||
$resultSuccess = $false
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($modelName)) {
|
||||
WriteLog "Could not determine model name from result object: $($result | ConvertTo-Json -Compress -Depth 3)"
|
||||
continue
|
||||
if ($result -is [hashtable]) {
|
||||
$lookupModelName = $result['Identifier']
|
||||
$resultStatus = $result['Status']
|
||||
if ($result.ContainsKey('DriverPath')) { $resultDriverPath = $result['DriverPath'] }
|
||||
if ($result.ContainsKey('Success')) {
|
||||
$resultSuccess = [bool]$result['Success']
|
||||
}
|
||||
elseif ($result.ContainsKey('ResultCode')) {
|
||||
$resultSuccess = ($result['ResultCode'] -eq 0)
|
||||
}
|
||||
}
|
||||
elseif ($result -is [pscustomobject]) {
|
||||
if ($result.PSObject.Properties.Name -contains 'Identifier' -and -not [string]::IsNullOrWhiteSpace($result.Identifier)) {
|
||||
$lookupModelName = $result.Identifier
|
||||
}
|
||||
elseif ($result.PSObject.Properties.Name -contains 'Model' -and -not [string]::IsNullOrWhiteSpace($result.Model)) {
|
||||
$lookupModelName = $result.Model
|
||||
}
|
||||
|
||||
if ($resultCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($driverPath)) {
|
||||
# The task was successful and returned a driver path.
|
||||
$makeJson = $makeLookup[$modelName]
|
||||
if ($makeJson) {
|
||||
$successfullyDownloaded.Add([PSCustomObject]@{
|
||||
Make = $makeJson
|
||||
Model = $modelName
|
||||
DriverPath = $driverPath
|
||||
if ($result.PSObject.Properties.Name -contains 'Status') { $resultStatus = $result.Status }
|
||||
if ($result.PSObject.Properties.Name -contains 'DriverPath') { $resultDriverPath = $result.DriverPath }
|
||||
if ($result.PSObject.Properties.Name -contains 'Success') {
|
||||
$resultSuccess = [bool]$result.Success
|
||||
}
|
||||
elseif ($result.PSObject.Properties.Name -contains 'ResultCode') {
|
||||
$resultSuccess = ($result.ResultCode -eq 0)
|
||||
}
|
||||
}
|
||||
|
||||
if ($resultSuccess -and -not [string]::IsNullOrWhiteSpace($resultDriverPath)) {
|
||||
$modelKey = if (-not [string]::IsNullOrWhiteSpace($lookupModelName)) { $lookupModelName } else { 'Unknown model' }
|
||||
$driverMetadata = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($lookupModelName) -and $driverLookup.ContainsKey($lookupModelName)) {
|
||||
$driverMetadata = $driverLookup[$lookupModelName]
|
||||
}
|
||||
|
||||
if ($driverMetadata) {
|
||||
$driverRecord = [PSCustomObject]@{
|
||||
Make = $driverMetadata.Make
|
||||
Model = $modelKey
|
||||
DriverPath = $resultDriverPath
|
||||
}
|
||||
if ($driverMetadata.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.SystemId)) {
|
||||
$driverRecord | Add-Member -NotePropertyName SystemId -NotePropertyValue $driverMetadata.SystemId
|
||||
}
|
||||
if ($driverMetadata.PSObject.Properties['MachineType'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.MachineType)) {
|
||||
$driverRecord | Add-Member -NotePropertyName MachineType -NotePropertyValue $driverMetadata.MachineType
|
||||
}
|
||||
if ($driverMetadata.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.ProductName)) {
|
||||
$driverRecord | Add-Member -NotePropertyName ProductName -NotePropertyValue $driverMetadata.ProductName
|
||||
}
|
||||
$successfullyDownloaded.Add($driverRecord)
|
||||
}
|
||||
else {
|
||||
WriteLog "Warning: Could not find driver metadata for successful download of model '$modelKey'. Skipping from DriverMapping.json."
|
||||
}
|
||||
}
|
||||
else {
|
||||
$failureModel = if (-not [string]::IsNullOrWhiteSpace($lookupModelName)) { $lookupModelName } else { 'Unknown model' }
|
||||
$failureStatus = if (-not [string]::IsNullOrWhiteSpace($resultStatus)) { $resultStatus } else { 'Driver download failed without a status message. Check the log for details.' }
|
||||
$failedDownloads.Add([PSCustomObject]@{
|
||||
Model = $failureModel
|
||||
Status = $failureStatus
|
||||
})
|
||||
}
|
||||
else {
|
||||
WriteLog "Warning: Could not find 'Make' for successful download of model '$modelName'. Skipping from DriverMapping.json."
|
||||
}
|
||||
}
|
||||
else {
|
||||
$logMessage = "Driver download failed or did not return a path for model '$modelName'. Status: $($result['Status'])"
|
||||
WriteLog $logMessage
|
||||
Write-Warning $logMessage
|
||||
WriteLog "Driver download failed for '$failureModel'. Status: $failureStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4977,7 +5146,13 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
|
||||
WriteLog "Invoke-ParallelProcessing returned null or no results."
|
||||
}
|
||||
|
||||
# Update the driver mapping JSON if there are any successful downloads
|
||||
if ($failedDownloads.Count -gt 0) {
|
||||
$firstFailure = $failedDownloads[0]
|
||||
$errorMessage = "Driver download failed for model '$($firstFailure.Model)': $($firstFailure.Status)"
|
||||
WriteLog $errorMessage
|
||||
throw $errorMessage
|
||||
}
|
||||
|
||||
if ($successfullyDownloaded.Count -gt 0) {
|
||||
try {
|
||||
WriteLog "Updating DriverMapping.json with $($successfullyDownloaded.Count) successfully downloaded drivers."
|
||||
@@ -4989,51 +5164,8 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
|
||||
Write-Warning "The driver download process completed, but failed to update the DriverMapping.json file. Please check the log for details."
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog "Finished processing drivers from $driversJsonPath."
|
||||
|
||||
# After processing, update the driver mapping file
|
||||
$successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
if ($null -ne $parallelResults) {
|
||||
# Create a lookup table from the original items to get the 'Make'
|
||||
$makeLookup = @{}
|
||||
$driversToProcess | ForEach-Object { $makeLookup[$_.Model] = $_.Make }
|
||||
|
||||
foreach ($result in $parallelResults) {
|
||||
if ($null -ne $result) {
|
||||
# Collect successful results for driver mapping
|
||||
if ($result.PSObject.Properties['Success'] -and $result.Success -and $result.PSObject.Properties['DriverPath'] -and -not [string]::IsNullOrWhiteSpace($result.DriverPath)) {
|
||||
$modelName = if ($result.PSObject.Properties.Name -contains 'Identifier') { $result.Identifier } else { $result.Model }
|
||||
|
||||
# Find the 'Make' from the original list
|
||||
$makeJson = $makeLookup[$modelName]
|
||||
|
||||
if ($makeJson) {
|
||||
$successfullyDownloaded.Add([PSCustomObject]@{
|
||||
Make = $makeJson
|
||||
Model = $modelName
|
||||
DriverPath = $result.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 $DriversFolder
|
||||
}
|
||||
catch {
|
||||
WriteLog "Warning: Failed to update DriverMapping.json: $($_.Exception.Message)"
|
||||
# This is not a fatal error for the build process itself, so just show a warning.
|
||||
Write-Warning "The driver download process completed, but failed to update the DriverMapping.json file. Please check the log for details."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# Existing single-model driver download logic
|
||||
@@ -5128,7 +5260,7 @@ if ($InstallApps) {
|
||||
# If there are no existing apps, use the original AppList.json directly
|
||||
if (-not $hasExistingApps) {
|
||||
WriteLog "No existing applications found. Using original AppList.json for all apps."
|
||||
Get-Apps -AppList $AppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath
|
||||
Get-Apps -AppList $AppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads
|
||||
}
|
||||
else {
|
||||
# Compare apps in AppList.json with existing installations
|
||||
@@ -5200,7 +5332,7 @@ if ($InstallApps) {
|
||||
|
||||
# Download missing apps
|
||||
WriteLog "Downloading missing applications"
|
||||
Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath
|
||||
Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads
|
||||
|
||||
# Cleanup modified app list
|
||||
Remove-Item -Path $modifiedAppListPath -Force
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
This script acts as the primary host for the UI, connecting the user interface with the underlying build and logic modules.
|
||||
#>
|
||||
#Requires -RunAsAdministrator
|
||||
|
||||
[CmdletBinding()]
|
||||
[System.STAThread()]
|
||||
|
||||
@@ -359,7 +359,7 @@
|
||||
|
||||
<!-- Arguments -->
|
||||
<TextBlock Text="Arguments:" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="txtAppArguments" Margin="0,0,0,10" ToolTip="Enter the arguments for the command line. If the application is an msi, the command line should only contain msiexec and the rest of the command line arguments would go here (e.g. /i D:\Win32\Mozilla firefox\setup.msi /qn /norestart)."/>
|
||||
<TextBox x:Name="txtAppArguments" Margin="0,0,0,10" ToolTip="Enter the arguments for the command line. If the application is an msi, the command line should only contain msiexec and the rest of the command line arguments would go here (e.g. /i "D:\Win32\Mozilla firefox\setup.msi" /qn /norestart)."/>
|
||||
|
||||
<!-- Source -->
|
||||
<TextBlock Text="Source:" Margin="0,0,0,5"/>
|
||||
@@ -641,7 +641,7 @@
|
||||
<TabItem Header="Build" Padding="20">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="10">
|
||||
<!-- Define 10 rows for the Build tab -->
|
||||
<!-- Define 12 rows for the Build tab -->
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Row 0: Header -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
@@ -657,13 +657,15 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- Row 6: Threads -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- Row 7: General Build Options Header -->
|
||||
<!-- Row 7: BITS Priority -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- Row 8: General Build Options Checkboxes -->
|
||||
<!-- Row 8: General Build Options Header -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- Row 9: Build USB Drive Section -->
|
||||
<!-- Row 9: General Build Options Checkboxes -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- Row 10: Post-Build Cleanup -->
|
||||
<!-- Row 10: Build USB Drive Section -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- Row 11: Post-Build Cleanup -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -729,11 +731,25 @@
|
||||
<TextBlock Grid.Column="0" Text="Threads" VerticalAlignment="Center" ToolTip="Controls the number of parallel threads used by ForEach-Object -Parallel and sets the value of the -ThrottleLimit parameter. Default is 5. Used in Winget, Application Copy, and driver downloads"/>
|
||||
<TextBox x:Name="txtThreads" Grid.Column="1" Margin="5" VerticalAlignment="Center" Width="50" HorizontalAlignment="Left" Text="5" ToolTip="Controls the number of parallel threads used by ForEach-Object -Parallel and sets the value of the -ThrottleLimit parameter. Default is 5. Used in Winget, Application Copy, and driver downloads"/>
|
||||
</Grid>
|
||||
<!-- Row 7: General Build Options Header -->
|
||||
<TextBlock Grid.Row="7" Text="General Build Options" FontWeight="Bold" FontSize="16" Margin="0,10,0,5"/>
|
||||
<!-- Row 7: BITS Priority -->
|
||||
<Grid Grid.Row="7" Margin="0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="BITS Priority" VerticalAlignment="Center" ToolTip="Controls the BITS download priority used by the UI and BuildFFUVM.ps1. Switch to Foreground to maximize download speed if needed."/>
|
||||
<ComboBox x:Name="cmbBitsPriority" Grid.Column="1" Margin="5" VerticalAlignment="Center" Width="150" HorizontalAlignment="Left" ToolTip="Controls the BITS download priority used by the UI and BuildFFUVM.ps1. Switch to Foreground to maximize download speed if needed.">
|
||||
<sys:String>Foreground</sys:String>
|
||||
<sys:String>High</sys:String>
|
||||
<sys:String>Normal</sys:String>
|
||||
<sys:String>Low</sys:String>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
<!-- Row 8: General Build Options Header -->
|
||||
<TextBlock Grid.Row="8" Text="General Build Options" FontWeight="Bold" FontSize="16" Margin="0,10,0,5"/>
|
||||
|
||||
<!-- Row 8: General Build Options Checkboxes -->
|
||||
<WrapPanel Grid.Row="8" Margin="0,5">
|
||||
<!-- Row 9: General Build Options Checkboxes -->
|
||||
<WrapPanel Grid.Row="9" Margin="0,5">
|
||||
<CheckBox x:Name="chkBuildUSBDriveEnable" Content="Build USB Drive" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will partition and format a USB drive and copy the captured FFU to the drive."/>
|
||||
<CheckBox x:Name="chkCompactOS" Content="Compact OS" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will compact the OS when building the FFU."/>
|
||||
<CheckBox x:Name="chkUpdateADK" Content="Update ADK" Margin="5" VerticalAlignment="Center" Tag="When set to $true, the script will check for and install/update to the latest Windows ADK and WinPE add-on."/>
|
||||
@@ -745,8 +761,8 @@
|
||||
<CheckBox x:Name="chkVerbose" Content="Verbose" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will enable write-verbose output to the console for the build script."/>
|
||||
</WrapPanel>
|
||||
|
||||
<!-- Row 9: Build USB Drive Section -->
|
||||
<StackPanel Grid.Row="9" Margin="0,10,0,5" x:Name="usbDriveSection" Visibility="Collapsed">
|
||||
<!-- Row 10: Build USB Drive Section -->
|
||||
<StackPanel Grid.Row="10" Margin="0,10,0,5" x:Name="usbDriveSection" Visibility="Collapsed">
|
||||
<TextBlock Text="Build USB Drive Settings" FontWeight="Bold" FontSize="16" Margin="0,0,0,10"/>
|
||||
<StackPanel Margin="5,0,0,10">
|
||||
<CheckBox x:Name="chkAllowExternalHardDiskMedia" Content="Allow External Hard Disk Media" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will allow the use of external hard disk media."/>
|
||||
@@ -802,7 +818,7 @@
|
||||
<GridView>
|
||||
|
||||
<GridViewColumn Header="Model" DisplayMemberBinding="{Binding Model}" Width="200"/>
|
||||
<GridViewColumn Header="Serial Number" DisplayMemberBinding="{Binding SerialNumber}" Width="150"/>
|
||||
<GridViewColumn Header="Unique ID" DisplayMemberBinding="{Binding UniqueId}" Width="300"/>
|
||||
<GridViewColumn Header="Size (GB)" DisplayMemberBinding="{Binding Size}" Width="80"/>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
@@ -811,8 +827,8 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Row 10: Post-Build Cleanup -->
|
||||
<StackPanel Grid.Row="10" Margin="0,10,0,5">
|
||||
<!-- Row 11: Post-Build Cleanup -->
|
||||
<StackPanel Grid.Row="11" Margin="0,10,0,5">
|
||||
<TextBlock Text="Post-Build Cleanup" FontWeight="Bold" FontSize="16" Margin="0,0,0,5"/>
|
||||
<CheckBox x:Name="chkCleanupAppsISO" Content="Cleanup Apps ISO" Margin="5" VerticalAlignment="Center" Tag="Remove Apps ISO after FFU capture."/>
|
||||
<CheckBox x:Name="chkCleanupCaptureISO" Content="Cleanup Capture ISO" Margin="5" VerticalAlignment="Center" Tag="Remove WinPE capture ISO after FFU capture."/>
|
||||
|
||||
@@ -12,6 +12,10 @@ $script:CommonCoreLogFilePath = $null
|
||||
# Mutex for log file access
|
||||
$script:commonCoreLogMutexName = "Global\FFUCommonCoreLogMutex" # Unique name
|
||||
$script:commonCoreLogMutex = New-Object System.Threading.Mutex($false, $script:commonCoreLogMutexName)
|
||||
$script:BitsTransferPriority = 'Normal'
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:FFU_BITS_PRIORITY)) {
|
||||
$script:BitsTransferPriority = $env:FFU_BITS_PRIORITY
|
||||
}
|
||||
|
||||
# Function to set the log file path for this module
|
||||
function Set-CommonCoreLogPath {
|
||||
@@ -31,6 +35,23 @@ function Set-CommonCoreLogPath {
|
||||
}
|
||||
}
|
||||
|
||||
function Set-BitsTransferPriority {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('Foreground', 'High', 'Normal', 'Low')]
|
||||
[string]$Priority
|
||||
)
|
||||
$script:BitsTransferPriority = $Priority
|
||||
try {
|
||||
Set-Item -Path Env:FFU_BITS_PRIORITY -Value $Priority -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to set FFU_BITS_PRIORITY environment variable: $($_.Exception.Message)"
|
||||
}
|
||||
WriteLog "BITS transfer priority set to $Priority."
|
||||
}
|
||||
|
||||
# Centralized WriteLog function
|
||||
function WriteLog {
|
||||
[CmdletBinding()]
|
||||
@@ -143,20 +164,36 @@ function Start-BitsTransferWithRetry {
|
||||
[string]$Source,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Destination,
|
||||
[int]$Retries = 3
|
||||
[int]$Retries = 3,
|
||||
[ValidateSet('Foreground','High','Normal','Low')]
|
||||
[string]$Priority
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Priority)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:FFU_BITS_PRIORITY)) {
|
||||
$Priority = $env:FFU_BITS_PRIORITY
|
||||
}
|
||||
elseif (-not [string]::IsNullOrWhiteSpace($script:BitsTransferPriority)) {
|
||||
$Priority = $script:BitsTransferPriority
|
||||
}
|
||||
else {
|
||||
$Priority = 'Normal'
|
||||
}
|
||||
}
|
||||
|
||||
$attempt = 0
|
||||
$lastError = $null
|
||||
$notLoggedOnHResult = [int]0x800704dd
|
||||
$fallbackTriggered = $false
|
||||
|
||||
while ($attempt -lt $Retries) {
|
||||
while ($attempt -lt $Retries -and -not $fallbackTriggered) {
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$OriginalProgressPreference = $ProgressPreference
|
||||
try {
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
Start-BitsTransfer -Source $Source -Destination $Destination -ErrorAction Stop
|
||||
Start-BitsTransfer -Source $Source -Destination $Destination -Priority $Priority -ErrorAction Stop
|
||||
|
||||
$ProgressPreference = $OriginalProgressPreference
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
@@ -166,7 +203,24 @@ function Start-BitsTransferWithRetry {
|
||||
catch {
|
||||
$lastError = $_
|
||||
$attempt++
|
||||
WriteLog "Attempt $attempt of $Retries failed to download $Source. Error: $($lastError.Exception.Message)."
|
||||
$errorMessage = $lastError.Exception.Message
|
||||
WriteLog "Attempt $attempt of $Retries failed to download $Source. Error: $errorMessage."
|
||||
$hResult = $null
|
||||
if ($null -ne $lastError.Exception) {
|
||||
$hResult = $lastError.Exception.HResult
|
||||
}
|
||||
$needsHttpFallback = $false
|
||||
if ($hResult -eq $notLoggedOnHResult) {
|
||||
$needsHttpFallback = $true
|
||||
}
|
||||
elseif ($errorMessage -match '0x800704DD' -or $errorMessage -match 'not.*logged on to the network') {
|
||||
$needsHttpFallback = $true
|
||||
}
|
||||
if ($needsHttpFallback) {
|
||||
WriteLog "BITS cannot download $Source because the current session is not logged on to the network. Falling back to Invoke-WebRequest."
|
||||
$fallbackTriggered = $true
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds (1 * $attempt)
|
||||
}
|
||||
finally {
|
||||
@@ -179,6 +233,41 @@ function Start-BitsTransferWithRetry {
|
||||
}
|
||||
}
|
||||
|
||||
if ($fallbackTriggered) {
|
||||
$remainingAttempts = $Retries - $attempt
|
||||
if ($remainingAttempts -lt 1) {
|
||||
$remainingAttempts = 1
|
||||
}
|
||||
$httpAttempt = 0
|
||||
while ($httpAttempt -lt $remainingAttempts) {
|
||||
$httpAttempt++
|
||||
$OriginalVerbosePreference = $VerbosePreference
|
||||
$OriginalProgressPreference = $ProgressPreference
|
||||
try {
|
||||
$VerbosePreference = 'SilentlyContinue'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
Invoke-WebRequest -Uri $Source -OutFile $Destination -ErrorAction Stop
|
||||
$ProgressPreference = $OriginalProgressPreference
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
WriteLog "Successfully transferred $Source to $Destination via HTTP fallback."
|
||||
return
|
||||
}
|
||||
catch {
|
||||
$lastError = $_
|
||||
WriteLog "HTTP fallback attempt $httpAttempt of $remainingAttempts failed to download $Source. Error: $($lastError.Exception.Message)."
|
||||
Start-Sleep -Seconds (1 * $httpAttempt)
|
||||
}
|
||||
finally {
|
||||
if (Get-Variable -Name 'OriginalProgressPreference' -ErrorAction SilentlyContinue) {
|
||||
$ProgressPreference = $OriginalProgressPreference
|
||||
}
|
||||
if (Get-Variable -Name 'OriginalVerbosePreference' -ErrorAction SilentlyContinue) {
|
||||
$VerbosePreference = $OriginalVerbosePreference
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog "Failed to download $Source after $Retries attempts. Last Error: $($lastError.Exception.Message)"
|
||||
throw $lastError
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
<#
|
||||
.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
|
||||
# Normalize model display using GroupManifest Display CDATA if available (strip 'PDK Catalog for')
|
||||
$gmDisplayNode = $doc.SelectSingleNode("/*[local-name()='GroupManifest']/*[local-name()='Display']")
|
||||
$modelFull = $null
|
||||
if ($gmDisplayNode -and $gmDisplayNode.InnerText) {
|
||||
$rawDisplay = $gmDisplayNode.InnerText.Trim()
|
||||
$modelFull = ($rawDisplay -replace '^\s*PDK Catalog for\s+','').Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($modelFull)) {
|
||||
# Fallback: assemble from brand/model nodes (legacy heuristic)
|
||||
$prefixedModelNumber = $modelNumber
|
||||
if ($modelNumber -and $brandDisplay) {
|
||||
if ($modelNumber.StartsWith($brandDisplay,[System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$prefixedModelNumber = $modelNumber
|
||||
}
|
||||
else {
|
||||
$prefixedModelNumber = "$brandDisplay $modelNumber"
|
||||
}
|
||||
}
|
||||
elseif ($brandDisplay -and -not $modelNumber) {
|
||||
$prefixedModelNumber = $brandDisplay
|
||||
}
|
||||
$modelFull = $prefixedModelNumber
|
||||
}
|
||||
$modelDisplay = "$modelFull ($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 '-'
|
||||
|
||||
# Capture a human‑readable driver name (preserve spaces like HP/Lenovo; remove only illegal path chars and extra whitespace)
|
||||
$displayNode = $comp.SelectSingleNode("*[local-name()='Name']/*[local-name()='Display']")
|
||||
$nameRaw = if ($displayNode) { $displayNode.InnerText.Trim() } else { $fileName }
|
||||
# Remove characters not suitable for display (and disallowed in file names) but keep spaces
|
||||
$nameDisplay = $nameRaw -replace '[\\\/:\*\?\"\<\>\|]', ' ' -replace '[,]', '-'
|
||||
# Collapse multiple spaces to single
|
||||
$nameDisplay = ($nameDisplay -replace '\s+', ' ').Trim()
|
||||
|
||||
$rawPackages.Add([pscustomobject]@{
|
||||
Path = $path
|
||||
DownloadUrl = $downloadUrl
|
||||
FileName = $fileName
|
||||
Name = $nameDisplay
|
||||
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
|
||||
Name = $pkg.Name
|
||||
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
|
||||
}
|
||||
|
||||
# Resolve a Dell per‑model CabUrl when missing by inspecting CatalogIndexPC
|
||||
function Resolve-DellCabUrlFromModel {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$DriversFolder,
|
||||
[Parameter()][string]$ModelDisplay,
|
||||
[Parameter()][string]$SystemId
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SystemId) -and -not [string]::IsNullOrWhiteSpace($ModelDisplay)) {
|
||||
# Try to parse the trailing (XXXX) token (SystemId)
|
||||
if ($ModelDisplay -match '\(([0-9A-Fa-f]{4})\)\s*$') {
|
||||
$SystemId = $matches[1].ToUpperInvariant()
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SystemId)) {
|
||||
WriteLog "Resolve-DellCabUrlFromModel: No SystemId could be determined from '$ModelDisplay'."
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$indexXml = Get-DellCatalogIndex -DriversFolder $DriversFolder
|
||||
# Reuse existing model parsing to avoid duplicating streaming logic
|
||||
$allModels = Get-DellClientModels -CatalogIndexXmlPath $indexXml
|
||||
$match = $allModels | Where-Object { $_.SystemId -eq $SystemId } | Select-Object -First 1
|
||||
if ($null -eq $match) {
|
||||
WriteLog "Resolve-DellCabUrlFromModel: SystemId '$SystemId' not found in CatalogIndexPC.xml."
|
||||
return $null
|
||||
}
|
||||
WriteLog "Resolve-DellCabUrlFromModel: Resolved CabUrl for '$($match.ModelDisplay)' -> $($match.CabUrl)"
|
||||
return [pscustomobject]@{
|
||||
Brand = $match.Brand
|
||||
ModelNumber = $match.ModelNumber
|
||||
SystemId = $match.SystemId
|
||||
CabRelativePath = $match.CabRelativePath
|
||||
CabUrl = $match.CabUrl
|
||||
ModelDisplay = $match.ModelDisplay
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Resolve-DellCabUrlFromModel: Failure resolving CabUrl for '$ModelDisplay' / SystemId '$SystemId' : $($_.Exception.Message)"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Convert-DellVendorVersion,Compare-DellVendorVersion,Get-DellCatalogIndex,Get-DellClientModels,Get-DellLatestDriverPackages,Resolve-DellCabUrlFromModel
|
||||
@@ -155,31 +155,185 @@ function Update-DriverMappingJson {
|
||||
$updatedCount = 0
|
||||
$addedCount = 0
|
||||
|
||||
$hpSystemIdCache = @{}
|
||||
$normalizeHpName = {
|
||||
param([string]$text)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return ([regex]::Replace($text.ToLowerInvariant(), '[^a-z0-9]', ''))
|
||||
}
|
||||
$getHpSystemId = {
|
||||
param([string]$modelName)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($modelName)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($hpSystemIdCache.ContainsKey($modelName)) {
|
||||
return $hpSystemIdCache[$modelName]
|
||||
}
|
||||
|
||||
$hpFolder = Join-Path -Path $DriversFolder -ChildPath 'HP'
|
||||
if (-not (Test-Path -Path $hpFolder -PathType Container)) {
|
||||
$hpSystemIdCache[$modelName] = $null
|
||||
return $null
|
||||
}
|
||||
|
||||
$platformListXml = Join-Path -Path $hpFolder -ChildPath 'PlatformList.xml'
|
||||
$platformListCab = Join-Path -Path $hpFolder -ChildPath 'platformList.cab'
|
||||
if (-not (Test-Path -Path $platformListXml -PathType Leaf)) {
|
||||
try {
|
||||
WriteLog "Attempting to refresh HP PlatformList.xml for SystemID lookup."
|
||||
Start-BitsTransferWithRetry -Source 'https://hpia.hpcloud.hp.com/ref/platformList.cab' -Destination $platformListCab -ErrorAction Stop
|
||||
if (Test-Path -Path $platformListXml) { Remove-Item -Path $platformListXml -Force -ErrorAction SilentlyContinue }
|
||||
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$platformListCab`"", "`"$platformListXml`"") -ErrorAction Stop | Out-Null
|
||||
if (Test-Path -Path $platformListCab) { Remove-Item -Path $platformListCab -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to refresh HP PlatformList.xml: $($_.Exception.Message)"
|
||||
$hpSystemIdCache[$modelName] = $null
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
[xml]$platformListContent = Get-Content -Path $platformListXml -Raw -Encoding UTF8 -ErrorAction Stop
|
||||
|
||||
$targetName = $modelName.Trim()
|
||||
$normalizedTarget = & $normalizeHpName $targetName
|
||||
|
||||
$modelMatch = $platformListContent.ImagePal.Platform | Where-Object {
|
||||
[string]::Equals($_.ProductName.'#text'.Trim(), $targetName, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} | Select-Object -First 1
|
||||
|
||||
if (-not $modelMatch -and $normalizedTarget) {
|
||||
$modelMatch = $platformListContent.ImagePal.Platform | Where-Object {
|
||||
$candidateName = $_.ProductName.'#text'
|
||||
$normalizedCandidate = & $normalizeHpName $candidateName
|
||||
$normalizedCandidate -eq $normalizedTarget
|
||||
} | Select-Object -First 1
|
||||
}
|
||||
|
||||
if (-not $modelMatch -and $normalizedTarget) {
|
||||
$modelMatch = $platformListContent.ImagePal.Platform | Where-Object {
|
||||
$candidateName = $_.ProductName.'#text'
|
||||
$normalizedCandidate = & $normalizeHpName $candidateName
|
||||
($normalizedCandidate -like "*$normalizedTarget*") -or ($normalizedTarget -like "*$normalizedCandidate*")
|
||||
} | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ($modelMatch -and -not [string]::IsNullOrWhiteSpace($modelMatch.SystemID)) {
|
||||
$resolvedId = $modelMatch.SystemID.Trim().ToUpperInvariant()
|
||||
$hpSystemIdCache[$modelName] = $resolvedId
|
||||
return $resolvedId
|
||||
}
|
||||
else {
|
||||
WriteLog "HP SystemId lookup: no match found in PlatformList.xml for model '$modelName'."
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to parse HP PlatformList.xml for model '$modelName': $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$hpSystemIdCache[$modelName] = $null
|
||||
return $null
|
||||
}
|
||||
|
||||
foreach ($driver in $DownloadedDrivers) {
|
||||
# Skip if any required property is missing or null
|
||||
if (-not $driver.PSObject.Properties['Make'] -or -not $driver.PSObject.Properties['Model'] -or -not $driver.PSObject.Properties['DriverPath'] -or [string]::IsNullOrWhiteSpace($driver.DriverPath)) {
|
||||
WriteLog "Skipping driver entry due to missing or empty Make, Model, or DriverPath. Details: $(($driver | ConvertTo-Json -Compress -Depth 3))"
|
||||
continue
|
||||
}
|
||||
|
||||
# Find existing entry
|
||||
$systemIdValue = $null
|
||||
$machineTypeValue = $null
|
||||
|
||||
if ($driver.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driver.SystemId)) {
|
||||
$systemIdValue = $driver.SystemId.Trim().ToUpperInvariant()
|
||||
}
|
||||
if ($driver.PSObject.Properties['MachineType'] -and -not [string]::IsNullOrWhiteSpace($driver.MachineType)) {
|
||||
$machineTypeValue = $driver.MachineType.Trim()
|
||||
}
|
||||
|
||||
switch ($driver.Make) {
|
||||
'Dell' {
|
||||
if (-not $systemIdValue -and $driver.Model -match '\(([^)]+)\)\s*$') {
|
||||
$systemIdValue = $matches[1].Trim().ToUpperInvariant()
|
||||
}
|
||||
}
|
||||
'HP' {
|
||||
if (-not $systemIdValue) {
|
||||
$systemIdValue = & $getHpSystemId $driver.Model
|
||||
}
|
||||
}
|
||||
'Lenovo' {
|
||||
if (-not $machineTypeValue -and $driver.Model -match '\(([^)]+)\)\s*$') {
|
||||
$machineTypeValue = $matches[1].Trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$existingEntry = $mappingList | Where-Object { $_.Manufacturer -eq $driver.Make -and $_.Model -eq $driver.Model } | Select-Object -First 1
|
||||
|
||||
if ($null -ne $existingEntry) {
|
||||
# Update existing entry if the path is different
|
||||
$entryUpdated = $false
|
||||
if ($existingEntry.DriverPath -ne $driver.DriverPath) {
|
||||
WriteLog "Updating driver path for '$($driver.Make) - $($driver.Model)' from '$($existingEntry.DriverPath)' to '$($driver.DriverPath)'."
|
||||
$existingEntry.DriverPath = $driver.DriverPath
|
||||
$entryUpdated = $true
|
||||
}
|
||||
|
||||
if ($driver.Make -in @('HP', 'Dell') -and -not [string]::IsNullOrWhiteSpace($systemIdValue)) {
|
||||
if ($existingEntry.PSObject.Properties['SystemId']) {
|
||||
if ($existingEntry.SystemId -ne $systemIdValue) {
|
||||
WriteLog "Updating SystemId for '$($driver.Make) - $($driver.Model)' to '$systemIdValue'."
|
||||
$existingEntry.SystemId = $systemIdValue
|
||||
$entryUpdated = $true
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "Adding SystemId '$systemIdValue' for '$($driver.Make) - $($driver.Model)'."
|
||||
$existingEntry | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemIdValue
|
||||
$entryUpdated = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($driver.Make -eq 'Lenovo' -and -not [string]::IsNullOrWhiteSpace($machineTypeValue)) {
|
||||
if ($existingEntry.PSObject.Properties['MachineType']) {
|
||||
if ($existingEntry.MachineType -ne $machineTypeValue) {
|
||||
WriteLog "Updating MachineType for '$($driver.Make) - $($driver.Model)' to '$machineTypeValue'."
|
||||
$existingEntry.MachineType = $machineTypeValue
|
||||
$entryUpdated = $true
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "Adding MachineType '$machineTypeValue' for '$($driver.Make) - $($driver.Model)'."
|
||||
$existingEntry | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineTypeValue
|
||||
$entryUpdated = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($entryUpdated) {
|
||||
$updatedCount++
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Add new entry
|
||||
$newEntry = [PSCustomObject]@{
|
||||
Manufacturer = $driver.Make
|
||||
Model = $driver.Model
|
||||
DriverPath = $driver.DriverPath
|
||||
}
|
||||
|
||||
if ($driver.Make -in @('HP', 'Dell') -and -not [string]::IsNullOrWhiteSpace($systemIdValue)) {
|
||||
$newEntry | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemIdValue
|
||||
}
|
||||
if ($driver.Make -eq 'Lenovo' -and -not [string]::IsNullOrWhiteSpace($machineTypeValue)) {
|
||||
$newEntry | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineTypeValue
|
||||
}
|
||||
|
||||
$mappingList.Add($newEntry)
|
||||
WriteLog "Adding new mapping for '$($driver.Make) - $($driver.Model)' with path '$($driver.DriverPath)'."
|
||||
$addedCount++
|
||||
@@ -292,113 +446,330 @@ function Get-LenovoPSREFToken {
|
||||
if your alternative works is to see if you can retrieve 100e, 300w, 500w, etc. These don't show up in catalogv2.xml, but they do in PSREF.
|
||||
#>
|
||||
|
||||
# Path to Edge
|
||||
$edgeExe = "$Env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||
$token = $null
|
||||
$socket = $null
|
||||
$edgeProcess = $null
|
||||
$tempProfile = $null
|
||||
$port = $null
|
||||
|
||||
# Any free port works. 9222 is common.
|
||||
$port = 9222
|
||||
$uri = 'https://psref.lenovo.com'
|
||||
|
||||
# Headless run with remote debugging.
|
||||
$flags = "--headless=new --disable-gpu --remote-debugging-port=$port $uri"
|
||||
$edge = Start-Process -FilePath $edgeExe -ArgumentList $flags -PassThru
|
||||
Writelog "Edge process started with PID: $($edge.Id)."
|
||||
|
||||
# Wait a short moment so the target appears.
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# Find the first page target.
|
||||
$targets = Invoke-RestMethod "http://localhost:$port/json"
|
||||
$wsUrl = ($targets | Where-Object type -eq 'page')[0].webSocketDebuggerUrl
|
||||
|
||||
# Connect to that WebSocket.
|
||||
$socket = [System.Net.WebSockets.ClientWebSocket]::new()
|
||||
$socket.ConnectAsync($wsUrl, [Threading.CancellationToken]::None).Wait()
|
||||
|
||||
# Helper to send a DevTools command.
|
||||
function Send-DevToolsCommand {
|
||||
param([int]$id, [string]$method, [hashtable]$params = @{})
|
||||
$cmd = @{ id = $id; method = $method; params = $params } |
|
||||
ConvertTo-Json -Compress
|
||||
$data = [Text.Encoding]::UTF8.GetBytes($cmd)
|
||||
$socket.SendAsync([ArraySegment[byte]]$data, 'Text', $true,
|
||||
[Threading.CancellationToken]::None).Wait()
|
||||
}
|
||||
|
||||
# Ask the page to return localStorage['asut'].
|
||||
Send-DevToolsCommand -id 1 -method 'Runtime.evaluate' -params @{
|
||||
expression = "localStorage.getItem('asut')"
|
||||
}
|
||||
|
||||
# Receive frames until the whole message arrives.
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
$buf = New-Object byte[] 8192
|
||||
do {
|
||||
$seg = [ArraySegment[byte]]::new($buf)
|
||||
$res = $socket.ReceiveAsync($seg,
|
||||
[Threading.CancellationToken]::None).Result
|
||||
$ms.Write($buf, 0, $res.Count)
|
||||
} until ($res.EndOfMessage)
|
||||
|
||||
$ms.Position = 0
|
||||
$json = ([System.IO.StreamReader]::new($ms, [Text.Encoding]::UTF8)).ReadToEnd() |
|
||||
ConvertFrom-Json
|
||||
|
||||
$token = $json.result.result.value
|
||||
# Concatenate the token value with X-PSREF-USER-TOKEN=
|
||||
$token = "X-PSREF-USER-TOKEN=$token"
|
||||
WriteLog "Retrieved Lenovo PSREF token: $token"
|
||||
|
||||
# Clean up.
|
||||
$socket.Dispose()
|
||||
|
||||
if ($null -ne $socket) {
|
||||
$socket.Dispose()
|
||||
}
|
||||
|
||||
# Find the PID listening on the debugging port for reliable termination.
|
||||
$listeningPid = $null
|
||||
function Get-FreeLocalTcpPort {
|
||||
$listener = $null
|
||||
try {
|
||||
# Find the process listening on the specific port. The regex now looks for the local address and port, followed by anything, then LISTENING.
|
||||
# Dots are escaped for literal matching.
|
||||
$netstatOutput = netstat -ano -p TCP | Where-Object { $_ -match "127\.0\.0\.1:$port.*LISTENING" }
|
||||
if ($netstatOutput) {
|
||||
# The last number in the line is the PID
|
||||
$listeningPid = ($netstatOutput -split '\s+')[-1]
|
||||
WriteLog "Found Edge process PID $listeningPid listening on port $port. This is the process we will terminate."
|
||||
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
||||
$listener.Start()
|
||||
$endpoint = [System.Net.IPEndPoint]$listener.LocalEndpoint
|
||||
return $endpoint.Port
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $listener) {
|
||||
$listener.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-EdgeDevToolsPageTarget {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][int]$Port,
|
||||
[int]$MaxAttempts = 20,
|
||||
[int]$DelayMilliseconds = 500,
|
||||
[string]$UrlContains
|
||||
)
|
||||
|
||||
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
||||
try {
|
||||
$targets = Invoke-RestMethod -Uri "http://localhost:$Port/json" -ErrorAction Stop
|
||||
if ($null -ne $targets) {
|
||||
if ($targets -isnot [System.Array]) { $targets = @($targets) }
|
||||
$pageTargets = $targets | Where-Object { $_.type -eq 'page' }
|
||||
if (-not [string]::IsNullOrWhiteSpace($UrlContains)) {
|
||||
$pageTargets = $pageTargets | Where-Object {
|
||||
-not [string]::IsNullOrWhiteSpace($_.url) -and $_.url -like "*$UrlContains*"
|
||||
}
|
||||
}
|
||||
|
||||
$target = $pageTargets | Select-Object -First 1
|
||||
if ($null -ne $target) {
|
||||
return $target
|
||||
}
|
||||
|
||||
WriteLog "DevTools endpoint on port $Port returned targets but no page matched the criteria (attempt $attempt of $MaxAttempts)."
|
||||
}
|
||||
else {
|
||||
WriteLog "Could not find any process listening on port $port."
|
||||
WriteLog "DevTools endpoint on port $Port returned no targets (attempt $attempt of $MaxAttempts)."
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Could not run netstat to find listening PID. Error: $($_.Exception.Message)"
|
||||
WriteLog "DevTools endpoint on port $Port not ready (attempt $attempt of $MaxAttempts). Error: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds $DelayMilliseconds
|
||||
}
|
||||
|
||||
throw "Edge DevTools endpoint on port $Port did not expose a matching page target after $MaxAttempts attempts."
|
||||
}
|
||||
|
||||
try {
|
||||
$ffuDevelopmentRoot = Split-Path -Path $PSScriptRoot -Parent
|
||||
WriteLog "Derived FFUDevelopmentPath from module path: $ffuDevelopmentRoot"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ffuDevelopmentRoot)) {
|
||||
throw "FFUDevelopmentPath could not be resolved. Unable to create Edge profile."
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $ffuDevelopmentRoot -PathType Container)) {
|
||||
throw "Resolved FFUDevelopmentPath '$ffuDevelopmentRoot' does not exist."
|
||||
}
|
||||
|
||||
$tempProfile = Join-Path -Path $ffuDevelopmentRoot -ChildPath ("edge-psref-" + [guid]::NewGuid())
|
||||
WriteLog "Creating temporary Edge profile at $tempProfile."
|
||||
New-Item -ItemType Directory -Path $tempProfile -Force | Out-Null
|
||||
|
||||
$edgeExe = "$Env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||
$uri = 'https://psref.lenovo.com'
|
||||
$port = Get-FreeLocalTcpPort
|
||||
WriteLog "Using Edge DevTools port $port for Lenovo PSREF token retrieval."
|
||||
|
||||
$flags = "--headless=new --disable-gpu --remote-debugging-port=$port $uri --user-data-dir=`"$tempProfile`""
|
||||
$edgeProcess = Start-Process -FilePath $edgeExe -ArgumentList $flags -PassThru
|
||||
WriteLog "Edge process started with PID: $($edgeProcess.Id)."
|
||||
|
||||
$pageTarget = Get-EdgeDevToolsPageTarget -Port $port -MaxAttempts 40 -DelayMilliseconds 500 -UrlContains 'psref.lenovo.com'
|
||||
if (-not [string]::IsNullOrWhiteSpace($pageTarget.url)) {
|
||||
WriteLog "Selected DevTools target URL: $($pageTarget.url)"
|
||||
}
|
||||
|
||||
$wsUrl = $pageTarget.webSocketDebuggerUrl
|
||||
if ([string]::IsNullOrWhiteSpace($wsUrl)) {
|
||||
throw "Edge DevTools page target on port $port did not provide a WebSocket URL."
|
||||
}
|
||||
|
||||
$socket = [System.Net.WebSockets.ClientWebSocket]::new()
|
||||
$socket.ConnectAsync($wsUrl, [Threading.CancellationToken]::None).Wait()
|
||||
|
||||
function Send-DevToolsCommand {
|
||||
param([int]$id, [string]$method, [hashtable]$params = @{})
|
||||
$cmd = @{ id = $id; method = $method; params = $params } | ConvertTo-Json -Compress
|
||||
$data = [Text.Encoding]::UTF8.GetBytes($cmd)
|
||||
$socket.SendAsync([ArraySegment[byte]]$data, 'Text', $true, [Threading.CancellationToken]::None).Wait()
|
||||
}
|
||||
|
||||
$buffer = New-Object byte[] 8192
|
||||
|
||||
function Invoke-DevToolsValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][int]$CommandId,
|
||||
[Parameter(Mandatory = $true)][string]$Expression,
|
||||
[int]$MaxPolls = 25
|
||||
)
|
||||
|
||||
Send-DevToolsCommand -id $CommandId -method 'Runtime.evaluate' -params @{
|
||||
expression = $Expression
|
||||
returnByValue = $true
|
||||
awaitPromise = $true
|
||||
}
|
||||
|
||||
for ($poll = 1; $poll -le $MaxPolls; $poll++) {
|
||||
$localStream = $null
|
||||
try {
|
||||
$localStream = New-Object System.IO.MemoryStream
|
||||
do {
|
||||
$segment = [ArraySegment[byte]]::new($buffer)
|
||||
$result = $socket.ReceiveAsync($segment, [Threading.CancellationToken]::None).Result
|
||||
$localStream.Write($buffer, 0, $result.Count)
|
||||
} until ($result.EndOfMessage)
|
||||
|
||||
$jsonBytes = $localStream.ToArray()
|
||||
$jsonText = [Text.Encoding]::UTF8.GetString($jsonBytes)
|
||||
$previewPayload = $jsonText
|
||||
if (-not [string]::IsNullOrEmpty($previewPayload) -and $previewPayload.Length -gt 500) {
|
||||
$previewPayload = $previewPayload.Substring(0, 500) + '...'
|
||||
}
|
||||
WriteLog "DevTools eval payload (cmd $CommandId, poll $poll): $previewPayload"
|
||||
|
||||
$message = $null
|
||||
try {
|
||||
$message = $jsonText | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to parse DevTools eval payload for command id $CommandId (poll $poll): $($_.Exception.Message)"
|
||||
continue
|
||||
}
|
||||
|
||||
if ($message.PSObject.Properties['id'] -and $message.id -eq $CommandId) {
|
||||
if ($message.PSObject.Properties['error']) {
|
||||
$errorMessage = $message.error.message
|
||||
throw "Edge DevTools reported an error for expression '$Expression': $errorMessage"
|
||||
}
|
||||
|
||||
if ($message.PSObject.Properties['result'] -and $message.result.PSObject.Properties['result']) {
|
||||
$innerResult = $message.result.result
|
||||
return [PSCustomObject]@{
|
||||
Value = $innerResult.value
|
||||
Type = $innerResult.type
|
||||
Subtype = $innerResult.subtype
|
||||
}
|
||||
}
|
||||
|
||||
$serializedMessage = $message | ConvertTo-Json -Compress -Depth 5
|
||||
WriteLog "DevTools response for command id $CommandId lacked result data. Message: $serializedMessage"
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($message.PSObject.Properties['method']) {
|
||||
WriteLog "Received DevTools event '$($message.method)' while waiting for command id $CommandId."
|
||||
}
|
||||
else {
|
||||
WriteLog "Received DevTools message without id or method while waiting for command id $CommandId."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $localStream) {
|
||||
$localStream.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw "No DevTools response received for command id $CommandId after $MaxPolls polls."
|
||||
}
|
||||
|
||||
WriteLog "Waiting for PSREF page to initialize local storage context."
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$commandCounter = 1000
|
||||
$rawToken = $null
|
||||
$maxTokenAttempts = 12
|
||||
for ($attempt = 1; $attempt -le $maxTokenAttempts -and [string]::IsNullOrWhiteSpace($rawToken); $attempt++) {
|
||||
$commandCounter++
|
||||
$tokenResponse = Invoke-DevToolsValue -CommandId $commandCounter -Expression "window.localStorage?.getItem('asut')" -MaxPolls 25
|
||||
if ($null -ne $tokenResponse -and -not [string]::IsNullOrWhiteSpace($tokenResponse.Value)) {
|
||||
$rawToken = $tokenResponse.Value
|
||||
WriteLog "DevTools response for command id $commandCounter returned token length $($rawToken.Length)."
|
||||
break
|
||||
}
|
||||
|
||||
WriteLog "Lenovo PSREF token not yet available (attempt $attempt of $maxTokenAttempts)."
|
||||
|
||||
$commandCounter++
|
||||
$keysResponse = Invoke-DevToolsValue -CommandId $commandCounter -Expression "JSON.stringify(Object.keys(window.localStorage || {}))" -MaxPolls 10
|
||||
if ($null -ne $keysResponse -and -not [string]::IsNullOrWhiteSpace($keysResponse.Value)) {
|
||||
WriteLog "Current localStorage keys: $($keysResponse.Value)"
|
||||
}
|
||||
|
||||
$commandCounter++
|
||||
$cookieResponse = Invoke-DevToolsValue -CommandId $commandCounter -Expression "document.cookie" -MaxPolls 10
|
||||
if ($null -ne $cookieResponse -and -not [string]::IsNullOrWhiteSpace($cookieResponse.Value)) {
|
||||
WriteLog "document.cookie contents: $($cookieResponse.Value)"
|
||||
$cookieEntry = ($cookieResponse.Value -split ';') | ForEach-Object { $_.Trim() } | Where-Object { $_ -like 'asut=*' } | Select-Object -First 1
|
||||
if ($cookieEntry) {
|
||||
$rawToken = $cookieEntry.Substring($cookieEntry.IndexOf('=') + 1)
|
||||
WriteLog "Extracted Lenovo PSREF token from cookies with length $($rawToken.Length)."
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 750
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($rawToken)) {
|
||||
throw "Received empty Lenovo PSREF token from Edge DevTools after $maxTokenAttempts attempts."
|
||||
}
|
||||
|
||||
$token = "X-PSREF-USER-TOKEN=$rawToken"
|
||||
WriteLog "Retrieved Lenovo PSREF token: $token"
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to retrieve Lenovo PSREF token. Error: $($_.Exception.Message)"
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $socket) {
|
||||
try {
|
||||
$socket.Dispose()
|
||||
WriteLog "Edge DevTools WebSocket disposed."
|
||||
}
|
||||
catch {
|
||||
WriteLog "Error disposing Edge DevTools WebSocket: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$listeningPid = $null
|
||||
if ($null -ne $port) {
|
||||
try {
|
||||
$netstatOutput = netstat -ano -p TCP | Where-Object { $_ -match "127\.0\.0\.1:$port.*LISTENING" }
|
||||
if ($netstatOutput) {
|
||||
$listeningPid = ($netstatOutput -split '\s+')[-1]
|
||||
WriteLog "Found Edge process PID $listeningPid listening on port $port."
|
||||
}
|
||||
else {
|
||||
WriteLog "No process reported as listening on port $port."
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Could not run netstat to find listening PID for port $port. Error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Determine the correct PID to kill. Prioritize the one found via netstat.
|
||||
$pidToKill = $null
|
||||
if ($listeningPid) {
|
||||
if ($null -ne $listeningPid) {
|
||||
$pidToKill = $listeningPid
|
||||
}
|
||||
elseif ($null -ne $edgeProcess -and -not $edgeProcess.HasExited) {
|
||||
$pidToKill = $edgeProcess.Id
|
||||
WriteLog "Could not find listening process via netstat. Falling back to initial Edge process PID $($pidToKill) for termination."
|
||||
WriteLog "Falling back to initial Edge process PID $pidToKill for termination."
|
||||
}
|
||||
|
||||
if ($pidToKill) {
|
||||
WriteLog "Attempting to terminate Edge process tree with PID: $pidToKill"
|
||||
if ($null -ne $pidToKill) {
|
||||
try {
|
||||
taskkill /PID $pidToKill /T /F | Out-Null
|
||||
WriteLog "Successfully issued termination command for Edge process tree with PID: $pidToKill."
|
||||
WriteLog "Issued termination command for Edge process tree with PID: $pidToKill."
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to terminate Edge process tree with PID: $pidToKill. It may have already closed. Error: $($_.Exception.Message)"
|
||||
WriteLog "Failed to terminate Edge process tree with PID: $pidToKill. Error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "No active Edge process found to terminate."
|
||||
}
|
||||
|
||||
if ($null -ne $edgeProcess) {
|
||||
try {
|
||||
$edgeProcess.WaitForExit(3000) | Out-Null
|
||||
}
|
||||
catch {
|
||||
WriteLog "Error while waiting for Edge process PID $($edgeProcess.Id) to exit: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 250
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($tempProfile) -and (Test-Path -Path $tempProfile -PathType Container)) {
|
||||
$maxRemoveAttempts = 5
|
||||
$originalProgressPreference = $ProgressPreference
|
||||
try {
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
for ($removeAttempt = 1; $removeAttempt -le $maxRemoveAttempts; $removeAttempt++) {
|
||||
try {
|
||||
Remove-Item -Path $tempProfile -Recurse -Force -ErrorAction Stop
|
||||
WriteLog "Removed temporary Edge profile at $tempProfile."
|
||||
break
|
||||
}
|
||||
catch {
|
||||
if ($removeAttempt -eq $maxRemoveAttempts) {
|
||||
WriteLog "Failed to remove temporary Edge profile at $tempProfile after $maxRemoveAttempts attempts. Error: $($_.Exception.Message)"
|
||||
}
|
||||
else {
|
||||
WriteLog "Temporary Edge profile still locked (attempt $removeAttempt of $maxRemoveAttempts). Retrying..."
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$ProgressPreference = $originalProgressPreference
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $token
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ function Invoke-ParallelProcessing {
|
||||
# Execute the appropriate background task based on $localTaskType
|
||||
switch ($localTaskType) {
|
||||
'WingetDownload' {
|
||||
# Pass the progress queue to the task function
|
||||
# Pass the progress queue and SkipWin32Json to the task function
|
||||
$wingetTaskArgs = @{
|
||||
ApplicationItemData = $currentItem
|
||||
AppListJsonPath = $localJobArgs['AppListJsonPath']
|
||||
@@ -164,6 +164,7 @@ function Invoke-ParallelProcessing {
|
||||
OrchestrationPath = $localJobArgs['OrchestrationPath']
|
||||
ProgressQueue = $localProgressQueue
|
||||
WindowsArch = $localJobArgs['WindowsArch']
|
||||
SkipWin32Json = [bool]$localJobArgs['SkipWin32Json']
|
||||
}
|
||||
$taskResult = Start-WingetAppDownloadTask @wingetTaskArgs
|
||||
if ($null -ne $taskResult) {
|
||||
@@ -269,7 +270,7 @@ function Invoke-ParallelProcessing {
|
||||
else {
|
||||
# Fallback for any task that *still* doesn't return 'Success'. This is now the exceptional case.
|
||||
WriteLog "Warning: Task for '$taskSpecificIdentifier' did not return a 'Success' property. Inferring from status: '$($taskResult.Status)'"
|
||||
if ($taskResult.Status -like 'Completed*' -or $taskResult.Status -like 'Already downloaded*') {
|
||||
if ($taskResult.Status -like 'Completed*' -or $taskResult.Status -like 'Already downloaded*' -or $taskResult.Status -like 'Compression successful*') {
|
||||
$resultCode = 0 # Treat as success
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -339,6 +339,324 @@ function Get-Application {
|
||||
|
||||
return $overallResult
|
||||
}
|
||||
# Function to handle downloading a winget application in parallel
|
||||
# This function is called by Invoke-ParallelProcessing for each app
|
||||
function Start-WingetAppDownloadTask {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[PSCustomObject]$ApplicationItemData,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AppListJsonPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AppsPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OrchestrationPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue,
|
||||
[string]$WindowsArch,
|
||||
[switch]$SkipWin32Json
|
||||
)
|
||||
|
||||
$appName = $ApplicationItemData.Name
|
||||
$appId = $ApplicationItemData.Id
|
||||
$source = $ApplicationItemData.Source
|
||||
$status = "Checking..."
|
||||
$resultCode = -1
|
||||
$sanitizedAppName = ConvertTo-SafeName -Name $appName
|
||||
|
||||
# Initial status update
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
|
||||
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
|
||||
|
||||
try {
|
||||
# Define paths
|
||||
$userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json"
|
||||
$appFound = $false
|
||||
|
||||
# 1. Check UserAppList.json and content
|
||||
if (Test-Path -Path $userAppListPath) {
|
||||
try {
|
||||
$userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json
|
||||
$userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName }
|
||||
|
||||
if ($userAppEntry) {
|
||||
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
|
||||
if (Test-Path -Path $appFolder -PathType Container) {
|
||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($folderSize -gt 1MB) {
|
||||
$appFound = $true
|
||||
$status = "Not Downloaded: App in $userAppListPath and found in $appFolder"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog "Found '$appName' in $userAppListPath and content exists in '$appFolder'."
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||
}
|
||||
else {
|
||||
$appFound = $true
|
||||
$status = "App in '$userAppListPath' but content missing/small in '$appFolder'. Copy content or remove from UserAppList.json."
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
|
||||
}
|
||||
}
|
||||
else {
|
||||
$appFound = $true
|
||||
$status = "App in '$userAppListPath' but content folder '$appFolder' not found. Copy content or remove from UserAppList.json."
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Warning: Could not read or parse '$userAppListPath'. Error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Check existing downloaded Win32 content (folder-based)
|
||||
if (-not $appFound -and $source -eq 'winget') {
|
||||
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
|
||||
if (Test-Path -Path $appFolder -PathType Container) {
|
||||
$contentFound = $false
|
||||
if ($ApplicationItemData.Architecture -eq 'x86 x64') {
|
||||
$x86Folder = Join-Path -Path $appFolder -ChildPath "x86"
|
||||
$x64Folder = Join-Path -Path $appFolder -ChildPath "x64"
|
||||
if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) {
|
||||
$x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
$x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($x86Size -gt 1MB -and $x64Size -gt 1MB) {
|
||||
$contentFound = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($folderSize -gt 1MB) {
|
||||
$contentFound = $true
|
||||
}
|
||||
}
|
||||
if ($contentFound) {
|
||||
$appFound = $true
|
||||
$status = "Not Downloaded: Existing content found in $appFolder"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry."
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check MSStore folder
|
||||
if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) {
|
||||
$appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName
|
||||
if (Test-Path -Path $appFolder -PathType Container) {
|
||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($folderSize -gt 1MB) {
|
||||
$appFound = $true
|
||||
$status = "Already downloaded (MSStore)"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog "Found '$appName' content in '$appFolder'."
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 3. If not found locally, add to AppList.json and download
|
||||
if (-not $appFound) {
|
||||
# Add to AppList.json with mutex lock for thread safety
|
||||
$appListContent = $null
|
||||
$appListDir = Split-Path -Path $AppListJsonPath -Parent
|
||||
if (-not (Test-Path -Path $appListDir -PathType Container)) {
|
||||
New-Item -Path $appListDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
try {
|
||||
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if (-not $appListContent.PSObject.Properties['apps']) {
|
||||
$appListContent = @{ apps = @() }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Warning: Could not read or parse '$AppListJsonPath'. Creating new structure. Error: $($_.Exception.Message)"
|
||||
$appListContent = @{ apps = @() }
|
||||
}
|
||||
}
|
||||
else {
|
||||
$appListContent = @{ apps = @() }
|
||||
}
|
||||
|
||||
$appExistsInAppList = $false
|
||||
if ($appListContent.apps) {
|
||||
foreach ($app in $appListContent.apps) {
|
||||
if ($app.id -eq $appId) {
|
||||
$appExistsInAppList = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $appExistsInAppList) {
|
||||
$newApp = @{ name = $sanitizedAppName; id = $appId; source = $source }
|
||||
if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() }
|
||||
$appListContent.apps += $newApp
|
||||
try {
|
||||
# Use a mutex lock to prevent race conditions when writing to the same file
|
||||
$lockName = "AppListJsonLock"
|
||||
$lock = New-Object System.Threading.Mutex($false, $lockName)
|
||||
try {
|
||||
$lock.WaitOne() | Out-Null
|
||||
# Re-read content inside lock to ensure latest version
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
$currentAppListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if (-not ($currentAppListContent.apps | Where-Object { $_.id -eq $appId })) {
|
||||
$currentAppListContent.apps += $newApp
|
||||
$currentAppListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Added '$appName' to '$AppListJsonPath'."
|
||||
}
|
||||
else {
|
||||
WriteLog "'$appName' already exists in '$AppListJsonPath' (checked inside lock)."
|
||||
}
|
||||
}
|
||||
else {
|
||||
# File doesn't exist, write the initial content
|
||||
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Created '$AppListJsonPath' and added '$appName'."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$lock.ReleaseMutex()
|
||||
$lock.Dispose()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Error saving '$AppListJsonPath'. Error: $($_.Exception.Message)"
|
||||
$status = "Failed to save AppList.json: $($_.Exception.Message)"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "'$appName' already exists in '$AppListJsonPath'."
|
||||
}
|
||||
|
||||
# Proceed with download
|
||||
$status = "Downloading..."
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
|
||||
# Ensure necessary folders exist
|
||||
WriteLog "Orchestration Path: $($OrchestrationPath)"
|
||||
if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) {
|
||||
New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32"
|
||||
if ($source -eq "winget" -and -not (Test-Path -Path $win32Folder -PathType Container)) {
|
||||
New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore"
|
||||
if ($source -eq "msstore" -and -not (Test-Path -Path $storeAppsFolder -PathType Container)) {
|
||||
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
# Call Get-Application to perform the actual download
|
||||
# Pass SkipWin32Json based on caller context (UI mode skips, CLI mode creates)
|
||||
$getAppParams = @{
|
||||
AppName = $appName
|
||||
AppId = $appId
|
||||
Source = $source
|
||||
AppsPath = $AppsPath
|
||||
ApplicationArch = $ApplicationItemData.Architecture
|
||||
WindowsArch = $WindowsArch
|
||||
OrchestrationPath = $OrchestrationPath
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
if ($SkipWin32Json) {
|
||||
$getAppParams['SkipWin32Json'] = $true
|
||||
}
|
||||
$resultCode = Get-Application @getAppParams
|
||||
|
||||
# Determine status based on result code
|
||||
switch ($resultCode) {
|
||||
0 { $status = "Downloaded successfully" }
|
||||
1 { $status = "Error: No app installers were found" }
|
||||
2 { $status = "Silent install switch could not be found. Did not download." }
|
||||
3 { $status = "Error: Publisher does not support download" }
|
||||
4 { $status = "Skipped: Use 'msstore' source instead." }
|
||||
default { $status = "Downloaded with status: $resultCode" }
|
||||
}
|
||||
|
||||
# Remove app from AppList.json if silent install switch could not be found (resultCode 2)
|
||||
if ($resultCode -eq 2) {
|
||||
try {
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if ($appListContent.apps) {
|
||||
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
|
||||
$appListContent.apps = $filteredApps
|
||||
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to missing silent install switch."
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$status = $_.Exception.Message
|
||||
WriteLog "Download error for $($appName): $($_.Exception.Message)"
|
||||
$resultCode = 1
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
|
||||
# Remove app from AppList.json if publisher does not support download
|
||||
if ($_.Exception.Message -match "does not support downloads by the publisher") {
|
||||
try {
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if ($appListContent.apps) {
|
||||
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
|
||||
$appListContent.apps = $filteredApps
|
||||
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to publisher download restriction."
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$status = $_.Exception.Message
|
||||
WriteLog "Unexpected error in Start-WingetAppDownloadTask for $($appName): $($_.Exception.Message)"
|
||||
$resultCode = 1
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
finally {
|
||||
# Ensure status is not empty before returning
|
||||
if ([string]::IsNullOrEmpty($status)) {
|
||||
$status = "Unknown failure"
|
||||
WriteLog "Status was empty for $appName ($appId), setting to default error."
|
||||
if ($resultCode -ne 0 -and $resultCode -ne 1 -and $resultCode -ne 2) {
|
||||
$resultCode = -1
|
||||
}
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
elseif ($resultCode -ne 0) {
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
else {
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
}
|
||||
|
||||
# Return the final status and result code
|
||||
return @{ Id = $appId; Status = $status; ResultCode = $resultCode }
|
||||
}
|
||||
|
||||
function Get-Apps {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
@@ -349,28 +667,31 @@ function Get-Apps {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WindowsArch,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OrchestrationPath
|
||||
[string]$OrchestrationPath,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$LogFilePath,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$ThrottleLimit = 5
|
||||
)
|
||||
|
||||
# Load and validate app list
|
||||
$apps = Get-Content -Path $AppList -Raw | ConvertFrom-Json
|
||||
if (-not $apps) {
|
||||
if (-not $apps -or -not $apps.apps -or $apps.apps.Count -eq 0) {
|
||||
WriteLog "No apps were specified in AppList.json file."
|
||||
return
|
||||
}
|
||||
|
||||
# Process WinGet apps
|
||||
# Log app list summary
|
||||
$wingetApps = $apps.apps | Where-Object { $_.source -eq "winget" }
|
||||
if ($wingetApps) {
|
||||
WriteLog 'Winget apps to be installed:'
|
||||
$wingetApps | ForEach-Object { WriteLog $_.Name }
|
||||
}
|
||||
|
||||
# Process Store apps
|
||||
$StoreApps = $apps.apps | Where-Object { $_.source -eq "msstore" }
|
||||
if ($StoreApps) {
|
||||
$storeApps = $apps.apps | Where-Object { $_.source -eq "msstore" }
|
||||
if ($storeApps) {
|
||||
WriteLog 'Store apps to be installed:'
|
||||
$StoreApps | ForEach-Object { WriteLog $_.Name }
|
||||
$storeApps | ForEach-Object { WriteLog $_.Name }
|
||||
}
|
||||
|
||||
# Ensure WinGet is available
|
||||
@@ -380,44 +701,51 @@ function Get-Apps {
|
||||
$win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32"
|
||||
$storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore"
|
||||
|
||||
# Process WinGet apps
|
||||
if ($wingetApps) {
|
||||
if (-not (Test-Path -Path $win32Folder -PathType Container)) {
|
||||
WriteLog "Creating folder for Winget Win32 apps: $win32Folder"
|
||||
New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null
|
||||
WriteLog "Folder created successfully."
|
||||
}
|
||||
|
||||
foreach ($wingetApp in $wingetApps) {
|
||||
try {
|
||||
$appArch = if ($wingetApp.PSObject.Properties['architecture']) { $wingetApp.architecture } else { $WindowsArch }
|
||||
Get-Application -AppName $wingetApp.Name -AppId $wingetApp.Id -Source 'winget' -AppsPath $AppsPath -ApplicationArch $appArch -OrchestrationPath $OrchestrationPath
|
||||
}
|
||||
catch {
|
||||
WriteLog "Error occurred while processing $($wingetApp.Name): $_"
|
||||
throw $_
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Process Store apps
|
||||
if ($StoreApps) {
|
||||
if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) {
|
||||
WriteLog "Creating folder for MSStore apps: $storeAppsFolder"
|
||||
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
foreach ($storeApp in $StoreApps) {
|
||||
try {
|
||||
$appArch = if ($storeApp.PSObject.Properties['architecture']) { $storeApp.architecture } else { $WindowsArch }
|
||||
Get-Application -AppName $storeApp.Name -AppId $storeApp.Id -Source 'msstore' -AppsPath $AppsPath -ApplicationArch $appArch -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath
|
||||
}
|
||||
catch {
|
||||
WriteLog "Error occurred while processing $($storeApp.Name): $_"
|
||||
throw $_
|
||||
# Transform apps into the format expected by Invoke-ParallelProcessing
|
||||
$itemsToProcess = $apps.apps | ForEach-Object {
|
||||
$appArch = if ($_.PSObject.Properties['architecture']) { $_.architecture } else { $WindowsArch }
|
||||
[PSCustomObject]@{
|
||||
Name = $_.name
|
||||
Id = $_.id
|
||||
Source = $_.source
|
||||
Architecture = $appArch
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog "Starting parallel download of $($itemsToProcess.Count) applications with ThrottleLimit: $ThrottleLimit"
|
||||
|
||||
# Build task arguments for Invoke-ParallelProcessing
|
||||
# CLI builds should create WinGetWin32Apps.json, so SkipWin32Json is false
|
||||
$taskArguments = @{
|
||||
AppsPath = $AppsPath
|
||||
AppListJsonPath = $AppList
|
||||
OrchestrationPath = $OrchestrationPath
|
||||
WindowsArch = $WindowsArch
|
||||
SkipWin32Json = $false
|
||||
}
|
||||
|
||||
# Invoke parallel processing in non-UI mode (no WindowObject or ListViewControl)
|
||||
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `
|
||||
-IdentifierProperty 'Id' `
|
||||
-StatusProperty 'DownloadStatus' `
|
||||
-TaskType 'WingetDownload' `
|
||||
-TaskArguments $taskArguments `
|
||||
-CompletedStatusText "Completed" `
|
||||
-ErrorStatusPrefix "Error: " `
|
||||
-MainThreadLogPath $LogFilePath `
|
||||
-ThrottleLimit $ThrottleLimit
|
||||
|
||||
WriteLog "Parallel download of applications completed."
|
||||
|
||||
# Post-processing: Override CommandLine / Arguments from AppList.json if provided
|
||||
# Users may supply custom silent install commands or arguments. These optional
|
||||
# properties (CommandLine, Arguments) in AppList.json replace the auto-generated
|
||||
@@ -749,4 +1077,4 @@ function Add-Win32SilentInstallCommand {
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
# Export functions needed by both BuildFFUVM and the UI Core module
|
||||
Export-ModuleMember -Function Get-Application, Get-Apps, Confirm-WinGetInstallation, Add-Win32SilentInstallCommand, Install-Winget
|
||||
Export-ModuleMember -Function Get-Application, Get-Apps, Start-WingetAppDownloadTask, Confirm-WinGetInstallation, Add-Win32SilentInstallCommand, Install-Winget
|
||||
@@ -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')
|
||||
|
||||
@@ -96,6 +96,7 @@ function Get-UIConfig {
|
||||
USBDriveList = @{}
|
||||
Username = $State.Controls.txtUsername.Text
|
||||
Threads = [int]$State.Controls.txtThreads.Text
|
||||
BitsPriority = $State.Controls.cmbBitsPriority.SelectedItem
|
||||
MaxUSBDrives = [int]$State.Controls.txtMaxUSBDrives.Text
|
||||
Verbose = $State.Controls.chkVerbose.IsChecked
|
||||
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
|
||||
@@ -113,8 +114,9 @@ function Get-UIConfig {
|
||||
WindowsVersion = $State.Controls.cmbWindowsVersion.SelectedItem
|
||||
}
|
||||
|
||||
# Save selected USB drives using UniqueId for reliable identification
|
||||
$State.Controls.lstUSBDrives.Items | Where-Object { $_.IsSelected } | ForEach-Object {
|
||||
$config.USBDriveList[$_.Model] = $_.SerialNumber
|
||||
$config.USBDriveList[$_.Model] = $_.UniqueId
|
||||
}
|
||||
|
||||
# Additional FFU file selections
|
||||
@@ -243,6 +245,55 @@ function Set-UIValue {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ConfigDriverBaseName {
|
||||
param(
|
||||
[string]$RawName
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RawName)) {
|
||||
return $RawName
|
||||
}
|
||||
|
||||
if ($RawName -match '^(.*?)\s*\((.+)\)\s*$') {
|
||||
return $matches[1].Trim()
|
||||
}
|
||||
|
||||
return $RawName.Trim()
|
||||
}
|
||||
|
||||
function Get-ConfigDriverDisplayName {
|
||||
param(
|
||||
[string]$Make,
|
||||
[string]$StoredName,
|
||||
[string]$ProductName,
|
||||
[string]$SystemId,
|
||||
[string]$MachineType
|
||||
)
|
||||
|
||||
$baseName = if (-not [string]::IsNullOrWhiteSpace($ProductName)) { $ProductName } else { Get-ConfigDriverBaseName -RawName $StoredName }
|
||||
|
||||
switch ($Make) {
|
||||
'Dell' {
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $StoredName }
|
||||
if ([string]::IsNullOrWhiteSpace($SystemId)) { return $baseName }
|
||||
return "{0} ({1})" -f $baseName.Trim(), $SystemId.Trim()
|
||||
}
|
||||
'HP' {
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $StoredName }
|
||||
if ([string]::IsNullOrWhiteSpace($SystemId)) { return $baseName }
|
||||
return "{0} ({1})" -f $baseName.Trim(), $SystemId.Trim()
|
||||
}
|
||||
'Lenovo' {
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $StoredName }
|
||||
if ([string]::IsNullOrWhiteSpace($MachineType)) { return $baseName }
|
||||
return "{0} ({1})" -f $baseName.Trim(), $MachineType.Trim()
|
||||
}
|
||||
default {
|
||||
return $StoredName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-LoadConfiguration {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -363,6 +414,7 @@ function Update-UIFromConfig {
|
||||
Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State
|
||||
Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State
|
||||
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
|
||||
Set-UIValue -ControlName 'cmbBitsPriority' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'BitsPriority' -State $State
|
||||
Set-UIValue -ControlName 'txtMaxUSBDrives' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'MaxUSBDrives' -State $State
|
||||
Set-UIValue -ControlName 'chkBuildUSBDriveEnable' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BuildUSBDrive' -State $State
|
||||
Set-UIValue -ControlName 'chkCompactOS' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompactOS' -State $State
|
||||
@@ -618,8 +670,9 @@ function Update-UIFromConfig {
|
||||
}
|
||||
}
|
||||
|
||||
if ($propertyExists -and ($propertyValue -eq $item.SerialNumber)) {
|
||||
WriteLog "LoadConfig: Selecting USB Drive Model '$($item.Model)' with Serial '$($item.SerialNumber)'."
|
||||
# Match USB drives by UniqueId instead of SerialNumber
|
||||
if ($propertyExists -and ($propertyValue -eq $item.UniqueId)) {
|
||||
WriteLog "LoadConfig: Selecting USB Drive Model '$($item.Model)' with UniqueId '$($item.UniqueId)'."
|
||||
$item.IsSelected = $true
|
||||
}
|
||||
else {
|
||||
@@ -704,6 +757,7 @@ function Update-UIFromConfig {
|
||||
WriteLog "LoadConfig: Error applying Additional FFU selections: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Update-BitsPrioritySetting -State $State
|
||||
WriteLog "LoadConfig: Configuration loading process finished."
|
||||
}
|
||||
|
||||
@@ -1056,15 +1110,28 @@ function Import-ConfigSupplementalAssets {
|
||||
if ($null -eq $modelEntry -or -not ($modelEntry.PSObject.Properties['Name'])) { continue }
|
||||
$modelName = $modelEntry.Name
|
||||
if ([string]::IsNullOrWhiteSpace($modelName)) { continue }
|
||||
$downloadStatus = if ($modelEntry.PSObject.Properties['DownloadStatus']) { $modelEntry.DownloadStatus } else { "" }
|
||||
$linkValue = if ($modelEntry.PSObject.Properties['Link']) { $modelEntry.Link } else { $null }
|
||||
$productName = if ($modelEntry.PSObject.Properties['ProductName']) { $modelEntry.ProductName } else { $null }
|
||||
$machineType = if ($modelEntry.PSObject.Properties['MachineType']) { $modelEntry.MachineType } else { $null }
|
||||
$systemId = if ($modelEntry.PSObject.Properties['SystemId']) { $modelEntry.SystemId } else { $null }
|
||||
$idValue = if ($modelEntry.PSObject.Properties['Id']) { $modelEntry.Id } else { $null }
|
||||
if ($null -eq $idValue -and -not [string]::IsNullOrWhiteSpace($systemId)) { $idValue = $systemId }
|
||||
if ($null -eq $idValue -and -not [string]::IsNullOrWhiteSpace($machineType)) { $idValue = $machineType }
|
||||
$displayModel = Get-ConfigDriverDisplayName -Make $makeName -StoredName $modelName -ProductName $productName -SystemId $systemId -MachineType $machineType
|
||||
if ([string]::IsNullOrWhiteSpace($displayModel)) {
|
||||
$displayModel = $modelName
|
||||
}
|
||||
$driverObj = [PSCustomObject]@{
|
||||
IsSelected = $true
|
||||
Make = $makeName
|
||||
Model = $modelName
|
||||
DownloadStatus = if ($modelEntry.PSObject.Properties['DownloadStatus']) { $modelEntry.DownloadStatus } else { "" }
|
||||
Link = if ($modelEntry.PSObject.Properties['Link']) { $modelEntry.Link } else { $null }
|
||||
ProductName = if ($modelEntry.PSObject.Properties['ProductName']) { $modelEntry.ProductName } else { $null }
|
||||
MachineType = if ($modelEntry.PSObject.Properties['MachineType']) { $modelEntry.MachineType } else { $null }
|
||||
Id = if ($modelEntry.PSObject.Properties['Id']) { $modelEntry.Id } else { $null }
|
||||
Model = $displayModel
|
||||
DownloadStatus = $downloadStatus
|
||||
Link = $linkValue
|
||||
ProductName = $productName
|
||||
MachineType = $machineType
|
||||
SystemId = $systemId
|
||||
Id = $idValue
|
||||
}
|
||||
$State.Data.allDriverModels.Add($driverObj)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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"
|
||||
if (-not (Test-Path -Path $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()
|
||||
$download = $true
|
||||
if (Test-Path -Path $dellCatalogXML) {
|
||||
if (((Get-Date) - (Get-Item $dellCatalogXML).CreationTime).TotalDays -lt 7) {
|
||||
$download = $false
|
||||
}
|
||||
}
|
||||
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"
|
||||
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
|
||||
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
|
||||
Invoke-Process -FilePath Expand.exe -ArgumentList """$dellCabFile"" ""$dellCatalogXML""" | Out-Null
|
||||
Remove-Item $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."
|
||||
}
|
||||
if (-not (Test-Path $dellCatalogXML)) { throw "Dell server catalog XML missing: $dellCatalogXML" }
|
||||
|
||||
# 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
|
||||
$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' { $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
|
||||
}
|
||||
}
|
||||
|
||||
# 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,261 @@ 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
|
||||
$modelDisplay = $DriverItemData.Model
|
||||
$make = 'Dell'
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Checking...' }
|
||||
|
||||
# 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
|
||||
$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
|
||||
|
||||
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
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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..." }
|
||||
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."
|
||||
# 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"
|
||||
$wimRelativePath = Join-Path $make "$sanitizedModelName.wim"
|
||||
$srcPath = Join-Path $makeDriversPath $sanitizedModelName
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Compressing existing...' }
|
||||
try {
|
||||
$null = Compress-DriverFolderToWim -SourceFolderPath $srcPath -DestinationWimPath $wimPath -WimName $modelDisplay -WimDescription "Drivers for $modelDisplay" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
$existing.Status = 'Compression successful'
|
||||
$existing.DriverPath = $wimRelativePath
|
||||
$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)'
|
||||
$existing.Success = $false
|
||||
}
|
||||
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 $existing
|
||||
}
|
||||
|
||||
return $existingDriver
|
||||
}
|
||||
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 }
|
||||
|
||||
# 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"
|
||||
|
||||
# 3. Parse the *EXISTING* XML and Find Drivers for *this specific model*
|
||||
$status = "Finding drivers..."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||
|
||||
# Check if the provided XML path exists
|
||||
if (-not (Test-Path -Path $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 ($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")
|
||||
$packages = @()
|
||||
|
||||
if ($WindowsRelease -le 11) {
|
||||
# Client OS check
|
||||
if ($osArch -eq $WindowsArch) {
|
||||
$validOS = $osNode
|
||||
break
|
||||
$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 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
|
||||
}
|
||||
}
|
||||
# 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" }
|
||||
|
||||
if ($validOS) {
|
||||
$modelSpecificDriversFound = $true
|
||||
|
||||
# Extract driver information
|
||||
$driverPath = $component.SoftwareComponent.GetAttribute("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." }
|
||||
}
|
||||
|
||||
$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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $reader) {
|
||||
$reader.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog "Searching $($softwareComponents.Count) DRVR components in '$dellCatalogXML' for model '$modelName'..."
|
||||
|
||||
[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) {
|
||||
# Check if SupportedSystems and Brand exist
|
||||
if ($null -eq $component.SupportedSystems -or $null -eq $component.SupportedSystems.Brand) { continue }
|
||||
# Ensure Model is iterable
|
||||
$componentModels = @($component.SupportedSystems.Brand.Model)
|
||||
if ($null -eq $componentModels) { continue }
|
||||
|
||||
$modelMatch = $false
|
||||
foreach ($item in $componentModels) {
|
||||
# Check if Display and its CDATA section exist before accessing
|
||||
if ($null -ne $item.Display -and $null -ne $item.Display.'#cdata-section' -and $item.Display.'#cdata-section'.Trim() -eq $modelName) {
|
||||
$modelMatch = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($modelMatch) {
|
||||
# Model matches, now check OS compatibility
|
||||
$validOS = $null
|
||||
if ($null -ne $component.SupportedOperatingSystems) {
|
||||
# Ensure OperatingSystem is always an array/collection
|
||||
$osList = @($component.SupportedOperatingSystems.OperatingSystem)
|
||||
|
||||
if ($null -ne $osList) {
|
||||
if ($WindowsRelease -le 11) {
|
||||
# Client OS check
|
||||
$validOS = $osList | Where-Object { $_.osArch -eq $WindowsArch } | Select-Object -First 1
|
||||
}
|
||||
else {
|
||||
# Server OS check
|
||||
$osCodePattern = switch ($WindowsRelease) {
|
||||
2016 { "W14" } # Note: Dell uses W14 for Server 2016
|
||||
2019 { "W19" }
|
||||
2022 { "W22" }
|
||||
2025 { "W25" }
|
||||
default { "W22" } # Fallback, adjust as needed
|
||||
}
|
||||
$validOS = $osList | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match $osCodePattern) } | Select-Object -First 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($validOS) {
|
||||
$modelSpecificDriversFound = $true # Mark that we found at least one relevant driver component
|
||||
$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)
|
||||
# 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]@{
|
||||
$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 = $driverFileName
|
||||
DriverFileName = $fileName
|
||||
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
|
||||
foreach ($cat in $latestDrivers.Keys) { foreach ($drv in $latestDrivers[$cat].Values) { $packages += $drv } }
|
||||
}
|
||||
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
|
||||
|
||||
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 }
|
||||
}
|
||||
WriteLog "Downloading from: $($driver.DownloadUrl) to $driverFilePath"
|
||||
try {
|
||||
Start-BitsTransferWithRetry -Source $driver.DownloadUrl -Destination $driverFilePath
|
||||
WriteLog "Driver downloaded: $($driver.DriverFileName)"
|
||||
|
||||
$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 "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
|
||||
$failureMessage = "Failed to download driver '$driverName' from $($pkg.DownloadUrl): $($_.Exception.Message)"
|
||||
WriteLog $failureMessage
|
||||
throw (New-Object System.Exception($failureMessage, $_.Exception))
|
||||
}
|
||||
}
|
||||
|
||||
$status = "$idx/$total Extracting $driverName"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $status }
|
||||
|
||||
# Extract the driver
|
||||
$status = "Extracting $($driversProcessed)/$($totalDriversToProcess): $($driver.Name)"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||
if (-not (Test-Path $extractFolder)) { New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null }
|
||||
|
||||
# Ensure extraction folder exists before attempting extraction
|
||||
if (-not (Test-Path -Path $extractFolder)) {
|
||||
WriteLog "Creating extraction folder: $extractFolder"
|
||||
$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
|
||||
}
|
||||
|
||||
# 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"
|
||||
}
|
||||
}
|
||||
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 "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
|
||||
}
|
||||
WriteLog "Extraction error: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# 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"
|
||||
if ($ok) {
|
||||
Remove-Item $driverFilePath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
else {
|
||||
WriteLog "Extraction failed for $($driver.DriverFileName). Downloaded file kept at $driverFilePath for inspection."
|
||||
# Update status to indicate partial failure?
|
||||
$failureMessage = "Failed to extract driver '$driverName'."
|
||||
WriteLog $failureMessage
|
||||
throw (New-Object System.Exception($failureMessage))
|
||||
}
|
||||
}
|
||||
|
||||
} # 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'..."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status 'Compressing...' }
|
||||
$wimPath = Join-Path $makeDriversPath "$sanitizedModelName.wim"
|
||||
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)"
|
||||
}
|
||||
$null = 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 "Error during compression for '$modelName': $($_.Exception.Message)"
|
||||
$status = "Completed (Compression Error)"
|
||||
WriteLog "Compression failed for $($modelDisplay): $($_.Exception.Message)"
|
||||
$statusFinal = 'Completed (Compression Failed)'
|
||||
}
|
||||
}
|
||||
else {
|
||||
$status = "Completed" # Final status if not compressing
|
||||
$statusFinal = 'Completed'
|
||||
}
|
||||
# --- End Compression ---
|
||||
|
||||
$success = $true # Mark success as download/extract was okay
|
||||
|
||||
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 }
|
||||
$errorStatus = "Error: $($_.Exception.Message)"
|
||||
WriteLog "Save-DellDriversTask error for $($modelDisplay): $($_.Exception.ToString())"
|
||||
Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelPath -Description $modelDisplay
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelDisplay -Status $errorStatus }
|
||||
return [pscustomobject]@{ Model = $modelDisplay; Status = $errorStatus; 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 *
|
||||
@@ -66,30 +66,45 @@ function Get-HPDriversModelList {
|
||||
$settings.Async = $false # Ensure synchronous reading
|
||||
|
||||
$reader = [System.Xml.XmlReader]::Create($platformListXml, $settings)
|
||||
$uniqueModels = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$uniqueEntries = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
|
||||
while ($reader.Read()) {
|
||||
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'Platform') {
|
||||
# Read the inner content of the Platform node
|
||||
$platformReader = $reader.ReadSubtree()
|
||||
$platformNames = [System.Collections.Generic.List[string]]::new()
|
||||
$platformSystemId = $null
|
||||
|
||||
while ($platformReader.Read()) {
|
||||
if ($platformReader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $platformReader.Name -eq 'ProductName') {
|
||||
if ($platformReader.NodeType -eq [System.Xml.XmlNodeType]::Element) {
|
||||
if ($platformReader.Name -eq 'ProductName') {
|
||||
$modelName = $platformReader.ReadElementContentAsString()
|
||||
if (-not [string]::IsNullOrWhiteSpace($modelName) -and $uniqueModels.Add($modelName)) {
|
||||
# Add to list only if it's a new unique model
|
||||
$modelList.Add([PSCustomObject]@{
|
||||
Make = $Make
|
||||
Model = $modelName
|
||||
})
|
||||
if (-not [string]::IsNullOrWhiteSpace($modelName)) {
|
||||
$platformNames.Add($modelName.Trim())
|
||||
}
|
||||
}
|
||||
elseif ($platformReader.Name -eq 'SystemID') {
|
||||
$platformSystemId = $platformReader.ReadElementContentAsString().Trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
$platformReader.Close()
|
||||
|
||||
foreach ($name in $platformNames) {
|
||||
$systemIdKey = if (-not [string]::IsNullOrWhiteSpace($platformSystemId)) { $platformSystemId } else { '' }
|
||||
$compositeKey = "$name|$systemIdKey"
|
||||
if ($uniqueEntries.Add($compositeKey)) {
|
||||
$modelList.Add([PSCustomObject]@{
|
||||
Make = $Make
|
||||
Model = $name
|
||||
SystemId = $platformSystemId
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$reader.Close()
|
||||
|
||||
WriteLog "Successfully parsed $($modelList.Count) unique HP models from PlatformList.xml."
|
||||
WriteLog "Successfully parsed $($modelList.Count) HP model and SystemID combinations from PlatformList.xml."
|
||||
|
||||
}
|
||||
catch {
|
||||
@@ -97,7 +112,7 @@ function Get-HPDriversModelList {
|
||||
}
|
||||
|
||||
# Sort the list alphabetically by Model name before returning
|
||||
return $modelList | Sort-Object -Property Model
|
||||
return $modelList | Sort-Object -Property Model, SystemId
|
||||
}
|
||||
# Function to download and extract drivers for a specific HP model (Designed for ForEach-Object -Parallel)
|
||||
function Save-HPDriversTask {
|
||||
@@ -123,11 +138,17 @@ function Save-HPDriversTask {
|
||||
[bool]$PreserveSourceOnCompress = $false
|
||||
)
|
||||
|
||||
$modelName = $DriverItemData.Model
|
||||
$displayModelName = if (-not [string]::IsNullOrWhiteSpace($DriverItemData.Model)) { $DriverItemData.Model } else { $DriverItemData.Id }
|
||||
$make = $DriverItemData.Make # Should be 'HP'
|
||||
$identifier = $modelName # Unique identifier for progress updates
|
||||
$sanitizedModelName = ConvertTo-SafeName -Name $modelName
|
||||
if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" }
|
||||
$productName = if ($DriverItemData.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($DriverItemData.ProductName)) { $DriverItemData.ProductName } else { ConvertTo-DriverBaseName -ModelString $displayModelName }
|
||||
if ([string]::IsNullOrWhiteSpace($productName)) { $productName = $displayModelName }
|
||||
$systemIdentifier = if ($DriverItemData.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($DriverItemData.SystemId)) { $DriverItemData.SystemId } else { $null }
|
||||
if ([string]::IsNullOrWhiteSpace($displayModelName)) {
|
||||
$displayModelName = if ([string]::IsNullOrWhiteSpace($systemIdentifier)) { $productName } else { Get-DriverDisplayName -BaseName $productName -Identifier $systemIdentifier }
|
||||
}
|
||||
$identifier = $displayModelName # Unique identifier for progress updates
|
||||
$sanitizedModelName = ConvertTo-SafeName -Name $identifier
|
||||
if ($sanitizedModelName -ne $identifier) { WriteLog "Sanitized model name: '$identifier' -> '$sanitizedModelName'" }
|
||||
$hpDriversBaseFolder = Join-Path -Path $DriversFolder -ChildPath $make # Changed variable name for clarity
|
||||
$platformListXml = Join-Path -Path $hpDriversBaseFolder -ChildPath "PlatformList.xml"
|
||||
$modelSpecificFolder = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName # Sanitize model name for folder path
|
||||
@@ -135,7 +156,16 @@ function Save-HPDriversTask {
|
||||
$finalStatus = "" # Initialize final status
|
||||
$successState = $true # Assume success unless an operation fails
|
||||
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking HP drivers for $modelName..." }
|
||||
if (-not (Test-Path -Path $DriversFolder -PathType Container)) {
|
||||
WriteLog "Creating Drivers folder: $DriversFolder"
|
||||
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
if (-not (Test-Path -Path $hpDriversBaseFolder -PathType Container)) {
|
||||
WriteLog "Creating HP drivers folder: $hpDriversBaseFolder"
|
||||
New-Item -Path $hpDriversBaseFolder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking HP drivers for $displayModelName..." }
|
||||
|
||||
try {
|
||||
# Check for existing drivers
|
||||
@@ -149,13 +179,14 @@ function Save-HPDriversTask {
|
||||
# Special handling for existing folders that need compression
|
||||
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
|
||||
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($sanitizedModelName).wim"
|
||||
$wimRelativePath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim"
|
||||
$sourceFolderPath = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName
|
||||
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
|
||||
try {
|
||||
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
$existingDriver.Status = "Already downloaded & Compressed"
|
||||
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim"
|
||||
$null = Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
$existingDriver.Status = "Compression successful"
|
||||
$existingDriver.DriverPath = $wimRelativePath
|
||||
$existingDriver.Success = $true
|
||||
WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath."
|
||||
}
|
||||
@@ -190,13 +221,23 @@ function Save-HPDriversTask {
|
||||
}
|
||||
}
|
||||
|
||||
# Parse PlatformList.xml to find SystemID and OSReleaseID for the specific model
|
||||
WriteLog "Parsing $platformListXml for model '$modelName' details..."
|
||||
# Parse the PlatformList.xml to find the SystemID based on the ProductName
|
||||
WriteLog "Parsing $platformListXml for model '$displayModelName' (SystemID: $systemIdentifier) details..."
|
||||
[xml]$platformListContent = Get-Content -Path $platformListXml -Raw -Encoding UTF8 -ErrorAction Stop
|
||||
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.ProductName.'#text' -match "^$([regex]::Escape($modelName))$" } | Select-Object -First 1
|
||||
$platformNode = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($systemIdentifier)) {
|
||||
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.SystemID -eq $systemIdentifier } | Select-Object -First 1
|
||||
if ($null -eq $platformNode) {
|
||||
WriteLog "SystemID '$systemIdentifier' not found in PlatformList.xml. Falling back to ProductName search."
|
||||
}
|
||||
}
|
||||
if ($null -eq $platformNode) {
|
||||
$searchName = if (-not [string]::IsNullOrWhiteSpace($productName)) { $productName } else { $displayModelName }
|
||||
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.ProductName.'#text' -match "^$([regex]::Escape($searchName))$" } | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ($null -eq $platformNode) {
|
||||
throw "Model '$modelName' not found in PlatformList.xml."
|
||||
throw "Model '$displayModelName' (SystemID: $systemIdentifier) not found in PlatformList.xml."
|
||||
}
|
||||
|
||||
$systemID = $platformNode.SystemID
|
||||
@@ -289,11 +330,11 @@ function Save-HPDriversTask {
|
||||
}
|
||||
$availableVersionsString = ($allAvailableVersions | Select-Object -Unique) -join ', '
|
||||
if ([string]::IsNullOrWhiteSpace($availableVersionsString)) { $availableVersionsString = "None" }
|
||||
throw "Could not find any suitable OS driver pack for model '$modelName' matching requested or fallback versions (Win$($WindowsRelease) $WindowsVersion). Available: $availableVersionsString"
|
||||
throw "Could not find any suitable OS driver pack for model '$displayModelName' matching requested or fallback versions (Win$($WindowsRelease) $WindowsVersion). Available: $availableVersionsString"
|
||||
}
|
||||
|
||||
$osReleaseIdFileName = $selectedOSNode.OSReleaseIdFileName -replace 'H', 'h'
|
||||
WriteLog "Using SystemID: $systemID and OS Info: Win$($selectedOSRelease) ($($selectedOSVersion)) for '$modelName'"
|
||||
WriteLog "Using SystemID: $systemID and OS Info: Win$($selectedOSRelease) ($($selectedOSVersion)) for '$displayModelName'"
|
||||
$archSuffix = $WindowsArch -replace "^x", ""
|
||||
$modelRelease = "$($systemID)_$($archSuffix)_$($selectedOSRelease).0.$($selectedOSVersion.ToLower())"
|
||||
$driverCabUrl = "https://hpia.hpcloud.hp.com/ref/$systemID/$modelRelease.cab"
|
||||
@@ -312,7 +353,7 @@ function Save-HPDriversTask {
|
||||
$updates = $driverXmlContent.ImagePal.Solutions.UpdateInfo | Where-Object { $_.Category -match '^Driver' }
|
||||
$totalDrivers = ($updates | Measure-Object).Count
|
||||
$downloadedCount = 0
|
||||
WriteLog "Found $totalDrivers driver updates for $modelName."
|
||||
WriteLog "Found $totalDrivers driver updates for $displayModelName."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Found $totalDrivers drivers. Downloading..." }
|
||||
|
||||
if (-not (Test-Path -Path $modelSpecificFolder)) {
|
||||
@@ -330,7 +371,7 @@ function Save-HPDriversTask {
|
||||
$extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverName + "_" + $version + "_" + ($driverFileName -replace '\.exe$', ''))
|
||||
|
||||
$downloadedCount++
|
||||
$progressMsg = "($downloadedCount/$totalDrivers) Downloading $driverName..."
|
||||
$progressMsg = "$downloadedCount/$totalDrivers Downloading $driverName"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $progressMsg }
|
||||
WriteLog "$progressMsg URL: $driverUrl"
|
||||
|
||||
@@ -344,6 +385,8 @@ function Save-HPDriversTask {
|
||||
WriteLog "Downloading driver to: $driverFilePath"
|
||||
Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath -ErrorAction Stop
|
||||
WriteLog "Driver downloaded: $driverFilePath"
|
||||
$progressMsg = "$downloadedCount/$totalDrivers Extracting $driverName"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $progressMsg }
|
||||
WriteLog "Creating extraction folder: $extractFolder"
|
||||
New-Item -Path $extractFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
$arguments = "/s /e /f `"$extractFolder`""
|
||||
@@ -357,7 +400,7 @@ function Save-HPDriversTask {
|
||||
}
|
||||
|
||||
Remove-Item -Path $driverCabFile, $driverXmlFile -Force -ErrorAction SilentlyContinue
|
||||
WriteLog "Cleaned up driver cab and xml files for $modelName"
|
||||
WriteLog "Cleaned up driver cab and xml files for $displayModelName"
|
||||
|
||||
$finalStatus = "Completed"
|
||||
if ($CompressToWim) {
|
||||
@@ -365,7 +408,7 @@ function Save-HPDriversTask {
|
||||
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim"
|
||||
WriteLog "Compressing '$modelSpecificFolder' to '$wimFilePath'..."
|
||||
try {
|
||||
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
$null = Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
WriteLog "Compression successful for '$identifier'."
|
||||
$finalStatus = "Completed & Compressed"
|
||||
$driverRelativePath = Join-Path -Path $make -ChildPath "$($identifier).wim" # Update relative path to the WIM
|
||||
@@ -378,15 +421,12 @@ function Save-HPDriversTask {
|
||||
$successState = $true
|
||||
}
|
||||
catch {
|
||||
$errorMessage = "Error saving HP drivers for $($modelName): $($_.Exception.Message)"
|
||||
$errorMessage = "Error saving HP drivers for $($displayModelName): $($_.Exception.Message)"
|
||||
WriteLog $errorMessage
|
||||
$finalStatus = "Error: $($_.Exception.Message.Split([Environment]::NewLine)[0])"
|
||||
$successState = $false
|
||||
$driverRelativePath = $null # Ensure path is null on error
|
||||
if (Test-Path -Path $modelSpecificFolder -PathType Container) {
|
||||
WriteLog "Attempting to remove partially created folder $modelSpecificFolder due to error."
|
||||
Remove-Item -Path $modelSpecificFolder -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelSpecificFolder -Description $identifier
|
||||
}
|
||||
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $finalStatus }
|
||||
|
||||
@@ -132,13 +132,14 @@ function Save-LenovoDriversTask {
|
||||
# Special handling for existing folders that need compression
|
||||
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
|
||||
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($sanitizedIdentifier).wim"
|
||||
$wimRelativePath = Join-Path -Path $make -ChildPath "$($sanitizedIdentifier).wim"
|
||||
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedIdentifier
|
||||
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
|
||||
try {
|
||||
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
$existingDriver.Status = "Already downloaded & Compressed"
|
||||
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedIdentifier).wim"
|
||||
$null = Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
$existingDriver.Status = "Compression successful"
|
||||
$existingDriver.DriverPath = $wimRelativePath
|
||||
$existingDriver.Success = $true
|
||||
WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath."
|
||||
}
|
||||
@@ -207,17 +208,16 @@ function Save-LenovoDriversTask {
|
||||
$packageXMLPath = Join-Path -Path $tempDownloadPath -ChildPath $packageName
|
||||
$baseURL = $packageUrl -replace [regex]::Escape($packageName), "" # Base URL for the driver file
|
||||
|
||||
$status = "($processedPackages/$totalPackages) Getting package info..."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
||||
|
||||
# Download the package XML
|
||||
WriteLog "($processedPackages/$totalPackages) Downloading package XML: $packageUrl"
|
||||
try {
|
||||
Start-BitsTransferWithRetry -Source $packageUrl -Destination $packageXMLPath
|
||||
}
|
||||
catch {
|
||||
WriteLog "($processedPackages/$totalPackages) Failed to download package XML '$packageUrl'. Skipping. Error: $($_.Exception.Message)"
|
||||
continue # Skip this package
|
||||
$failureMessage = "Failed to download Lenovo package XML '$packageUrl': $($_.Exception.Message)"
|
||||
WriteLog "($processedPackages/$totalPackages) $failureMessage"
|
||||
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
|
||||
throw (New-Object System.Exception($failureMessage, $_.Exception))
|
||||
}
|
||||
|
||||
# Load and parse the package XML
|
||||
@@ -278,7 +278,7 @@ function Save-LenovoDriversTask {
|
||||
}
|
||||
|
||||
# Download the driver .exe
|
||||
$status = "($processedPackages/$totalPackages) Downloading $packageTitle..."
|
||||
$status = "$processedPackages/$totalPackages Downloading $packageTitle"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
||||
WriteLog "($processedPackages/$totalPackages) Downloading driver: $driverUrl to $driverFilePath"
|
||||
try {
|
||||
@@ -286,13 +286,14 @@ function Save-LenovoDriversTask {
|
||||
WriteLog "($processedPackages/$totalPackages) Driver downloaded: $driverFileName"
|
||||
}
|
||||
catch {
|
||||
WriteLog "($processedPackages/$totalPackages) Failed to download driver '$driverUrl'. Skipping. Error: $($_.Exception.Message)"
|
||||
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
|
||||
continue # Skip this driver
|
||||
$failureMessage = "Failed to download driver '$packageTitle' from $($driverUrl): $($_.Exception.Message)"
|
||||
WriteLog "($processedPackages/$totalPackages) $failureMessage"
|
||||
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
|
||||
throw (New-Object System.Exception($failureMessage, $_.Exception))
|
||||
}
|
||||
|
||||
# --- Extraction Logic ---
|
||||
$status = "($processedPackages/$totalPackages) Extracting $packageTitle..."
|
||||
$status = "$processedPackages/$totalPackages Extracting $packageTitle"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
||||
|
||||
# Always use a temporary extraction path to avoid long path issues
|
||||
@@ -317,7 +318,7 @@ function Save-LenovoDriversTask {
|
||||
|
||||
# Modify the extract command to point to the temporary folder
|
||||
$modifiedExtractCommand = $extractCommand -replace '%PACKAGEPATH%', "`"$extractFolder`""
|
||||
WriteLog "($processedPackages/$totalPackages) Extracting driver: $driverFilePath using command: $modifiedExtractCommand"
|
||||
WriteLog "$processedPackages/$totalPackages Extracting driver: $driverFilePath using command: $modifiedExtractCommand"
|
||||
|
||||
try {
|
||||
Invoke-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand -Wait $true | Out-Null
|
||||
@@ -325,14 +326,13 @@ function Save-LenovoDriversTask {
|
||||
$extractionSucceeded = $true
|
||||
}
|
||||
catch {
|
||||
WriteLog "($processedPackages/$totalPackages) Failed to extract driver '$driverFilePath' to temporary path. Skipping. Error: $($_.Exception.Message)"
|
||||
# Don't delete the downloaded exe yet if extraction fails
|
||||
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
|
||||
# Clean up temp folder if extraction failed
|
||||
$failureMessage = "Failed to extract driver package '$packageTitle': $($_.Exception.Message)"
|
||||
WriteLog "($processedPackages/$totalPackages) $failureMessage"
|
||||
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
|
||||
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
|
||||
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
continue # Skip further processing for this driver
|
||||
throw (New-Object System.Exception($failureMessage, $_.Exception))
|
||||
}
|
||||
|
||||
# --- Post-Extraction Handling (Move from Temp to Final Destination) ---
|
||||
@@ -375,10 +375,9 @@ function Save-LenovoDriversTask {
|
||||
Move-Item -Path $item.FullName -Destination $finalDestinationPath -Force -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
WriteLog "($processedPackages/$totalPackages) Failed to move item '$($item.FullName)' to '$finalDestinationPath'. Error: $($_.Exception.Message)"
|
||||
# Decide if this should stop the whole process or just skip this item
|
||||
# For now, we'll log and continue, but mark overall success as false
|
||||
$extractionSucceeded = $false
|
||||
$failureMessage = "Failed to move extracted item '$($item.FullName)' to '$finalDestinationPath': $($_.Exception.Message)"
|
||||
WriteLog "($processedPackages/$totalPackages) $failureMessage"
|
||||
throw (New-Object System.Exception($failureMessage, $_.Exception))
|
||||
}
|
||||
} # End foreach ($item in $extractedItems)
|
||||
|
||||
@@ -415,6 +414,9 @@ function Save-LenovoDriversTask {
|
||||
# Always delete the package XML
|
||||
WriteLog "($processedPackages/$totalPackages) Deleting package XML file: $packageXMLPath"
|
||||
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
|
||||
if (-not $extractionSucceeded) {
|
||||
throw (New-Object System.Exception("Failed to extract driver '$packageTitle'. See log for details."))
|
||||
}
|
||||
|
||||
} # End foreach package
|
||||
|
||||
@@ -451,12 +453,11 @@ function Save-LenovoDriversTask {
|
||||
|
||||
}
|
||||
catch {
|
||||
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
|
||||
WriteLog "Error saving Lenovo drivers for '$identifier': $($_.Exception.ToString())" # Log full exception string
|
||||
$status = "Error: $($_.Exception.Message)"
|
||||
WriteLog "Error saving Lenovo drivers for '$identifier': $($_.Exception.ToString())"
|
||||
$success = $false
|
||||
# Enqueue the error status before returning
|
||||
Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelPath -Description $identifier
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
||||
# Ensure return object is created even on error
|
||||
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success; DriverPath = $null }
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -106,6 +106,10 @@ function Save-MicrosoftDriversTask {
|
||||
$driverRelativePath = Join-Path -Path $make -ChildPath $modelName # Relative path for the driver folder
|
||||
$status = "Getting download link..." # Initial local status
|
||||
$success = $false
|
||||
$sanitizedModelName = ConvertTo-SafeName -Name $modelName
|
||||
if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" }
|
||||
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make
|
||||
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
|
||||
|
||||
# Initial status update
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
|
||||
@@ -123,13 +127,14 @@ function Save-MicrosoftDriversTask {
|
||||
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
|
||||
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $make
|
||||
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($modelName).wim"
|
||||
$wimRelativePath = Join-Path -Path $make -ChildPath "$($modelName).wim"
|
||||
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
|
||||
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -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 "$($modelName).wim"
|
||||
$null = Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||
$existingDriver.Status = "Compression successful"
|
||||
$existingDriver.DriverPath = $wimRelativePath
|
||||
$existingDriver.Success = $true
|
||||
WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath."
|
||||
}
|
||||
@@ -226,7 +231,7 @@ function Save-MicrosoftDriversTask {
|
||||
### DOWNLOAD AND EXTRACT
|
||||
if ($downloadLink) {
|
||||
WriteLog "Selected Download Link for $modelName (Actual: Windows $downloadedVersion): $downloadLink"
|
||||
$status = "Downloading (Win$downloadedVersion)..." # Update status message
|
||||
$status = "Downloading Win$downloadedVersion $fileName"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||
|
||||
# Create directories
|
||||
@@ -234,10 +239,6 @@ function Save-MicrosoftDriversTask {
|
||||
WriteLog "Creating Drivers folder: $DriversFolder"
|
||||
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$sanitizedModelName = ConvertTo-SafeName -Name $modelName
|
||||
if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" }
|
||||
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
||||
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
|
||||
if (-Not (Test-Path -Path $modelPath)) {
|
||||
WriteLog "Creating model folder: $modelPath"
|
||||
New-Item -Path $modelPath -ItemType Directory -Force | Out-Null
|
||||
@@ -257,7 +258,7 @@ function Save-MicrosoftDriversTask {
|
||||
|
||||
### EXTRACT
|
||||
if ($fileExtension -eq ".msi") {
|
||||
$status = "Waiting for MSI lock..." # Set initial status
|
||||
$status = "Waiting for MSI lock..."
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||
|
||||
# Use a named mutex to ensure only one MSI extraction happens at a time across all parallel tasks
|
||||
@@ -286,14 +287,14 @@ function Save-MicrosoftDriversTask {
|
||||
catch [System.Threading.WaitHandleCannotBeOpenedException] {
|
||||
# Mutex is clear, proceed to extraction attempt
|
||||
WriteLog "System MSI mutex clear. Proceeding with MSI extraction attempt for $modelName."
|
||||
$status = "Extracting MSI..."
|
||||
$status = "Extracting Win$downloadedVersion $fileName"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||
$mutexClear = $true
|
||||
}
|
||||
catch {
|
||||
# Handle other potential errors when checking the mutex
|
||||
WriteLog "Warning: Error checking system MSI mutex for $($modelName): $_. Proceeding with caution."
|
||||
$status = "Extracting MSI (Mutex Error)..."
|
||||
$status = "Extracting Win$downloadedVersion $fileName (Mutex Error)"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||
$mutexClear = $true # Proceed despite mutex error
|
||||
}
|
||||
@@ -351,7 +352,7 @@ function Save-MicrosoftDriversTask {
|
||||
}
|
||||
}
|
||||
elseif ($fileExtension -eq ".zip") {
|
||||
$status = "Extracting ZIP..." # Set status before extraction
|
||||
$status = "Extracting Win$downloadedVersion $fileName"
|
||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
||||
WriteLog "Extracting ZIP file to $modelPath"
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
@@ -421,6 +422,7 @@ function Save-MicrosoftDriversTask {
|
||||
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
|
||||
WriteLog "Error saving Microsoft drivers for $($modelName): $($_.Exception.Message)"
|
||||
$success = $false
|
||||
Remove-DriverModelFolder -DriversFolder $DriversFolder -TargetFolder $modelPath -Description $modelName
|
||||
# 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
|
||||
|
||||
@@ -5,6 +5,140 @@
|
||||
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.
|
||||
#>
|
||||
|
||||
function ConvertTo-DriverBaseName {
|
||||
param(
|
||||
[string]$ModelString
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ModelString)) {
|
||||
return $ModelString
|
||||
}
|
||||
|
||||
if ($ModelString -match '^(.*?)\s*\((.+)\)\s*$') {
|
||||
return $matches[1].Trim()
|
||||
}
|
||||
|
||||
return $ModelString.Trim()
|
||||
}
|
||||
|
||||
function Get-DriverDisplayName {
|
||||
param(
|
||||
[string]$BaseName,
|
||||
[string]$Identifier
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($BaseName)) {
|
||||
return $Identifier
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Identifier)) {
|
||||
return $BaseName.Trim()
|
||||
}
|
||||
|
||||
return "$($BaseName.Trim()) ($($Identifier.Trim()))"
|
||||
}
|
||||
|
||||
function Convert-DriverItemToJsonModel {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$DriverItem
|
||||
)
|
||||
|
||||
$makeName = $DriverItem.Make
|
||||
switch ($makeName) {
|
||||
'Microsoft' {
|
||||
$modelObject = @{ Name = $DriverItem.Model }
|
||||
if ($DriverItem.PSObject.Properties['Link'] -and -not [string]::IsNullOrWhiteSpace($DriverItem.Link)) {
|
||||
$modelObject.Link = $DriverItem.Link
|
||||
}
|
||||
return $modelObject
|
||||
}
|
||||
'Dell' {
|
||||
$systemId = if ($DriverItem.PSObject.Properties['SystemId']) { $DriverItem.SystemId } else { $null }
|
||||
$baseName = ConvertTo-DriverBaseName -ModelString $DriverItem.Model
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) {
|
||||
$baseName = $DriverItem.Model
|
||||
}
|
||||
$modelObject = @{ Name = $baseName }
|
||||
if (-not [string]::IsNullOrWhiteSpace($systemId)) {
|
||||
$modelObject.SystemId = $systemId
|
||||
}
|
||||
if ($DriverItem.PSObject.Properties['CabUrl'] -and -not [string]::IsNullOrWhiteSpace($DriverItem.CabUrl)) {
|
||||
$modelObject.CabUrl = $DriverItem.CabUrl
|
||||
}
|
||||
return $modelObject
|
||||
}
|
||||
'HP' {
|
||||
$baseName = if ($DriverItem.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($DriverItem.ProductName)) { $DriverItem.ProductName } else { ConvertTo-DriverBaseName -ModelString $DriverItem.Model }
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) {
|
||||
$baseName = $DriverItem.Model
|
||||
}
|
||||
$systemId = if ($DriverItem.PSObject.Properties['SystemId']) { $DriverItem.SystemId } else { $null }
|
||||
$modelObject = @{ Name = $baseName.Trim() }
|
||||
if (-not [string]::IsNullOrWhiteSpace($systemId)) {
|
||||
$modelObject.SystemId = $systemId
|
||||
}
|
||||
return $modelObject
|
||||
}
|
||||
'Lenovo' {
|
||||
$machineType = $DriverItem.MachineType
|
||||
$baseName = if ($DriverItem.ProductName) { $DriverItem.ProductName } else { ConvertTo-DriverBaseName -ModelString $DriverItem.Model }
|
||||
if ([string]::IsNullOrWhiteSpace($baseName) -or [string]::IsNullOrWhiteSpace($machineType)) {
|
||||
WriteLog "Skipping Lenovo driver '$($DriverItem.Model)' because Name or MachineType is missing."
|
||||
return $null
|
||||
}
|
||||
return @{
|
||||
Name = $baseName
|
||||
MachineType = $machineType
|
||||
}
|
||||
}
|
||||
default {
|
||||
WriteLog "Convert-DriverItemToJsonModel: Unsupported Make '$makeName'."
|
||||
return $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-DriverModelFolder {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$DriversFolder,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TargetFolder,
|
||||
[string]$Description
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DriversFolder) -or [string]::IsNullOrWhiteSpace($TargetFolder)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -Path $TargetFolder -PathType Container)) {
|
||||
return
|
||||
}
|
||||
|
||||
$driversRoot = [System.IO.Path]::GetFullPath((Resolve-Path -Path $DriversFolder -ErrorAction Stop).ProviderPath)
|
||||
$targetPath = [System.IO.Path]::GetFullPath((Resolve-Path -Path $TargetFolder -ErrorAction Stop).ProviderPath)
|
||||
|
||||
if ($targetPath -eq $driversRoot) {
|
||||
WriteLog "Remove-DriverModelFolder skipped deleting Drivers root: $targetPath"
|
||||
return
|
||||
}
|
||||
|
||||
if (-not ($targetPath.StartsWith($driversRoot, [System.StringComparison]::OrdinalIgnoreCase))) {
|
||||
WriteLog "Remove-DriverModelFolder skipped path outside Drivers root: $targetPath"
|
||||
return
|
||||
}
|
||||
|
||||
$contextMessage = if ([string]::IsNullOrWhiteSpace($Description)) { $targetPath } else { "$Description ($targetPath)" }
|
||||
WriteLog "Removing driver folder $contextMessage due to failure."
|
||||
Remove-Item -Path $targetPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
WriteLog "Remove-DriverModelFolder failed for $($TargetFolder): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Helper function to get models for a selected Make and standardize them
|
||||
function Get-ModelsForMake {
|
||||
param(
|
||||
@@ -84,11 +218,12 @@ 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
|
||||
$systemId = $null
|
||||
|
||||
if ($RawDriverObject.PSObject.Properties['Link']) {
|
||||
$link = $RawDriverObject.Link
|
||||
@@ -102,7 +237,30 @@ function ConvertTo-StandardizedDriverModel {
|
||||
$id = $RawDriverObject.MachineType
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
# HP specific handling
|
||||
if ($Make -eq 'HP') {
|
||||
$productName = if ($RawDriverObject.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($RawDriverObject.ProductName)) { $RawDriverObject.ProductName } else { ConvertTo-DriverBaseName -ModelString $RawDriverObject.Model }
|
||||
if ([string]::IsNullOrWhiteSpace($productName)) { $productName = $RawDriverObject.Model }
|
||||
if ($RawDriverObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($RawDriverObject.SystemId)) {
|
||||
$systemId = $RawDriverObject.SystemId
|
||||
}
|
||||
$modelDisplay = if ([string]::IsNullOrWhiteSpace($systemId)) { $productName } else { Get-DriverDisplayName -BaseName $productName -Identifier $systemId }
|
||||
$id = if ([string]::IsNullOrWhiteSpace($systemId)) { $productName } else { $systemId }
|
||||
}
|
||||
|
||||
# Dell-specific passthrough (needed for per-model cab workflow)
|
||||
$dellBrand = $null
|
||||
$dellModelNumber = $null
|
||||
$dellSystemId = $null
|
||||
$dellCabUrl = $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 }
|
||||
}
|
||||
|
||||
$output = [PSCustomObject]@{
|
||||
IsSelected = $false
|
||||
Make = $Make
|
||||
Model = $modelDisplay
|
||||
@@ -110,12 +268,25 @@ function ConvertTo-StandardizedDriverModel {
|
||||
Id = $id
|
||||
ProductName = $productName
|
||||
MachineType = $machineType
|
||||
Version = "" # Placeholder
|
||||
Type = "" # Placeholder
|
||||
Size = "" # Placeholder
|
||||
Arch = "" # Placeholder
|
||||
DownloadStatus = "" # Initial download status
|
||||
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
|
||||
}
|
||||
elseif ($Make -eq 'HP' -and -not [string]::IsNullOrWhiteSpace($systemId)) {
|
||||
$output | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemId
|
||||
}
|
||||
|
||||
return $output
|
||||
}
|
||||
|
||||
# Function to filter the driver model list based on text input
|
||||
@@ -188,35 +359,7 @@ function Save-DriversJson {
|
||||
$modelsForThisMake = @() # Initialize an array to hold model objects
|
||||
|
||||
foreach ($driverItem in $_.Group) {
|
||||
$modelObject = $null
|
||||
switch ($makeName) {
|
||||
'Microsoft' {
|
||||
$modelObject = @{
|
||||
Name = $driverItem.Model # Model is the display name
|
||||
Link = $driverItem.Link
|
||||
}
|
||||
}
|
||||
'Dell' {
|
||||
$modelObject = @{
|
||||
Name = $driverItem.Model
|
||||
}
|
||||
}
|
||||
'HP' {
|
||||
$modelObject = @{
|
||||
Name = $driverItem.Model
|
||||
}
|
||||
}
|
||||
'Lenovo' {
|
||||
$modelObject = @{
|
||||
Name = $driverItem.Model # This is "ProductName (MachineType)"
|
||||
ProductName = $driverItem.ProductName # This is "ProductName"
|
||||
MachineType = $driverItem.MachineType # This is "MachineType"
|
||||
}
|
||||
}
|
||||
default {
|
||||
WriteLog "Save-DriversJson: Unknown Make '$makeName' encountered for model '$($driverItem.Model)'. Skipping."
|
||||
}
|
||||
}
|
||||
$modelObject = Convert-DriverItemToJsonModel -DriverItem $driverItem
|
||||
if ($null -ne $modelObject) {
|
||||
$modelsForThisMake += $modelObject
|
||||
}
|
||||
@@ -307,11 +450,97 @@ function Import-DriversJson {
|
||||
continue
|
||||
}
|
||||
|
||||
$normalizedName = $importedModelNameFromObject
|
||||
$skipModel = $false
|
||||
switch ($makeName) {
|
||||
'Lenovo' {
|
||||
$productName = if ($importedModelObject.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.ProductName)) { $importedModelObject.ProductName } else { ConvertTo-DriverBaseName -ModelString $normalizedName }
|
||||
$machineType = if ($importedModelObject.PSObject.Properties['MachineType'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.MachineType)) { $importedModelObject.MachineType } else { $null }
|
||||
if ([string]::IsNullOrWhiteSpace($machineType) -and $normalizedName -match '(.+?)\s*\((.+?)\)$') {
|
||||
if ([string]::IsNullOrWhiteSpace($productName)) { $productName = $matches[1].Trim() }
|
||||
$machineType = $matches[2].Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($productName) -or [string]::IsNullOrWhiteSpace($machineType)) {
|
||||
WriteLog "Import-DriversJson: Skipping Lenovo model '$normalizedName' due to missing ProductName or MachineType."
|
||||
$skipModel = $true
|
||||
}
|
||||
else {
|
||||
$normalizedName = Get-DriverDisplayName -BaseName $productName -Identifier $machineType
|
||||
if ($importedModelObject.PSObject.Properties['ProductName']) {
|
||||
$importedModelObject.ProductName = $productName
|
||||
}
|
||||
else {
|
||||
$importedModelObject | Add-Member -NotePropertyName ProductName -NotePropertyValue $productName
|
||||
}
|
||||
if ($importedModelObject.PSObject.Properties['MachineType']) {
|
||||
$importedModelObject.MachineType = $machineType
|
||||
}
|
||||
else {
|
||||
$importedModelObject | Add-Member -NotePropertyName MachineType -NotePropertyValue $machineType
|
||||
}
|
||||
}
|
||||
}
|
||||
'Dell' {
|
||||
$baseName = ConvertTo-DriverBaseName -ModelString $normalizedName
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $normalizedName }
|
||||
$systemId = if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) { $importedModelObject.SystemId } else { $null }
|
||||
if ([string]::IsNullOrWhiteSpace($systemId) -and $normalizedName -match '(.+?)\s*\((.+?)\)$') {
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $matches[1].Trim() }
|
||||
$systemId = $matches[2].Trim()
|
||||
}
|
||||
$normalizedName = if ([string]::IsNullOrWhiteSpace($systemId)) { $baseName.Trim() } else { Get-DriverDisplayName -BaseName $baseName -Identifier $systemId }
|
||||
if ($importedModelObject.PSObject.Properties['SystemId']) {
|
||||
$importedModelObject.SystemId = $systemId
|
||||
}
|
||||
else {
|
||||
$importedModelObject | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemId
|
||||
}
|
||||
}
|
||||
'HP' {
|
||||
$baseName = ConvertTo-DriverBaseName -ModelString $normalizedName
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $normalizedName }
|
||||
$systemId = if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) { $importedModelObject.SystemId } else { $null }
|
||||
if ([string]::IsNullOrWhiteSpace($systemId) -and $normalizedName -match '(.+?)\s*\((.+?)\)$') {
|
||||
if ([string]::IsNullOrWhiteSpace($baseName)) { $baseName = $matches[1].Trim() }
|
||||
$systemId = $matches[2].Trim()
|
||||
}
|
||||
$normalizedName = if ([string]::IsNullOrWhiteSpace($systemId)) { $baseName.Trim() } else { Get-DriverDisplayName -BaseName $baseName -Identifier $systemId }
|
||||
if ($importedModelObject.PSObject.Properties['ProductName']) {
|
||||
$importedModelObject.ProductName = $baseName
|
||||
}
|
||||
else {
|
||||
$importedModelObject | Add-Member -NotePropertyName ProductName -NotePropertyValue $baseName
|
||||
}
|
||||
if ($importedModelObject.PSObject.Properties['SystemId']) {
|
||||
$importedModelObject.SystemId = $systemId
|
||||
}
|
||||
else {
|
||||
$importedModelObject | Add-Member -NotePropertyName SystemId -NotePropertyValue $systemId
|
||||
}
|
||||
}
|
||||
default {
|
||||
$normalizedName = $normalizedName.Trim()
|
||||
}
|
||||
}
|
||||
|
||||
if ($skipModel) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($normalizedName)) {
|
||||
WriteLog "Import-DriversJson: Skipping normalized model name for Make '$makeName'."
|
||||
continue
|
||||
}
|
||||
|
||||
$importedModelObject.Name = $normalizedName
|
||||
$importedModelNameFromObject = $normalizedName
|
||||
|
||||
$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"
|
||||
$existingModel.Model = $importedModelNameFromObject
|
||||
|
||||
if ($makeName -eq 'Microsoft' -and $importedModelObject.PSObject.Properties['Link']) {
|
||||
if ($existingModel.Link -ne $importedModelObject.Link) {
|
||||
@@ -327,13 +556,36 @@ function Import-DriversJson {
|
||||
}
|
||||
if ($importedModelObject.PSObject.Properties['MachineType'] -and $existingModel.PSObject.Properties['MachineType'] -and $existingModel.MachineType -ne $importedModelObject.MachineType) {
|
||||
$existingModel.MachineType = $importedModelObject.MachineType
|
||||
$existingModel.Id = $importedModelObject.MachineType # Update Id as well
|
||||
$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)'."
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($makeName -eq 'HP') {
|
||||
$importedProductName = if ($importedModelObject.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.ProductName)) { $importedModelObject.ProductName } else { ConvertTo-DriverBaseName -ModelString $importedModelNameFromObject }
|
||||
if ([string]::IsNullOrWhiteSpace($importedProductName)) { $importedProductName = $importedModelNameFromObject }
|
||||
if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
|
||||
$importedId = $importedModelObject.SystemId
|
||||
}
|
||||
}
|
||||
|
||||
$existingModelsUpdated++
|
||||
WriteLog "Import-DriversJson: Marked existing model '$($existingModel.Make) - $($existingModel.Model)' as imported."
|
||||
}
|
||||
@@ -370,7 +622,7 @@ function Import-DriversJson {
|
||||
$newDriverModel = [PSCustomObject]@{
|
||||
IsSelected = $true
|
||||
Make = $makeName
|
||||
Model = $importedModelNameFromObject # Full display name
|
||||
Model = $importedModelNameFromObject
|
||||
Link = $importedLink
|
||||
Id = $importedId
|
||||
ProductName = $importedProductName
|
||||
@@ -381,6 +633,20 @@ function Import-DriversJson {
|
||||
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
|
||||
}
|
||||
}
|
||||
elseif ($makeName -eq 'HP') {
|
||||
if ($importedModelObject.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($importedModelObject.SystemId)) {
|
||||
$newDriverModel | Add-Member -NotePropertyName SystemId -NotePropertyValue $importedModelObject.SystemId
|
||||
}
|
||||
}
|
||||
$State.Data.allDriverModels.Add($newDriverModel)
|
||||
$newModelsAdded++
|
||||
WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)"
|
||||
@@ -582,10 +848,10 @@ function Invoke-DownloadSelectedDrivers {
|
||||
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" }
|
||||
$catalogBaseName = if ($localWindowsRelease -le 11) { "CatalogIndexPC" } 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" }
|
||||
$catalogUrl = if ($localWindowsRelease -le 11) { "https://downloads.dell.com/catalog/CatalogIndexPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" }
|
||||
|
||||
$downloadCatalog = $true
|
||||
if (Test-Path -Path $dellCatalogXML -PathType Leaf) {
|
||||
@@ -648,13 +914,17 @@ function Invoke-DownloadSelectedDrivers {
|
||||
|
||||
$overallSuccess = $true
|
||||
$successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
$failedDownloads = [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 }
|
||||
# Create a lookup from the original selected drivers to retain full metadata for mapping.
|
||||
$driverLookup = @{}
|
||||
foreach ($driver in $selectedDrivers) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($driver.Model)) {
|
||||
$driverLookup[$driver.Model] = $driver
|
||||
}
|
||||
}
|
||||
|
||||
# Filter for objects that could be results, avoiding stray log strings
|
||||
foreach ($result in ($parallelResults | Where-Object { $_ -is [hashtable] })) {
|
||||
@@ -669,27 +939,61 @@ function Invoke-DownloadSelectedDrivers {
|
||||
if ([string]::IsNullOrWhiteSpace($modelName)) {
|
||||
WriteLog "Could not determine model name from result object: $($result | ConvertTo-Json -Compress -Depth 3)"
|
||||
$overallSuccess = $false
|
||||
$failedDownloads.Add([PSCustomObject]@{
|
||||
Model = 'Unknown model'
|
||||
Status = 'Driver task returned without a model identifier.'
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if ($resultCode -ne 0) {
|
||||
$overallSuccess = $false
|
||||
$failureStatus = $result['Status']
|
||||
if ([string]::IsNullOrWhiteSpace($failureStatus)) { $failureStatus = 'Driver download failed. Check the log for details.' }
|
||||
$failedDownloads.Add([PSCustomObject]@{
|
||||
Model = $modelName
|
||||
Status = $failureStatus
|
||||
})
|
||||
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
|
||||
$driverMetadata = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($modelName) -and $driverLookup.ContainsKey($modelName)) {
|
||||
$driverMetadata = $driverLookup[$modelName]
|
||||
}
|
||||
|
||||
if ($driverMetadata) {
|
||||
$driverRecord = [PSCustomObject]@{
|
||||
Make = $driverMetadata.Make
|
||||
Model = $modelName
|
||||
DriverPath = $driverPath
|
||||
})
|
||||
}
|
||||
if ($driverMetadata.PSObject.Properties['SystemId'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.SystemId)) {
|
||||
$driverRecord | Add-Member -NotePropertyName SystemId -NotePropertyValue $driverMetadata.SystemId
|
||||
}
|
||||
if ($driverMetadata.PSObject.Properties['MachineType'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.MachineType)) {
|
||||
$driverRecord | Add-Member -NotePropertyName MachineType -NotePropertyValue $driverMetadata.MachineType
|
||||
}
|
||||
if ($driverMetadata.PSObject.Properties['ProductName'] -and -not [string]::IsNullOrWhiteSpace($driverMetadata.ProductName)) {
|
||||
$driverRecord | Add-Member -NotePropertyName ProductName -NotePropertyValue $driverMetadata.ProductName
|
||||
}
|
||||
$successfullyDownloaded.Add($driverRecord)
|
||||
}
|
||||
else {
|
||||
WriteLog "Warning: Could not find 'Make' for successful download of model '$modelName'. Skipping from DriverMapping.json."
|
||||
WriteLog "Warning: Could not find driver metadata for successful download of model '$modelName'. Skipping from DriverMapping.json."
|
||||
}
|
||||
}
|
||||
else {
|
||||
$overallSuccess = $false
|
||||
$fallbackStatus = $result['Status']
|
||||
if ([string]::IsNullOrWhiteSpace($fallbackStatus)) { $fallbackStatus = 'Driver download did not return a driver path.' }
|
||||
$failedDownloads.Add([PSCustomObject]@{
|
||||
Model = $modelName
|
||||
Status = $fallbackStatus
|
||||
})
|
||||
WriteLog "Driver download did not provide a path for model $modelName."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,42 +1022,16 @@ function Invoke-DownloadSelectedDrivers {
|
||||
$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 # Model is the display name
|
||||
}
|
||||
}
|
||||
'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."
|
||||
}
|
||||
}
|
||||
$modelObject = Convert-DriverItemToJsonModel -DriverItem $driverItem
|
||||
if ($null -ne $modelObject) {
|
||||
$modelsForThisMake += $modelObject
|
||||
}
|
||||
}
|
||||
# Add the models array to the make-specific object
|
||||
|
||||
if ($modelsForThisMake.Count -gt 0) {
|
||||
$outputJson[$makeName] = @{ Models = $modelsForThisMake }
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure directory exists
|
||||
$parentDir = Split-Path -Path $driversJsonPath -Parent
|
||||
@@ -778,8 +1056,21 @@ function Invoke-DownloadSelectedDrivers {
|
||||
[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")
|
||||
$State.Controls.txtStatus.Text = "Driver download failed. Resolve the errors and try again."
|
||||
$messageLines = [System.Collections.Generic.List[string]]::new()
|
||||
if ($failedDownloads.Count -gt 0) {
|
||||
$messageLines.Add("Driver download failed for:")
|
||||
foreach ($item in ($failedDownloads | Select-Object -First 5)) {
|
||||
$messageLines.Add("- $($item.Model): $($item.Status)")
|
||||
}
|
||||
if ($failedDownloads.Count -gt 5) {
|
||||
$messageLines.Add("...see the log for additional failures.")
|
||||
}
|
||||
}
|
||||
else {
|
||||
$messageLines.Add("One or more driver downloads failed. Check the log for details.")
|
||||
}
|
||||
[System.Windows.MessageBox]::Show(($messageLines -join [System.Environment]::NewLine), "Driver Download Failed", "OK", "Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,17 @@ function Register-EventHandlers {
|
||||
$localState.Controls.chkPromptExternalHardDiskMedia.IsChecked = $false
|
||||
})
|
||||
|
||||
if ($null -ne $State.Controls.cmbBitsPriority) {
|
||||
$State.Controls.cmbBitsPriority.Add_SelectionChanged({
|
||||
param($eventSource, $selectionChangedEventArgs)
|
||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||
if ($null -eq $window -or $null -eq $window.Tag) {
|
||||
return
|
||||
}
|
||||
Update-BitsPrioritySetting -State $window.Tag
|
||||
})
|
||||
}
|
||||
|
||||
# Additional FFU Files events
|
||||
$State.Controls.chkCopyAdditionalFFUFiles.Add_Checked({
|
||||
param($eventSource, $routedEventArgs)
|
||||
|
||||
@@ -118,6 +118,7 @@ function Initialize-UIControls {
|
||||
$State.Controls.txtShareName = $window.FindName('txtShareName')
|
||||
$State.Controls.txtUsername = $window.FindName('txtUsername')
|
||||
$State.Controls.txtThreads = $window.FindName('txtThreads')
|
||||
$State.Controls.cmbBitsPriority = $window.FindName('cmbBitsPriority')
|
||||
$State.Controls.txtMaxUSBDrives = $window.FindName('txtMaxUSBDrives')
|
||||
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
|
||||
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
|
||||
@@ -234,6 +235,7 @@ function Initialize-UIDefaults {
|
||||
$State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName
|
||||
$State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username
|
||||
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
|
||||
$State.Controls.cmbBitsPriority.SelectedItem = $State.Defaults.generalDefaults.BitsPriority
|
||||
$State.Controls.txtMaxUSBDrives.Text = $State.Defaults.generalDefaults.MaxUSBDrives
|
||||
$State.Controls.chkBuildUSBDriveEnable.IsChecked = $State.Defaults.generalDefaults.BuildUSBDriveEnable
|
||||
$State.Controls.chkCompactOS.IsChecked = $State.Defaults.generalDefaults.CompactOS
|
||||
@@ -263,6 +265,7 @@ function Initialize-UIDefaults {
|
||||
$State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
|
||||
$State.Controls.chkCopyAdditionalFFUFiles.IsChecked = $State.Defaults.generalDefaults.CopyAdditionalFFUFiles
|
||||
$State.Controls.additionalFFUPanel.Visibility = if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) { 'Visible' } else { 'Collapsed' }
|
||||
Update-BitsPrioritySetting -State $State
|
||||
|
||||
# Hyper-V Settings defaults from General Defaults
|
||||
Initialize-VMSwitchData -State $State
|
||||
@@ -322,12 +325,16 @@ function Initialize-UIDefaults {
|
||||
|
||||
# Drivers tab UI logic
|
||||
$makeList = @('Microsoft', 'Dell', 'HP', 'Lenovo')
|
||||
if ($null -ne $State.Controls.cmbMake) {
|
||||
# Clear existing items to prevent duplication on re-initialization (e.g., after Restore Defaults)
|
||||
$State.Controls.cmbMake.Items.Clear()
|
||||
foreach ($m in $makeList) {
|
||||
[void]$State.Controls.cmbMake.Items.Add($m)
|
||||
}
|
||||
if ($State.Controls.cmbMake.Items.Count -gt 0) {
|
||||
$State.Controls.cmbMake.SelectedIndex = 0
|
||||
}
|
||||
}
|
||||
Update-DriverDownloadPanelVisibility -State $State
|
||||
|
||||
# Set initial state for driver checkbox interplay
|
||||
@@ -648,14 +655,14 @@ function Initialize-DynamicUIElements {
|
||||
$modelColumn.Header = $modelHeader
|
||||
}
|
||||
|
||||
# Serial Number Column (index 1 in XAML, now 2)
|
||||
# Unique ID Column (index 1 in XAML, now 2)
|
||||
if ($usbDrivesGridView.Columns.Count -gt 2) {
|
||||
$serialColumn = $usbDrivesGridView.Columns[2]
|
||||
$serialHeader = New-Object System.Windows.Controls.GridViewColumnHeader
|
||||
$serialHeader.Content = "Serial Number"
|
||||
$serialHeader.Tag = "SerialNumber" # Property to sort by
|
||||
$serialHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
|
||||
$serialColumn.Header = $serialHeader
|
||||
$uniqueIdColumn = $usbDrivesGridView.Columns[2]
|
||||
$uniqueIdHeader = New-Object System.Windows.Controls.GridViewColumnHeader
|
||||
$uniqueIdHeader.Content = "Unique ID"
|
||||
$uniqueIdHeader.Tag = "UniqueId" # Property to sort by
|
||||
$uniqueIdHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
|
||||
$uniqueIdColumn.Header = $uniqueIdHeader
|
||||
}
|
||||
|
||||
# Size Column (index 2 in XAML, now 3)
|
||||
|
||||
@@ -220,6 +220,32 @@ function Invoke-ProgressUpdate {
|
||||
$ProgressQueue.Enqueue(@{ Identifier = $Identifier; Status = $Status })
|
||||
}
|
||||
|
||||
function Update-BitsPrioritySetting {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[pscustomobject]$State
|
||||
)
|
||||
|
||||
$combo = $State.Controls.cmbBitsPriority
|
||||
if ($null -eq $combo) {
|
||||
WriteLog "BITS priority control not available; skipping priority update."
|
||||
return
|
||||
}
|
||||
|
||||
$selectedPriority = $combo.SelectedItem
|
||||
if ([string]::IsNullOrWhiteSpace($selectedPriority)) {
|
||||
$selectedPriority = 'Normal'
|
||||
}
|
||||
|
||||
try {
|
||||
Set-BitsTransferPriority -Priority $selectedPriority
|
||||
WriteLog "BITS transfer priority set to $selectedPriority."
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to set BITS transfer priority: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Add a function to create a sortable list view
|
||||
function Add-SortableColumn {
|
||||
param(
|
||||
@@ -505,21 +531,29 @@ function Invoke-ListViewSort {
|
||||
[PSCustomObject]$State
|
||||
)
|
||||
|
||||
# Preserve any active CollectionView filter so sorting does not reset a filtered driver model list
|
||||
$existingFilter = $null
|
||||
$existingCollectionView = $null
|
||||
if ($null -ne $listView.ItemsSource) {
|
||||
$existingCollectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($listView.ItemsSource)
|
||||
if ($null -ne $existingCollectionView -and $existingCollectionView.Filter) {
|
||||
$existingFilter = $existingCollectionView.Filter
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure $State.Flags is a hashtable and contains the required sort properties
|
||||
if ($State.Flags -is [hashtable]) {
|
||||
if (-not $State.Flags.ContainsKey('lastSortProperty')) {
|
||||
$State.Flags['lastSortProperty'] = $null
|
||||
}
|
||||
if (-not $State.Flags.ContainsKey('lastSortAscending')) {
|
||||
$State.Flags['lastSortAscending'] = $true # Default to ascending
|
||||
$State.Flags['lastSortAscending'] = $true
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning "Invoke-ListViewSort: \$State.Flags is not a hashtable or is null. Sort state may not work correctly."
|
||||
# Attempt to initialize if $State.Flags is null or unexpectedly not a hashtable,
|
||||
# though this might indicate a deeper issue with $State.Flags initialization.
|
||||
if ($null -eq $State.Flags) { $State.Flags = @{} }
|
||||
if ($State.Flags -is [hashtable]) { # Check again after potential initialization
|
||||
if ($State.Flags -is [hashtable]) {
|
||||
if (-not $State.Flags.ContainsKey('lastSortProperty')) { $State.Flags['lastSortProperty'] = $null }
|
||||
if (-not $State.Flags.ContainsKey('lastSortAscending')) { $State.Flags['lastSortAscending'] = $true }
|
||||
}
|
||||
@@ -534,10 +568,15 @@ function Invoke-ListViewSort {
|
||||
}
|
||||
$State.Flags.lastSortProperty = $property
|
||||
|
||||
# Get items from ItemsSource or Items collection
|
||||
# Build the set of items to sort, enumerating the filtered view if a filter is active
|
||||
$currentItemsSource = $listView.ItemsSource
|
||||
$itemsToSort = @()
|
||||
if ($null -ne $currentItemsSource) {
|
||||
if ($null -ne $existingCollectionView -and $null -ne $existingFilter) {
|
||||
foreach ($vItem in $existingCollectionView) {
|
||||
$itemsToSort += $vItem
|
||||
}
|
||||
}
|
||||
elseif ($null -ne $currentItemsSource) {
|
||||
$itemsToSort = @($currentItemsSource)
|
||||
}
|
||||
else {
|
||||
@@ -548,10 +587,11 @@ function Invoke-ListViewSort {
|
||||
return
|
||||
}
|
||||
|
||||
# Separate selected vs unselected for selected-first ordering
|
||||
$selectedItems = @($itemsToSort | Where-Object { $_.IsSelected })
|
||||
$unselectedItems = @($itemsToSort | Where-Object { -not $_.IsSelected })
|
||||
|
||||
# Define the primary sort criterion
|
||||
# Define primary sort criterion
|
||||
$primarySortDefinition = @{
|
||||
Expression = {
|
||||
$val = $_.$property
|
||||
@@ -579,11 +619,11 @@ function Invoke-ListViewSort {
|
||||
$secondarySortPropertyName = "Key"
|
||||
}
|
||||
else {
|
||||
# Default secondary sort for IsSelected or other properties
|
||||
$secondarySortPropertyName = "Key"
|
||||
}
|
||||
}
|
||||
|
||||
# Add secondary sort definition if applicable
|
||||
if ($null -ne $secondarySortPropertyName -and $property -ne $secondarySortPropertyName) {
|
||||
$itemsHaveSecondaryProperty = $false
|
||||
if ($unselectedItems.Count -gt 0) {
|
||||
@@ -598,35 +638,40 @@ function Invoke-ListViewSort {
|
||||
}
|
||||
|
||||
if ($itemsHaveSecondaryProperty) {
|
||||
# Create a scriptblock for the secondary sort expression dynamically
|
||||
$expressionScriptBlock = [scriptblock]::Create("`$_.$secondarySortPropertyName")
|
||||
|
||||
$secondarySortDefinition = @{
|
||||
Expression = {
|
||||
$val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_
|
||||
if ($null -eq $val) { '' } else { $val }
|
||||
}
|
||||
Ascending = $true # Secondary sort always ascending
|
||||
Ascending = $true
|
||||
}
|
||||
$sortCriteria.Add($secondarySortDefinition)
|
||||
}
|
||||
}
|
||||
|
||||
# Sort unselected items by combined sort criteria
|
||||
$sortedUnselected = $unselectedItems | Sort-Object -Property $sortCriteria.ToArray()
|
||||
# Ensure $sortedUnselected is not null before attempting to add its range
|
||||
if ($null -eq $sortedUnselected) {
|
||||
$sortedUnselected = @()
|
||||
}
|
||||
|
||||
# Combine sorted items: selected items first, then sorted unselected items
|
||||
# Merge selected first, then sorted unselected
|
||||
$newSortedList = [System.Collections.Generic.List[object]]::new()
|
||||
$newSortedList.AddRange($selectedItems)
|
||||
$newSortedList.AddRange($sortedUnselected)
|
||||
|
||||
# Set the new sorted list as the ItemsSource
|
||||
# Try nulling out ItemsSource first to force a more complete refresh
|
||||
# Reset ItemsSource and assign sorted list
|
||||
$listView.ItemsSource = $null
|
||||
$listView.ItemsSource = $newSortedList.ToArray()
|
||||
|
||||
# Reapply preserved filter to maintain the user's filtered view
|
||||
if ($null -ne $existingFilter) {
|
||||
$newView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($listView.ItemsSource)
|
||||
if ($null -ne $newView) {
|
||||
$newView.Filter = $existingFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@@ -126,7 +126,7 @@ $script:mctWindowsReleases = @(
|
||||
|
||||
$script:windowsVersionMap = @{
|
||||
10 = @("22H2")
|
||||
11 = @("22H2", "23H2", "24H2", "25H2")
|
||||
11 = @("25H2", "24H2", "23H2", "22H2")
|
||||
2016 = @("1607") # Windows 10 LTSB 2016 & Server 2016
|
||||
2019 = @("1809") # Windows 10 LTSC 2019 & Server 2019
|
||||
# Note: Server 2016 and LTSB 2016 now share the key 2016, mapping to version "1607"
|
||||
@@ -268,10 +268,15 @@ function Get-AvailableWindowsVersions {
|
||||
# Logic for when an ISO is specified
|
||||
$result.Versions = $validVersions
|
||||
# Set default selection logic (e.g., latest for Win11)
|
||||
if ($SelectedRelease -eq 11 -and $validVersions -contains "24H2") {
|
||||
if ($SelectedRelease -eq 11) {
|
||||
if ($validVersions -contains "25H2") {
|
||||
$result.DefaultVersion = "25H2"
|
||||
}
|
||||
elseif ($validVersions -contains "24H2") {
|
||||
$result.DefaultVersion = "24H2"
|
||||
}
|
||||
elseif ($validVersions.Count -gt 0) {
|
||||
}
|
||||
if (-not $result.DefaultVersion -and $validVersions.Count -gt 0) {
|
||||
$result.DefaultVersion = $validVersions[0]
|
||||
}
|
||||
$result.IsEnabled = $true
|
||||
@@ -280,7 +285,7 @@ function Get-AvailableWindowsVersions {
|
||||
# Logic for when no ISO is specified (MCT scenario)
|
||||
switch ($SelectedRelease) {
|
||||
10 { $result.DefaultVersion = "22H2" }
|
||||
11 { $result.DefaultVersion = "24H2" }
|
||||
11 { $result.DefaultVersion = "25H2" }
|
||||
# Server versions typically require an ISO, but handle just in case
|
||||
2016 { $result.DefaultVersion = "1607" }
|
||||
2019 { $result.DefaultVersion = "1809" }
|
||||
@@ -515,7 +520,7 @@ function Update-WindowsArchCombo {
|
||||
}
|
||||
else {
|
||||
# Standard Windows 11
|
||||
if ($versionValue -eq '24H2') {
|
||||
if ($versionValue -in @('24H2', '25H2')) {
|
||||
$availableArchitectures = @('x64', 'arm64')
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -378,324 +378,9 @@ function Confirm-WingetInstallationUI {
|
||||
|
||||
return $result
|
||||
}
|
||||
# Function to handle downloading a winget application (Modified for ForEach-Object -Parallel)
|
||||
function Start-WingetAppDownloadTask {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[PSCustomObject]$ApplicationItemData, # Pass data, not the UI object
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AppListJsonPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AppsPath, # Pass necessary paths
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OrchestrationPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue, # Add queue parameter
|
||||
[string]$WindowsArch
|
||||
)
|
||||
|
||||
$appName = $ApplicationItemData.Name
|
||||
$appId = $ApplicationItemData.Id
|
||||
$source = $ApplicationItemData.Source
|
||||
$status = "Checking..." # Initial local status
|
||||
$resultCode = -1 # Default to error/unknown
|
||||
$sanitizedAppName = ConvertTo-SafeName -Name $appName
|
||||
|
||||
# Initial status update
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
|
||||
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
|
||||
|
||||
try {
|
||||
# Define paths
|
||||
$userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json"
|
||||
$appFound = $false # Flag to track if the app is found locally
|
||||
# WriteLog "UserAppList Path: $($userAppListPath)"
|
||||
# WriteLog "Checking for existing app in UserAppList.json and content folder."
|
||||
|
||||
# 1. Check UserAppList.json and content
|
||||
if (Test-Path -Path $userAppListPath) {
|
||||
# WriteLog "UserAppList.json found at $($userAppListPath). Checking for app entry."
|
||||
try {
|
||||
$userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json
|
||||
$userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName }
|
||||
|
||||
if ($userAppEntry) {
|
||||
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
|
||||
if (Test-Path -Path $appFolder -PathType Container) {
|
||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($folderSize -gt 1MB) {
|
||||
$appFound = $true
|
||||
$status = "Not Downloaded: App in $userAppListPath and found in $appFolder"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog "Found '$appName' in $userAppListPath and content exists in '$appFolder'."
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||
}
|
||||
else {
|
||||
$appFound = $true
|
||||
$status = "App in '$userAppListPath' but content missing/small in '$appFolder'. Copy content or remove from UserAppList.json."
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
|
||||
}
|
||||
}
|
||||
else {
|
||||
$appFound = $true
|
||||
$status = "App in '$userAppListPath' but content folder '$appFolder' not found. Copy content or remove from UserAppList.json."
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Warning: Could not read or parse '$userAppListPath'. Error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Check existing downloaded Win32 content (folder-based; no WinGetWin32Apps.json dependency)
|
||||
if (-not $appFound -and $source -eq 'winget') {
|
||||
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
|
||||
if (Test-Path -Path $appFolder -PathType Container) {
|
||||
$contentFound = $false
|
||||
if ($ApplicationItemData.Architecture -eq 'x86 x64') {
|
||||
$x86Folder = Join-Path -Path $appFolder -ChildPath "x86"
|
||||
$x64Folder = Join-Path -Path $appFolder -ChildPath "x64"
|
||||
if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) {
|
||||
$x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
$x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($x86Size -gt 1MB -and $x64Size -gt 1MB) {
|
||||
$contentFound = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($folderSize -gt 1MB) {
|
||||
$contentFound = $true
|
||||
}
|
||||
}
|
||||
if ($contentFound) {
|
||||
$appFound = $true
|
||||
$status = "Not Downloaded: Existing content found in $appFolder"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry."
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check MSStore folder
|
||||
if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) {
|
||||
$appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName
|
||||
if (Test-Path -Path $appFolder -PathType Container) {
|
||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||
if ($folderSize -gt 1MB) {
|
||||
$appFound = $true
|
||||
$status = "Already downloaded (MSStore)"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
WriteLog "Found '$appName' content in '$appFolder'."
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 3. If not found locally, add to AppList.json and download
|
||||
if (-not $appFound) {
|
||||
# Add to AppList.json
|
||||
$appListContent = $null
|
||||
$appListDir = Split-Path -Path $AppListJsonPath -Parent
|
||||
if (-not (Test-Path -Path $appListDir -PathType Container)) {
|
||||
New-Item -Path $appListDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
try {
|
||||
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if (-not $appListContent.PSObject.Properties['apps']) {
|
||||
$appListContent = @{ apps = @() }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Warning: Could not read or parse '$AppListJsonPath'. Creating new structure. Error: $($_.Exception.Message)"
|
||||
$appListContent = @{ apps = @() }
|
||||
}
|
||||
}
|
||||
else {
|
||||
$appListContent = @{ apps = @() }
|
||||
}
|
||||
|
||||
$appExistsInAppList = $false
|
||||
if ($appListContent.apps) {
|
||||
foreach ($app in $appListContent.apps) {
|
||||
if ($app.id -eq $appId) {
|
||||
$appExistsInAppList = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $appExistsInAppList) {
|
||||
$newApp = @{ name = $sanitizedAppName; id = $appId; source = $source }
|
||||
if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() }
|
||||
$appListContent.apps += $newApp
|
||||
try {
|
||||
# Use a lock to prevent race conditions when writing to the same file
|
||||
$lockName = "AppListJsonLock"
|
||||
$lock = New-Object System.Threading.Mutex($false, $lockName)
|
||||
try {
|
||||
$lock.WaitOne() | Out-Null
|
||||
# Re-read content inside lock to ensure latest version
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
$currentAppListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if (-not ($currentAppListContent.apps | Where-Object { $_.id -eq $appId })) {
|
||||
$currentAppListContent.apps += $newApp
|
||||
$currentAppListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Added '$appName' to '$AppListJsonPath'."
|
||||
}
|
||||
else {
|
||||
WriteLog "'$appName' already exists in '$AppListJsonPath' (checked inside lock)."
|
||||
}
|
||||
}
|
||||
else {
|
||||
# File doesn't exist, write the initial content
|
||||
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Created '$AppListJsonPath' and added '$appName'."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$lock.ReleaseMutex()
|
||||
$lock.Dispose()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Error saving '$AppListJsonPath'. Error: $($_.Exception.Message)"
|
||||
$status = "Failed to save AppList.json: $($_.Exception.Message)"
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
|
||||
}
|
||||
}
|
||||
else {
|
||||
WriteLog "'$appName' already exists in '$AppListJsonPath'."
|
||||
}
|
||||
|
||||
# Proceed with download
|
||||
$status = "Downloading..."
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
|
||||
# Ensure variables needed by Get-Application are accessible
|
||||
# (Assuming they are available via $using: scope or global scope from main script)
|
||||
# $global:AppsPath = $AppsPath # Potentially redundant
|
||||
# $global:WindowsArch = $ApplicationItemData.Architecture # Potentially redundant
|
||||
# $global:orchestrationPath = $OrchestrationPath # Potentially redundant"
|
||||
WriteLog "Orchestration Path: $($OrchestrationPath)"
|
||||
if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) {
|
||||
New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32"
|
||||
if ($source -eq "winget" -and -not (Test-Path -Path $win32Folder -PathType Container)) {
|
||||
New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore"
|
||||
if ($source -eq "msstore" -and -not (Test-Path -Path $storeAppsFolder -PathType Container)) {
|
||||
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
# Call Get-Application
|
||||
$resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -ApplicationArch $ApplicationItemData.Architecture -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -SkipWin32Json -ErrorAction Stop
|
||||
|
||||
# Determine status based on result code
|
||||
switch ($resultCode) {
|
||||
0 { $status = "Downloaded successfully" }
|
||||
1 { $status = "Error: No app installers were found" }
|
||||
2 { $status = "Silent install switch could not be found. Did not download." }
|
||||
3 { $status = "Error: Publisher does not support download" }
|
||||
4 { $status = "Skipped: Use 'msstore' source instead." }
|
||||
default { $status = "Downloaded with status: $resultCode" } # Should not happen with current Get-Application
|
||||
}
|
||||
|
||||
# Remove app from AppList.json if silent install switch could not be found (resultCode 2)
|
||||
if ($resultCode -eq 2) {
|
||||
try {
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if ($appListContent.apps) {
|
||||
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
|
||||
$appListContent.apps = $filteredApps
|
||||
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to missing silent install switch."
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$status = $_.Exception.Message
|
||||
WriteLog "Download error for $($appName): $($_.Exception.Message)"
|
||||
$resultCode = 1 # Indicate error
|
||||
# Enqueue error status
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
|
||||
# Remove app from AppList.json if publisher does not support download
|
||||
if ($_.Exception.Message -match "does not support downloads by the publisher") {
|
||||
try {
|
||||
if (Test-Path -Path $AppListJsonPath) {
|
||||
$appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json
|
||||
if ($appListContent.apps) {
|
||||
$filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId })
|
||||
$appListContent.apps = $filteredApps
|
||||
$appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8
|
||||
WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to publisher download restriction."
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} # End if (-not $appFound)
|
||||
|
||||
}
|
||||
catch {
|
||||
$status = $_.Exception.Message
|
||||
WriteLog "Unexpected error in Start-WingetAppDownloadTask for $($appName): $($_.Exception.Message)"
|
||||
$resultCode = 1 # Indicate error
|
||||
# Enqueue error status
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
finally {
|
||||
# Ensure status is not empty before returning
|
||||
if ([string]::IsNullOrEmpty($status)) {
|
||||
$status = "Unknown failure" # Provide a default error status
|
||||
WriteLog "Status was empty for $appName ($appId), setting to default error."
|
||||
if ($resultCode -ne 0 -and $resultCode -ne 1 -and $resultCode -ne 2) {
|
||||
$resultCode = -1 # Ensure resultCode reflects an error if it was empty
|
||||
}
|
||||
# Enqueue the final (error) status if it was previously empty
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
elseif ($resultCode -ne 0) {
|
||||
# Enqueue the final status if it's an error (already set in try/catch)
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
else {
|
||||
# Enqueue the final success status
|
||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||
}
|
||||
}
|
||||
|
||||
# Prepare the return object as a Hashtable
|
||||
$returnObject = @{ Id = $appId; Status = $status; ResultCode = $resultCode }
|
||||
|
||||
# Return the final status and result code as a Hashtable
|
||||
return $returnObject
|
||||
}
|
||||
# Note: Start-WingetAppDownloadTask has been moved to FFU.Common.Winget.psm1
|
||||
# to enable code reuse between UI and CLI builds. It is imported via the FFU.Common module.
|
||||
|
||||
function Invoke-WingetDownload {
|
||||
param(
|
||||
@@ -721,11 +406,13 @@ function Invoke-WingetDownload {
|
||||
$localOrchestrationPath = Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath "Orchestration"
|
||||
|
||||
# Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing
|
||||
# UI downloads skip WinGetWin32Apps.json creation - it's generated at build time
|
||||
$taskArguments = @{
|
||||
AppsPath = $localAppsPath
|
||||
AppListJsonPath = $localAppListJsonPath
|
||||
OrchestrationPath = $localOrchestrationPath
|
||||
WindowsArch = $localWindowsArch
|
||||
SkipWin32Json = $true
|
||||
}
|
||||
|
||||
# Select only necessary properties before passing to Invoke-ParallelProcessing
|
||||
|
||||
@@ -117,6 +117,7 @@ function Get-GeneralDefaults {
|
||||
ShareName = "FFUCaptureShare"
|
||||
Username = "ffu_user"
|
||||
Threads = 5
|
||||
BitsPriority = 'Normal'
|
||||
MaxUSBDrives = 5
|
||||
BuildUSBDriveEnable = $false
|
||||
CompactOS = $true
|
||||
@@ -183,16 +184,32 @@ function Get-GeneralDefaults {
|
||||
}
|
||||
|
||||
# Function to get USB Drives (Moved from BuildFFUVM_UI.ps1)
|
||||
# Uses Get-Disk to retrieve UniqueId which is more reliable than SerialNumber
|
||||
# UniqueId is trimmed to remove the machine name suffix (characters after colon)
|
||||
function Get-USBDrives {
|
||||
Get-WmiObject Win32_DiskDrive | Where-Object {
|
||||
($_.MediaType -eq 'Removable Media' -or $_.MediaType -eq 'External hard disk media')
|
||||
} | ForEach-Object {
|
||||
$size = [math]::Round($_.Size / 1GB, 2)
|
||||
$serialNumber = if ($_.SerialNumber) { $_.SerialNumber.Trim() } else { "N/A" }
|
||||
# Get the disk using the index to retrieve UniqueId
|
||||
$disk = Get-Disk -Number $_.Index -ErrorAction SilentlyContinue
|
||||
# Trim the machine name suffix (everything after the colon) from UniqueId
|
||||
$uniqueId = if ($disk -and $disk.UniqueId) {
|
||||
$rawId = $disk.UniqueId
|
||||
if ($rawId -match ':') {
|
||||
$rawId.Split(':')[0]
|
||||
}
|
||||
else {
|
||||
$rawId
|
||||
}
|
||||
}
|
||||
else {
|
||||
"N/A"
|
||||
}
|
||||
@{
|
||||
IsSelected = $false
|
||||
Model = $_.Model.Trim()
|
||||
SerialNumber = $serialNumber
|
||||
UniqueId = $uniqueId
|
||||
Size = $size
|
||||
DriveIndex = $_.Index
|
||||
}
|
||||
|
||||
@@ -121,13 +121,6 @@ $Destination = $Drive + ":\"
|
||||
Start-Job -ScriptBlock $jobScriptBlock -ArgumentList $ImagesPath, $Destination | Out-Null
|
||||
}
|
||||
}
|
||||
if(!($Images)){
|
||||
foreach ($Drive in $DeployDrives) {
|
||||
WriteLog "Create images directory"
|
||||
$drivepath = $Drive + ":\"
|
||||
New-Item -Path "$drivepath" -Name Images -ItemType Directory -Force -Confirm: $false | Out-Null
|
||||
}
|
||||
}
|
||||
if($Drivers){
|
||||
writelog "Copying driver files to all drives labeled deploy concurrently"
|
||||
foreach ($Drive in $DeployDrives) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Reference in New Issue
Block a user