mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
3613 lines
188 KiB
PowerShell
3613 lines
188 KiB
PowerShell
# FFU UI Core Logic Module
|
|
# Contains non-UI specific helper functions, data retrieval, and core processing logic.
|
|
|
|
#Requires -Modules BitsTransfer
|
|
|
|
# Import shared modules
|
|
Import-Module "$PSScriptRoot\..\common\FFU.Common.Core.psm1"
|
|
Import-Module "$PSScriptRoot\..\common\FFU.Common.Winget.psm1"
|
|
Import-Module "$PSScriptRoot\..\common\FFU.Common.Drivers.psm1"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# SECTION: Module Variables (Static Data & State)
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Mutex for log file access is now in FFU.Common.Core.psm1
|
|
|
|
# Static data moved from UI_Helpers
|
|
$script:allowedFeatures = @(
|
|
"AppServerClient", "Client-DeviceLockdown", "Client-EmbeddedBootExp", "Client-EmbeddedLogon",
|
|
"Client-EmbeddedShellLauncher", "Client-KeyboardFilter", "Client-ProjFS", "Client-UnifiedWriteFilter",
|
|
"Containers", "Containers-DisposableClientVM", "Containers-HNS", "Containers-SDN", "DataCenterBridging",
|
|
"DirectoryServices-ADAM-Client", "DirectPlay", "HostGuardian", "HypervisorPlatform", "IIS-ApplicationDevelopment",
|
|
"IIS-ApplicationInit", "IIS-ASP", "IIS-ASPNET45", "IIS-BasicAuthentication", "IIS-CertProvider",
|
|
"IIS-CGI", "IIS-ClientCertificateMappingAuthentication", "IIS-CommonHttpFeatures", "IIS-CustomLogging",
|
|
"IIS-DefaultDocument", "IIS-DirectoryBrowsing", "IIS-DigestAuthentication", "IIS-ESP", "IIS-FTPServer",
|
|
"IIS-FTPExtensibility", "IIS-FTPSvc", "IIS-HealthAndDiagnostics", "IIS-HostableWebCore", "IIS-HttpCompressionDynamic",
|
|
"IIS-HttpCompressionStatic", "IIS-HttpErrors", "IIS-HttpLogging", "IIS-HttpRedirect", "IIS-HttpTracing",
|
|
"IIS-IPSecurity", "IIS-IIS6ManagementCompatibility", "IIS-IISCertificateMappingAuthentication",
|
|
"IIS-ISAPIExtensions", "IIS-ISAPIFilter", "IIS-LoggingLibraries", "IIS-ManagementConsole", "IIS-ManagementService",
|
|
"IIS-ManagementScriptingTools", "IIS-Metabase", "IIS-NetFxExtensibility", "IIS-NetFxExtensibility45",
|
|
"IIS-ODBCLogging", "IIS-Performance", "IIS-RequestFiltering", "IIS-RequestMonitor", "IIS-Security", "IIS-ServerSideIncludes",
|
|
"IIS-StaticContent", "IIS-URLAuthorization", "IIS-WebDAV", "IIS-WebServer", "IIS-WebServerManagementTools",
|
|
"IIS-WebServerRole", "IIS-WebSockets", "LegacyComponents", "MediaPlayback", "Microsoft-Hyper-V", "Microsoft-Hyper-V-All",
|
|
"Microsoft-Hyper-V-Hypervisor", "Microsoft-Hyper-V-Management-Clients", "Microsoft-Hyper-V-Management-PowerShell",
|
|
"Microsoft-Hyper-V-Services", "Microsoft-Windows-Subsystem-Linux", "MSMQ-ADIntegration", "MSMQ-Container", "MSMQ-DCOMProxy",
|
|
"MSMQ-HTTP", "MSMQ-Multicast", "MSMQ-Server", "MSMQ-Triggers", "MultiPoint-Connector", "MultiPoint-Connector-Services",
|
|
"MultiPoint-Tools", "NetFx3", "NetFx4-AdvSrvs", "NetFx4Extended-ASPNET45", "NFS-Administration", "Printing-Foundation-Features",
|
|
"Printing-Foundation-InternetPrinting-Client", "Printing-Foundation-LPDPrintService", "Printing-Foundation-LPRPortMonitor",
|
|
"Printing-PrintToPDFServices-Features", "Printing-XPSServices-Features", "SearchEngine-Client-Package",
|
|
"ServicesForNFS-ClientOnly", "SimpleTCP", "SMB1Protocol", "SMB1Protocol-Client", "SMB1Protocol-Deprecation",
|
|
"SMB1Protocol-Server", "SmbDirect", "TFTP", "TelnetClient", "TIFFIFilter", "VirtualMachinePlatform", "WAS-ConfigurationAPI",
|
|
"WAS-NetFxEnvironment", "WAS-ProcessModel", "WAS-WindowsActivationService", "WCF-HTTP-Activation", "WCF-HTTP-Activation45",
|
|
"WCF-MSMQ-Activation45", "WCF-MSMQ-Activation", "WCF-NonHTTP-Activation", "WCF-Pipe-Activation45", "WCF-Services45",
|
|
"WCF-TCP-Activation45", "WCF-TCP-PortSharing45", "Windows-Defender-ApplicationGuard",
|
|
"Windows-Defender-Default-Definitions", "Windows-Identity-Foundation", "WindowsMediaPlayer", "WorkFolders-Client"
|
|
)
|
|
|
|
$script:skuList = @(
|
|
'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', 'Standard',
|
|
'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)'
|
|
)
|
|
|
|
$script:allowedLangs = @(
|
|
'ar-sa', 'bg-bg', 'cs-cz', 'da-dk', 'de-de', 'el-gr', 'en-gb', 'en-us', 'es-es', 'es-mx', 'et-ee',
|
|
'fi-fi', 'fr-ca', 'fr-fr', 'he-il', 'hr-hr', 'hu-hu', 'it-it', 'ja-jp', 'ko-kr', 'lt-lt', 'lv-lv',
|
|
'nb-no', 'nl-nl', 'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk', 'sl-si', 'sr-latn-rs',
|
|
'sv-se', 'th-th', 'tr-tr', 'uk-ua', 'zh-cn', 'zh-tw'
|
|
)
|
|
|
|
$script:allWindowsReleases = @(
|
|
[PSCustomObject]@{ Display = "Windows 10"; Value = 10 },
|
|
[PSCustomObject]@{ Display = "Windows 11"; Value = 11 },
|
|
[PSCustomObject]@{ Display = "Windows Server 2016"; Value = 2016 },
|
|
[PSCustomObject]@{ Display = "Windows Server 2019"; Value = 2019 },
|
|
[PSCustomObject]@{ Display = "Windows Server 2022"; Value = 2022 },
|
|
[PSCustomObject]@{ Display = "Windows Server 2025"; Value = 2025 }
|
|
)
|
|
|
|
$script:mctWindowsReleases = @(
|
|
[PSCustomObject]@{ Display = "Windows 10"; Value = 10 },
|
|
[PSCustomObject]@{ Display = "Windows 11"; Value = 11 }
|
|
)
|
|
|
|
$script:windowsVersionMap = @{
|
|
10 = @("22H2")
|
|
11 = @("22H2", "23H2", "24H2")
|
|
2016 = @("1607")
|
|
2019 = @("1809")
|
|
2022 = @("21H2")
|
|
2025 = @("24H2")
|
|
}
|
|
|
|
# --------------------------------------------------------------------------
|
|
# SECTION: Logging Function (Moved from UI_Helpers)
|
|
# --------------------------------------------------------------------------
|
|
# WriteLog function has been moved to FFU.Common.Core.psm1
|
|
# All WriteLog calls in this module will now use the common WriteLog.
|
|
|
|
# --------------------------------------------------------------------------
|
|
# SECTION: Data Retrieval Functions (Moved from UI_Helpers & BuildFFUVM_UI)
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Function to get VM Switch names and associated IP addresses (Moved from UI_Helpers)
|
|
function Get-VMSwitchData {
|
|
[CmdletBinding()]
|
|
param() # No parameters needed
|
|
|
|
$switchMap = @{}
|
|
$switchNames = @()
|
|
|
|
try {
|
|
# Attempt to get Hyper-V virtual switches
|
|
# SilentlyContinue is used as Hyper-V role might not be installed
|
|
$allSwitches = Get-VMSwitch -ErrorAction SilentlyContinue
|
|
if ($null -ne $allSwitches) {
|
|
foreach ($sw in $allSwitches) {
|
|
# Construct a pattern to find the network adapter associated with the vSwitch
|
|
# The adapter name often includes the vSwitch name in parentheses
|
|
$adapterNamePattern = "*($($sw.Name))*"
|
|
|
|
# Attempt to find the network adapter associated with the vSwitch
|
|
# Select-Object -First 1 ensures we only get one adapter if multiple match (unlikely but possible)
|
|
$netAdapter = Get-NetAdapter -Name $adapterNamePattern -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
|
|
if ($netAdapter) {
|
|
# Get IPv4 addresses for the found adapter's interface index
|
|
$netIPs = Get-NetIPAddress -InterfaceIndex $netAdapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue
|
|
|
|
# Filter out Automatic Private IP Addressing (APIPA) addresses (169.254.x.x)
|
|
# and select the first valid IP found.
|
|
$validIP = $netIPs | Where-Object { $_.IPAddress -notlike '169.254.*' -and $_.IPAddress } | Select-Object -First 1
|
|
|
|
if ($validIP) {
|
|
# Store the valid IP address in the map with the switch name as the key
|
|
$switchMap[$sw.Name] = $validIP.IPAddress
|
|
# Log the found IP address for debugging/information using WriteLog
|
|
WriteLog "Found IP $($validIP.IPAddress) for vSwitch '$($sw.Name)' (Adapter: $($netAdapter.Name)). Adding to list."
|
|
# Add the switch name to the list ONLY if a valid IP was found
|
|
$switchNames += $sw.Name
|
|
}
|
|
else {
|
|
# Log if no valid non-APIPA IP was found for the adapter
|
|
WriteLog "No valid non-APIPA IPv4 address found for vSwitch '$($sw.Name)' (Adapter: $($netAdapter.Name)). Skipping from list."
|
|
# Do NOT add $sw.Name to $switchNames
|
|
}
|
|
}
|
|
else {
|
|
# Log if no matching network adapter was found for the vSwitch
|
|
WriteLog "Could not find a network adapter matching pattern '$adapterNamePattern' for vSwitch '$($sw.Name)'. Skipping from list."
|
|
# Do NOT add $sw.Name to $switchNames
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# Log if no vSwitches were found at all (Hyper-V might be disabled or not installed)
|
|
WriteLog "No Hyper-V virtual switches found on this system."
|
|
}
|
|
}
|
|
catch {
|
|
# Log any unexpected errors during the process
|
|
WriteLog "Error occurred while getting VM Switch data: $($_.Exception.Message)"
|
|
# Optionally re-throw or handle the error appropriately depending on requirements
|
|
# For UI stability, we might just log and return empty/partial data
|
|
}
|
|
|
|
# Return a custom object containing both the list of switch names and the map of names to IP addresses
|
|
return [PSCustomObject]@{
|
|
SwitchNames = $switchNames
|
|
SwitchMap = $switchMap
|
|
}
|
|
}
|
|
|
|
# Function to return the default settings and static lists (Moved from UI_Helpers)
|
|
function Get-WindowsSettingsDefaults {
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
return [PSCustomObject]@{
|
|
DefaultISOPath = ""
|
|
DefaultWindowsArch = "x64"
|
|
DefaultWindowsLang = "en-us"
|
|
DefaultWindowsSKU = "Pro"
|
|
DefaultMediaType = "Consumer"
|
|
DefaultOptionalFeatures = ""
|
|
DefaultProductKey = ""
|
|
AllowedFeatures = $script:allowedFeatures # Return the list
|
|
SkuList = $script:skuList
|
|
AllowedLanguages = $script:allowedLangs
|
|
AllowedArchitectures = @('x86', 'x64', 'arm64')
|
|
AllowedMediaTypes = @('Consumer', 'Business')
|
|
}
|
|
}
|
|
|
|
# Function to get the appropriate list of Windows Releases based on ISO path (Moved from UI_Helpers)
|
|
function Get-AvailableWindowsReleases {
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$IsoPath
|
|
)
|
|
|
|
if ([string]::IsNullOrEmpty($IsoPath)) {
|
|
return $script:mctWindowsReleases
|
|
}
|
|
else {
|
|
return $script:allWindowsReleases
|
|
}
|
|
}
|
|
|
|
# Function to get available Windows Versions for a given release and ISO path (Moved from UI_Helpers)
|
|
function Get-AvailableWindowsVersions {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[int]$SelectedRelease,
|
|
|
|
[string]$IsoPath
|
|
)
|
|
|
|
$result = [PSCustomObject]@{
|
|
Versions = @()
|
|
DefaultVersion = $null
|
|
IsEnabled = $false
|
|
}
|
|
|
|
if (-not $script:windowsVersionMap.ContainsKey($SelectedRelease)) {
|
|
return $result # Return empty/disabled state
|
|
}
|
|
|
|
$validVersions = $script:windowsVersionMap[$SelectedRelease]
|
|
|
|
if ([string]::IsNullOrEmpty($IsoPath)) {
|
|
# Logic for when no ISO is specified (MCT scenario)
|
|
switch ($SelectedRelease) {
|
|
10 { $result.DefaultVersion = "22H2" }
|
|
11 { $result.DefaultVersion = "24H2" }
|
|
# Server versions typically require an ISO, but handle just in case
|
|
2016 { $result.DefaultVersion = "1607" }
|
|
2019 { $result.DefaultVersion = "1809" }
|
|
2022 { $result.DefaultVersion = "21H2" }
|
|
2025 { $result.DefaultVersion = "24H2" }
|
|
default { $result.DefaultVersion = $validVersions[0] }
|
|
}
|
|
$result.Versions = @($result.DefaultVersion) # Only the default is available/relevant
|
|
$result.IsEnabled = $false # Combo should be disabled
|
|
}
|
|
else {
|
|
# 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") {
|
|
$result.DefaultVersion = "24H2"
|
|
}
|
|
elseif ($validVersions.Count -gt 0) {
|
|
$result.DefaultVersion = $validVersions[0] # Default to first in list otherwise
|
|
}
|
|
$result.IsEnabled = $true # Combo should be enabled
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
# Function to return general default settings for various UI elements (Moved from UI_Helpers)
|
|
function Get-GeneralDefaults {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$FFUDevelopmentPath # Base path needed to derive other paths
|
|
)
|
|
|
|
# Derive paths based on the main development path
|
|
$appsPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "Apps"
|
|
$driversPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "Drivers"
|
|
$peDriversPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "PEDrivers"
|
|
$vmLocationPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "VM"
|
|
$ffuCapturePath = Join-Path -Path $FFUDevelopmentPath -ChildPath "FFU"
|
|
$officePath = Join-Path -Path $appsPath -ChildPath "Office"
|
|
$appListJsonPath = Join-Path -Path $appsPath -ChildPath "AppList.json"
|
|
|
|
return [PSCustomObject]@{
|
|
# Build Tab Defaults
|
|
CustomFFUNameTemplate = "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}"
|
|
FFUCaptureLocation = $ffuCapturePath
|
|
ShareName = "FFUCaptureShare"
|
|
Username = "ffu_user"
|
|
BuildUSBDriveEnable = $false
|
|
CompactOS = $false
|
|
Optimize = $false
|
|
AllowVHDXCaching = $false
|
|
CreateCaptureMedia = $false
|
|
CreateDeploymentMedia = $false
|
|
AllowExternalHardDiskMedia = $false
|
|
PromptExternalHardDiskMedia = $false
|
|
SelectSpecificUSBDrives = $false
|
|
CopyAutopilot = $false # New
|
|
CopyUnattend = $false # New
|
|
CopyPPKG = $false # New
|
|
CleanupAppsISO = $false
|
|
CleanupCaptureISO = $false
|
|
CleanupDeployISO = $false
|
|
CleanupDrivers = $false
|
|
RemoveFFU = $false
|
|
RemoveApps = $false
|
|
RemoveUpdates = $false
|
|
# Hyper-V Settings Defaults
|
|
VMHostIPAddress = "" # Requires user input
|
|
DiskSizeGB = 30
|
|
MemoryGB = 4
|
|
Processors = 4
|
|
VMLocation = $vmLocationPath
|
|
VMNamePrefix = "_FFU"
|
|
LogicalSectorSize = 512
|
|
# Updates Tab Defaults
|
|
UpdateLatestCU = $false
|
|
UpdateLatestNet = $false
|
|
UpdateLatestDefender = $false
|
|
UpdateEdge = $false
|
|
UpdateOneDrive = $false
|
|
UpdateLatestMSRT = $false
|
|
UpdatePreviewCU = $false
|
|
# Applications Tab Defaults
|
|
InstallApps = $false
|
|
ApplicationPath = $appsPath
|
|
AppListJsonPath = $appListJsonPath
|
|
InstallWingetApps = $false
|
|
BringYourOwnApps = $false
|
|
# M365 Apps/Office Tab Defaults
|
|
InstallOffice = $false
|
|
OfficePath = $officePath
|
|
CopyOfficeConfigXML = $false
|
|
OfficeConfigXMLFilePath = "" # Requires user input
|
|
# Drivers Tab Defaults
|
|
DriversFolder = $driversPath
|
|
PEDriversFolder = $peDriversPath
|
|
DownloadDrivers = $false
|
|
InstallDrivers = $false
|
|
CopyDrivers = $false
|
|
CopyPEDrivers = $false
|
|
}
|
|
}
|
|
|
|
# Function to get the list of Dell models from the catalog using XML streaming (Moved from UI_Helpers)
|
|
# Depends on private functions: Start-BitsTransferWithRetry, Invoke-Process
|
|
function Get-DellDriversModelList {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$WindowsRelease,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers)
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Make # Should be 'Dell'
|
|
)
|
|
|
|
# Define Dell specific drivers folder and catalog file names
|
|
$dellDriversFolder = Join-Path -Path $DriversFolder -ChildPath "Dell"
|
|
$catalogBaseName = if ($WindowsRelease -le 11) { "CatalogPC" } else { "Catalog" }
|
|
$dellCabFile = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).cab"
|
|
$dellCatalogXML = Join-Path -Path $dellDriversFolder -ChildPath "$($catalogBaseName).xml"
|
|
$catalogUrl = if ($WindowsRelease -le 11) { "http://downloads.dell.com/catalog/CatalogPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" }
|
|
|
|
$uniqueModelNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
$reader = $null # Initialize reader variable
|
|
|
|
try {
|
|
# Check if the Dell catalog XML exists and is recent
|
|
$downloadCatalog = $true
|
|
if (Test-Path -Path $dellCatalogXML -PathType Leaf) {
|
|
WriteLog "Dell Catalog XML found: $dellCatalogXML"
|
|
$dellCatalogCreationTime = (Get-Item $dellCatalogXML).CreationTime
|
|
WriteLog "Dell Catalog XML Creation time: $dellCatalogCreationTime"
|
|
# Check if the XML file is less than 7 days old
|
|
if (((Get-Date) - $dellCatalogCreationTime).TotalDays -lt 7) {
|
|
WriteLog "Using existing Dell Catalog XML (less than 7 days old): $dellCatalogXML"
|
|
$downloadCatalog = $false
|
|
}
|
|
else {
|
|
WriteLog "Existing Dell Catalog XML is older than 7 days: $dellCatalogXML"
|
|
}
|
|
}
|
|
else {
|
|
WriteLog "Dell Catalog XML not found: $dellCatalogXML"
|
|
}
|
|
|
|
if ($downloadCatalog) {
|
|
WriteLog "Attempting to download and extract Dell Catalog for Get-DellDriversModelList..."
|
|
# Ensure Dell drivers folder exists
|
|
if (-not (Test-Path -Path $dellDriversFolder -PathType Container)) {
|
|
WriteLog "Creating Dell drivers folder: $dellDriversFolder"
|
|
New-Item -Path $dellDriversFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
# Check URL accessibility
|
|
try {
|
|
$request = [System.Net.WebRequest]::Create($catalogUrl)
|
|
$request.Method = 'HEAD'; $response = $request.GetResponse(); $response.Close()
|
|
}
|
|
catch { throw "Dell Catalog URL '$catalogUrl' not accessible: $($_.Exception.Message)" }
|
|
|
|
# Remove existing files before download if they exist
|
|
if (Test-Path -Path $dellCabFile) { Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue }
|
|
if (Test-Path -Path $dellCatalogXML) { Remove-Item -Path $dellCatalogXML -Force -ErrorAction SilentlyContinue }
|
|
|
|
WriteLog "Downloading Dell Catalog cab file: $catalogUrl to $dellCabFile"
|
|
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $dellCabFile
|
|
WriteLog "Dell Catalog cab file downloaded to $dellCabFile"
|
|
|
|
WriteLog "Extracting Dell Catalog cab file '$dellCabFile' to '$dellCatalogXML'"
|
|
Invoke-Process -FilePath "Expand.exe" -ArgumentList """$dellCabFile"" ""$dellCatalogXML""" | Out-Null
|
|
WriteLog "Dell Catalog cab file extracted to $dellCatalogXML"
|
|
|
|
# Delete the CAB file after extraction
|
|
WriteLog "Deleting Dell Catalog CAB file: $dellCabFile"
|
|
Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# Ensure the XML file exists before trying to read it
|
|
if (-not (Test-Path -Path $dellCatalogXML -PathType Leaf)) {
|
|
throw "Dell Catalog XML file '$dellCatalogXML' not found after download/check attempt."
|
|
}
|
|
|
|
# Use XmlReader for streaming from the XML file
|
|
$settings = New-Object System.Xml.XmlReaderSettings
|
|
$settings.IgnoreWhitespace = $true
|
|
$settings.IgnoreComments = $true
|
|
# $settings.DtdProcessing = [System.Xml.DtdProcessing]::Ignore # Optional
|
|
|
|
$reader = [System.Xml.XmlReader]::Create($dellCatalogXML, $settings)
|
|
WriteLog "Starting XML stream parsing for Dell models from '$dellCatalogXML'..."
|
|
|
|
$isDriverComponent = $false
|
|
$isModelElement = $false
|
|
$modelDepth = -1 # Track depth to handle nested elements if needed
|
|
|
|
# Read through the XML stream node by node
|
|
while ($reader.Read()) {
|
|
switch ($reader.NodeType) {
|
|
([System.Xml.XmlNodeType]::Element) {
|
|
switch ($reader.Name) {
|
|
'SoftwareComponent' { $isDriverComponent = $false } # Reset flag
|
|
'ComponentType' { if ($reader.GetAttribute('value') -eq 'DRVR') { $isDriverComponent = $true } }
|
|
'Model' { if ($isDriverComponent) { $isModelElement = $true; $modelDepth = $reader.Depth } }
|
|
}
|
|
}
|
|
([System.Xml.XmlNodeType]::CDATA) {
|
|
if ($isModelElement -and $isDriverComponent) {
|
|
$modelName = $reader.Value.Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($modelName)) { $uniqueModelNames.Add($modelName) | Out-Null }
|
|
$isModelElement = $false # Reset after reading CDATA
|
|
}
|
|
}
|
|
([System.Xml.XmlNodeType]::EndElement) {
|
|
switch ($reader.Name) {
|
|
'SoftwareComponent' { $isDriverComponent = $false; $isModelElement = $false; $modelDepth = -1 }
|
|
'Model' { if ($reader.Depth -eq $modelDepth) { $isModelElement = $false; $modelDepth = -1 } }
|
|
}
|
|
}
|
|
}
|
|
} # End while ($reader.Read())
|
|
|
|
WriteLog "Finished XML stream parsing. Found $($uniqueModelNames.Count) unique Dell models."
|
|
|
|
}
|
|
catch {
|
|
WriteLog "Error getting Dell models: $($_.Exception.ToString())" # Log full exception
|
|
throw "Failed to retrieve Dell models. Check log for details." # Re-throw for UI handling
|
|
}
|
|
finally {
|
|
# Ensure the reader is closed and disposed
|
|
if ($null -ne $reader) {
|
|
$reader.Dispose()
|
|
}
|
|
# REMOVED: Cleanup of temp folder - XML is kept in DriversFolder
|
|
# Ensure CAB file is deleted even if extraction failed but download succeeded
|
|
if (Test-Path -Path $dellCabFile) {
|
|
WriteLog "Cleaning up downloaded Dell CAB file: $dellCabFile"
|
|
Remove-Item -Path $dellCabFile -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# Convert HashSet to sorted list of PSCustomObjects
|
|
$models = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
foreach ($modelName in ($uniqueModelNames | Sort-Object)) {
|
|
$models.Add([PSCustomObject]@{
|
|
Make = $Make
|
|
Model = $modelName
|
|
# Link is not applicable here like for Microsoft
|
|
})
|
|
}
|
|
|
|
return $models
|
|
}
|
|
|
|
# Function to get the list of Microsoft Surface models
|
|
function Get-MicrosoftDriversModelList {
|
|
[CmdletBinding()]
|
|
param(
|
|
[hashtable]$Headers, # Pass necessary headers
|
|
[string]$UserAgent # Pass UserAgent
|
|
)
|
|
|
|
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120"
|
|
$models = @()
|
|
|
|
try {
|
|
WriteLog "Getting Surface driver information from $url"
|
|
$OriginalVerbosePreference = $VerbosePreference
|
|
$VerbosePreference = 'SilentlyContinue'
|
|
# Use passed-in UserAgent and Headers
|
|
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent
|
|
$VerbosePreference = $OriginalVerbosePreference
|
|
WriteLog "Complete"
|
|
|
|
WriteLog "Parsing web content for models and download links"
|
|
$html = $webContent.Content
|
|
$divPattern = '<div[^>]*class="selectable-content-options__option-content(?: ocHidden)?"[^>]*>(.*?)</div>'
|
|
$divMatches = [regex]::Matches($html, $divPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
|
|
foreach ($divMatch in $divMatches) {
|
|
$divContent = $divMatch.Groups[1].Value
|
|
$tablePattern = '<table[^>]*>(.*?)</table>'
|
|
$tableMatches = [regex]::Matches($divContent, $tablePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
|
|
foreach ($tableMatch in $tableMatches) {
|
|
$tableContent = $tableMatch.Groups[1].Value
|
|
$rowPattern = '<tr[^>]*>(.*?)</tr>'
|
|
$rowMatches = [regex]::Matches($tableContent, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
|
|
foreach ($rowMatch in $rowMatches) {
|
|
$rowContent = $rowMatch.Groups[1].Value
|
|
$cellPattern = '<td[^>]*>\s*(?:<p[^>]*>)?(.*?)(?:</p>)?\s*</td>'
|
|
$cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
|
|
if ($cellMatches.Count -ge 2) {
|
|
$modelName = ($cellMatches[0].Groups[1].Value).Trim()
|
|
$secondTdContent = $cellMatches[1].Groups[1].Value.Trim()
|
|
# $linkPattern = '<a[^>]+href="([^"]+)"[^>]*>'
|
|
# Change linkPattern to match https://www.microsoft.com/download/details.aspx?id=
|
|
$linkPattern = '<a[^>]+href="(https://www\.microsoft\.com/download/details\.aspx\?id=\d+)"[^>]*>'
|
|
$linkMatch = [regex]::Match($secondTdContent, $linkPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
|
|
if ($linkMatch.Success) {
|
|
$modelLink = $linkMatch.Groups[1].Value
|
|
}
|
|
else {
|
|
continue
|
|
}
|
|
|
|
$models += [PSCustomObject]@{
|
|
Make = 'Microsoft'
|
|
Model = $modelName
|
|
Link = $modelLink
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
WriteLog "Parsing complete. Found $($models.Count) models."
|
|
return $models
|
|
}
|
|
catch {
|
|
WriteLog "Error getting Microsoft models: $($_.Exception.Message)"
|
|
throw "Failed to retrieve Microsoft Surface models."
|
|
}
|
|
}
|
|
|
|
# Function to get the list of Lenovo models using the PSREF API
|
|
function Get-LenovoDriversModelList {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ModelSearchTerm, # User input for model/machine type
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable]$Headers,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserAgent
|
|
)
|
|
|
|
WriteLog "Querying Lenovo PSREF API for model/machine type: $ModelSearchTerm"
|
|
$url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$([uri]::EscapeDataString($ModelSearchTerm))"
|
|
$models = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
|
|
try {
|
|
$OriginalVerbosePreference = $VerbosePreference
|
|
$VerbosePreference = 'SilentlyContinue'
|
|
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent -ErrorAction Stop
|
|
$VerbosePreference = $OriginalVerbosePreference
|
|
WriteLog "PSREF API query complete."
|
|
|
|
$jsonResponse = $response.Content | ConvertFrom-Json
|
|
|
|
if ($null -ne $jsonResponse.data -and $jsonResponse.data.Count -gt 0) {
|
|
foreach ($item in $jsonResponse.data) {
|
|
$productName = $item.ProductName
|
|
$machineTypes = $item.MachineType -split " / " # Split if multiple machine types are listed
|
|
|
|
foreach ($machineTypeRaw in $machineTypes) {
|
|
$machineType = $machineTypeRaw.Trim()
|
|
# Only add if machine type is not empty
|
|
if (-not [string]::IsNullOrWhiteSpace($machineType)) {
|
|
# Create the combined display string
|
|
$displayModel = "$productName ($machineType)"
|
|
# Add each combination as a separate entry
|
|
$models.Add([PSCustomObject]@{
|
|
Make = 'Lenovo'
|
|
Model = $displayModel # Combined string for display
|
|
ProductName = $productName # Original product name stored separately if needed
|
|
MachineType = $machineType # Machine type needed for catalog URL
|
|
})
|
|
}
|
|
else {
|
|
WriteLog "Skipping entry for product '$productName' due to missing machine type."
|
|
}
|
|
}
|
|
}
|
|
WriteLog "Found $($models.Count) potential model/machine type combinations for '$ModelSearchTerm'."
|
|
}
|
|
else {
|
|
WriteLog "No models found matching '$ModelSearchTerm' in Lenovo PSREF."
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Error querying Lenovo PSREF API: $($_.Exception.Message)"
|
|
# Return empty list on error
|
|
}
|
|
|
|
# Return the list (sorting might be done in the UI layer if needed)
|
|
return $models
|
|
}
|
|
|
|
# Function to download and extract drivers for a specific Lenovo model (Background Task)
|
|
function Save-LenovoDriversTask {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[PSCustomObject]$DriverItemData, # Contains Model (ProductName) and MachineType
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DriversFolder,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$WindowsRelease,
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable]$Headers,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserAgent,
|
|
[Parameter()] # Made optional
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
|
[Parameter()]
|
|
[bool]$CompressToWim = $false # New parameter for compression
|
|
)
|
|
|
|
# The Model property from the UI already contains the combined "ProductName (MachineType)" string
|
|
$identifier = $DriverItemData.Model
|
|
# We still need the machine type for the catalog URL
|
|
$machineType = $DriverItemData.MachineType
|
|
$make = "Lenovo"
|
|
# $identifier = "$($modelName) ($($machineType))" # No longer needed, use Model directly
|
|
$status = "Starting..."
|
|
$success = $false
|
|
|
|
# Define paths
|
|
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
|
# Use the identifier (which contains the model name and machine type) and sanitize it for the path
|
|
$modelPath = Join-Path -Path $makeDriversPath -ChildPath ($identifier -replace '[\\/:"*?<>|]', '_')
|
|
$tempDownloadPath = Join-Path -Path $makeDriversPath -ChildPath "_TEMP_$($machineType)_$($PID)" # Temp folder for catalog/package XMLs
|
|
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking..." }
|
|
|
|
try {
|
|
# 1. Check if drivers already exist for this model (final destination)
|
|
if (Test-Path -Path $modelPath -PathType Container) {
|
|
$folderSize = (Get-ChildItem -Path $modelPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($folderSize -gt 1MB) {
|
|
$status = "Already downloaded"
|
|
WriteLog "Drivers for '$identifier' already exist in '$modelPath'."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $true }
|
|
}
|
|
else {
|
|
WriteLog "Driver folder '$modelPath' for '$identifier' exists but is empty/small. Re-downloading."
|
|
}
|
|
}
|
|
|
|
# Ensure base directories exist
|
|
if (-not (Test-Path -Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
|
|
if (-not (Test-Path -Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
|
|
if (-not (Test-Path -Path $tempDownloadPath)) { New-Item -Path $tempDownloadPath -ItemType Directory -Force | Out-Null }
|
|
|
|
# 2. Construct and Download Catalog URL
|
|
$modelRelease = $machineType + "_Win" + $WindowsRelease
|
|
$catalogUrl = "https://download.lenovo.com/catalog/$modelRelease.xml"
|
|
$lenovoCatalogXML = Join-Path -Path $tempDownloadPath -ChildPath "$modelRelease.xml"
|
|
|
|
$status = "Downloading Catalog..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
WriteLog "Downloading Lenovo Driver catalog for '$identifier' from $catalogUrl"
|
|
|
|
# Check URL accessibility first
|
|
try {
|
|
$request = [System.Net.WebRequest]::Create($catalogUrl); $request.Method = 'HEAD'; $response = $request.GetResponse(); $response.Close()
|
|
}
|
|
catch { throw "Lenovo Driver catalog URL is not accessible: $catalogUrl. Error: $($_.Exception.Message)" }
|
|
|
|
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $lenovoCatalogXML
|
|
WriteLog "Catalog download Complete: $lenovoCatalogXML"
|
|
|
|
# 3. Parse Catalog and Process Packages
|
|
$status = "Parsing Catalog..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
[xml]$xmlContent = Get-Content -Path $lenovoCatalogXML -Encoding UTF8
|
|
|
|
$packages = @($xmlContent.packages.package) # Ensure it's an array
|
|
$totalPackages = $packages.Count
|
|
$processedPackages = 0
|
|
WriteLog "Found $totalPackages packages in catalog for '$identifier'."
|
|
|
|
foreach ($package in $packages) {
|
|
$processedPackages++
|
|
$category = $package.category
|
|
$packageUrl = $package.location # URL to the package's *XML* file
|
|
|
|
# Skip BIOS/Firmware based on category
|
|
if ($category -like 'BIOS*' -or $category -like 'Firmware*') {
|
|
WriteLog "($processedPackages/$totalPackages) Skipping BIOS/Firmware package: $category"
|
|
continue
|
|
}
|
|
|
|
# Sanitize category for path
|
|
$categoryClean = $category -replace '[\\/:"*?<>|]', '_'
|
|
if ($categoryClean -eq 'Motherboard Devices Backplanes core chipset onboard video PCIe switches') {
|
|
$categoryClean = 'Motherboard Devices' # Shorten long category name
|
|
}
|
|
|
|
$packageName = [System.IO.Path]::GetFileName($packageUrl)
|
|
$packageXMLPath = Join-Path -Path $tempDownloadPath -ChildPath $packageName
|
|
$baseURL = $packageUrl -replace [regex]::Escape($packageName), "" # Base URL for the driver file
|
|
|
|
$status = "($processedPackages/$totalPackages) Getting package info..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
|
|
# Download the package XML
|
|
WriteLog "($processedPackages/$totalPackages) Downloading package XML: $packageUrl"
|
|
try {
|
|
Start-BitsTransferWithRetry -Source $packageUrl -Destination $packageXMLPath
|
|
}
|
|
catch {
|
|
WriteLog "($processedPackages/$totalPackages) Failed to download package XML '$packageUrl'. Skipping. Error: $($_.Exception.Message)"
|
|
continue # Skip this package
|
|
}
|
|
|
|
# Load and parse the package XML
|
|
[xml]$packageXmlContent = Get-Content -Path $packageXMLPath -Encoding UTF8
|
|
$packageType = $packageXmlContent.Package.PackageType.type
|
|
$packageTitleRaw = $packageXmlContent.Package.title.InnerText
|
|
|
|
# Filter out non-driver packages (Type 2 = Driver)
|
|
if ($packageType -ne 2) {
|
|
WriteLog "($processedPackages/$totalPackages) Skipping package '$packageTitleRaw' (Type: $packageType) - Not a driver."
|
|
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
|
|
continue
|
|
}
|
|
|
|
# Sanitize title for folder name
|
|
$packageTitle = $packageTitleRaw -replace '[\\/:"*?<>|]', '_' -replace ' - .*', ''
|
|
|
|
# Extract driver file name and extract command
|
|
$driverFileName = $null
|
|
$extractCommand = $null
|
|
try {
|
|
$driverFileName = $packageXmlContent.Package.Files.Installer.File.Name
|
|
$extractCommand = $packageXmlContent.Package.ExtractCommand
|
|
}
|
|
catch {
|
|
WriteLog "($processedPackages/$totalPackages) Error parsing package XML '$packageXMLPath' for file name/command. Skipping. Error: $($_.Exception.Message)"
|
|
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
|
|
continue
|
|
}
|
|
|
|
|
|
# Skip if essential info is missing
|
|
if ([string]::IsNullOrWhiteSpace($driverFileName) -or [string]::IsNullOrWhiteSpace($extractCommand)) {
|
|
WriteLog "($processedPackages/$totalPackages) Skipping package '$packageTitleRaw' - Missing driver file name or extract command in XML."
|
|
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
|
|
continue
|
|
}
|
|
|
|
# Construct paths
|
|
$driverUrl = $baseURL + $driverFileName
|
|
$categoryPath = Join-Path -Path $modelPath -ChildPath $categoryClean
|
|
$downloadFolder = Join-Path -Path $categoryPath -ChildPath $packageTitle # Final destination subfolder
|
|
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driverFileName
|
|
$extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverFileName -replace '\.exe$', '') # Extract to subfolder named after exe
|
|
# Check if already extracted
|
|
if (Test-Path -Path $extractFolder -PathType Container) {
|
|
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($extractSize -gt 1KB) {
|
|
WriteLog "($processedPackages/$totalPackages) Driver '$packageTitleRaw' already extracted to '$extractFolder'. Skipping."
|
|
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
|
|
continue
|
|
}
|
|
}
|
|
|
|
# Ensure download folder exists
|
|
if (-not (Test-Path -Path $downloadFolder)) {
|
|
New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
# Download the driver .exe
|
|
$status = "($processedPackages/$totalPackages) Downloading $packageTitle..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
WriteLog "($processedPackages/$totalPackages) Downloading driver: $driverUrl to $driverFilePath"
|
|
try {
|
|
Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath
|
|
WriteLog "($processedPackages/$totalPackages) Driver downloaded: $driverFileName"
|
|
}
|
|
catch {
|
|
WriteLog "($processedPackages/$totalPackages) Failed to download driver '$driverUrl'. Skipping. Error: $($_.Exception.Message)"
|
|
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
|
|
continue # Skip this driver
|
|
}
|
|
|
|
# --- Extraction Logic ---
|
|
$status = "($processedPackages/$totalPackages) Extracting $packageTitle..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
|
|
# Always use a temporary extraction path to avoid long path issues
|
|
$originalExtractFolder = $extractFolder # Store the originally intended final path
|
|
$extractionSucceeded = $false
|
|
$tempExtractBase = $null # Initialize
|
|
|
|
# Create randomized number for use with temp folder name
|
|
$randomNumber = Get-Random -Minimum 1000 -Maximum 9999
|
|
$tempExtractBase = Join-Path $env:TEMP "LenovoDriverExtract_$randomNumber"
|
|
$extractFolder = Join-Path $tempExtractBase ($driverFileName -replace '\.exe$', '') # Actual temp extraction folder
|
|
WriteLog "($processedPackages/$totalPackages) Using temporary extraction path: $extractFolder"
|
|
|
|
# Ensure the base temp directory exists
|
|
if (-not (Test-Path -Path $tempExtractBase)) {
|
|
New-Item -Path $tempExtractBase -ItemType Directory -Force | Out-Null
|
|
}
|
|
# Ensure the target temporary extraction folder exists
|
|
if (-not (Test-Path -Path $extractFolder)) {
|
|
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
# Modify the extract command to point to the temporary folder
|
|
$modifiedExtractCommand = $extractCommand -replace '%PACKAGEPATH%', "`"$extractFolder`""
|
|
WriteLog "($processedPackages/$totalPackages) Extracting driver: $driverFilePath using command: $modifiedExtractCommand"
|
|
|
|
try {
|
|
Invoke-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand -Wait $true | Out-Null
|
|
WriteLog "($processedPackages/$totalPackages) Driver extracted to temporary path: $extractFolder"
|
|
$extractionSucceeded = $true
|
|
}
|
|
catch {
|
|
WriteLog "($processedPackages/$totalPackages) Failed to extract driver '$driverFilePath' to temporary path. Skipping. Error: $($_.Exception.Message)"
|
|
# Don't delete the downloaded exe yet if extraction fails
|
|
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
|
|
# Clean up temp folder if extraction failed
|
|
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
|
|
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
continue # Skip further processing for this driver
|
|
}
|
|
|
|
# --- Post-Extraction Handling (Move from Temp to Final Destination) ---
|
|
if ($extractionSucceeded) {
|
|
WriteLog "($processedPackages/$totalPackages) Performing post-extraction move from temp to final destination..."
|
|
try {
|
|
# Ensure the *original* final destination folder exists and is empty
|
|
if (Test-Path -Path $originalExtractFolder) {
|
|
WriteLog "($processedPackages/$totalPackages) Clearing existing final destination folder: $originalExtractFolder"
|
|
Get-ChildItem -Path $originalExtractFolder -Recurse | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
else {
|
|
WriteLog "($processedPackages/$totalPackages) Creating final destination folder: $originalExtractFolder"
|
|
New-Item -Path $originalExtractFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
# Get all items (files and folders) directly inside the temp extraction folder
|
|
$extractedItems = Get-ChildItem -Path $extractFolder -ErrorAction Stop
|
|
|
|
foreach ($item in $extractedItems) {
|
|
$itemName = $item.Name
|
|
$finalDestinationPath = $null
|
|
|
|
# Check if it's a directory containing 'Liteon'
|
|
if ($item.PSIsContainer -and $itemName -like '*Liteon*') {
|
|
# Rename Liteon folders with a random number suffix
|
|
$randomNumber = Get-Random -Minimum 1000 -Maximum 9999
|
|
$finalFolderName = "Liteon_$randomNumber"
|
|
$finalDestinationPath = Join-Path -Path $originalExtractFolder -ChildPath $finalFolderName
|
|
WriteLog "($processedPackages/$totalPackages) Moving Liteon folder '$itemName' to '$finalDestinationPath'"
|
|
}
|
|
else {
|
|
# For other files/folders, move them directly
|
|
$finalDestinationPath = Join-Path -Path $originalExtractFolder -ChildPath $itemName
|
|
WriteLog "($processedPackages/$totalPackages) Moving item '$itemName' to '$finalDestinationPath'"
|
|
}
|
|
|
|
# Perform the move
|
|
try {
|
|
Move-Item -Path $item.FullName -Destination $finalDestinationPath -Force -ErrorAction Stop
|
|
}
|
|
catch {
|
|
WriteLog "($processedPackages/$totalPackages) Failed to move item '$($item.FullName)' to '$finalDestinationPath'. Error: $($_.Exception.Message)"
|
|
# Decide if this should stop the whole process or just skip this item
|
|
# For now, we'll log and continue, but mark overall success as false
|
|
$extractionSucceeded = $false
|
|
}
|
|
} # End foreach ($item in $extractedItems)
|
|
|
|
if ($extractionSucceeded) {
|
|
WriteLog "($processedPackages/$totalPackages) All driver contents moved successfully from temp to final destination."
|
|
}
|
|
else {
|
|
WriteLog "($processedPackages/$totalPackages) Some driver contents failed to move. Check logs."
|
|
}
|
|
|
|
}
|
|
catch {
|
|
WriteLog "($processedPackages/$totalPackages) Error during post-extraction move: $($_.Exception.Message). Files might remain in temp."
|
|
$extractionSucceeded = $false # Mark as failed for cleanup logic below
|
|
}
|
|
finally {
|
|
# Clean up the base temporary directory regardless of move success/failure
|
|
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
|
|
WriteLog "($processedPackages/$totalPackages) Cleaning up temporary extraction base: $tempExtractBase"
|
|
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Final Cleanup ---
|
|
# Delete the downloaded .exe only if extraction AND move were successful
|
|
if ($extractionSucceeded) {
|
|
WriteLog "($processedPackages/$totalPackages) Deleting driver installation file: $driverFilePath"
|
|
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
else {
|
|
WriteLog "($processedPackages/$totalPackages) Keeping driver installation file due to extraction/move failure: $driverFilePath"
|
|
}
|
|
# Always delete the package XML
|
|
WriteLog "($processedPackages/$totalPackages) Deleting package XML file: $packageXMLPath"
|
|
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
|
|
|
|
} # End foreach package
|
|
|
|
# --- Compress to WIM if requested (after all drivers processed) ---
|
|
if ($CompressToWim) {
|
|
$status = "Compressing..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
$wimFileName = "$($identifier).wim" # Use sanitized identifier for filename
|
|
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
|
|
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
|
try {
|
|
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $identifier -WimDescription $identifier -ErrorAction Stop
|
|
if ($compressResult) {
|
|
WriteLog "Compression successful for '$identifier'."
|
|
$status = "Completed & Compressed"
|
|
}
|
|
else {
|
|
WriteLog "Compression failed for '$identifier'. Check verbose/error output from Compress-DriverFolderToWim."
|
|
$status = "Completed (Compression Failed)"
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Error during compression for '$identifier': $($_.Exception.Message)"
|
|
$status = "Completed (Compression Error)"
|
|
}
|
|
}
|
|
else {
|
|
$status = "Completed" # Final status if not compressing
|
|
}
|
|
# --- End Compression ---
|
|
|
|
$success = $true # Mark success as download/extract was okay
|
|
|
|
}
|
|
catch {
|
|
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
|
|
WriteLog "Error saving Lenovo drivers for '$identifier': $($_.Exception.ToString())" # Log full exception string
|
|
$success = $false
|
|
# Enqueue the error status before returning
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
# Ensure return object is created even on error
|
|
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success }
|
|
}
|
|
finally {
|
|
# Clean up the main catalog XML and temp folder
|
|
WriteLog "Cleaning up temporary download folder: $tempDownloadPath"
|
|
Remove-Item -Path $tempDownloadPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# Enqueue the final status (success or error) before returning
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
|
|
|
|
# Return the final status
|
|
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success }
|
|
}
|
|
|
|
# Function to get the list of HP models from the PlatformList.xml
|
|
# Depends on private functions: Start-BitsTransferWithRetry, Invoke-Process
|
|
function Get-HPDriversModelList {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DriversFolder,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Make # Expected to be 'HP'
|
|
)
|
|
|
|
WriteLog "Getting HP driver model list..."
|
|
$hpDriversFolder = Join-Path -Path $DriversFolder -ChildPath $Make
|
|
$platformListUrl = 'https://hpia.hpcloud.hp.com/ref/platformList.cab'
|
|
$platformListCab = Join-Path -Path $hpDriversFolder -ChildPath "platformList.cab"
|
|
$platformListXml = Join-Path -Path $hpDriversFolder -ChildPath "PlatformList.xml"
|
|
$modelList = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
|
|
try {
|
|
# Ensure HP drivers folder exists
|
|
if (-not (Test-Path -Path $hpDriversFolder)) {
|
|
WriteLog "Creating HP Drivers folder: $hpDriversFolder"
|
|
New-Item -Path $hpDriversFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
# Download PlatformList.cab if it doesn't exist or is outdated (e.g., older than 7 days)
|
|
if (-not (Test-Path -Path $platformListCab) -or ((Get-Date) - (Get-Item $platformListCab).LastWriteTime).TotalDays -gt 7) {
|
|
WriteLog "Downloading $platformListUrl to $platformListCab"
|
|
# Use the private helper function for download with retry
|
|
Start-BitsTransferWithRetry -Source $platformListUrl -Destination $platformListCab -ErrorAction Stop
|
|
WriteLog "PlatformList.cab download complete."
|
|
# Force extraction if downloaded
|
|
if (Test-Path -Path $platformListXml) {
|
|
Remove-Item -Path $platformListXml -Force
|
|
}
|
|
}
|
|
else {
|
|
WriteLog "Using existing PlatformList.cab found at $platformListCab"
|
|
}
|
|
|
|
# Extract PlatformList.xml if it doesn't exist
|
|
if (-not (Test-Path -Path $platformListXml)) {
|
|
WriteLog "Expanding $platformListCab to $platformListXml"
|
|
# Use the private helper function for process invocation
|
|
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$platformListCab`"", "`"$platformListXml`"") -ErrorAction Stop | Out-Null
|
|
WriteLog "PlatformList.xml extraction complete."
|
|
}
|
|
else {
|
|
WriteLog "Using existing PlatformList.xml found at $platformListXml"
|
|
}
|
|
|
|
# Parse the PlatformList.xml using XmlReader for efficiency
|
|
WriteLog "Parsing PlatformList.xml to extract HP models..."
|
|
$settings = New-Object System.Xml.XmlReaderSettings
|
|
$settings.Async = $false # Ensure synchronous reading
|
|
|
|
$reader = [System.Xml.XmlReader]::Create($platformListXml, $settings)
|
|
$uniqueModels = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
|
|
while ($reader.Read()) {
|
|
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'Platform') {
|
|
# Read the inner content of the Platform node
|
|
$platformReader = $reader.ReadSubtree()
|
|
while ($platformReader.Read()) {
|
|
if ($platformReader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $platformReader.Name -eq 'ProductName') {
|
|
$modelName = $platformReader.ReadElementContentAsString()
|
|
if (-not [string]::IsNullOrWhiteSpace($modelName) -and $uniqueModels.Add($modelName)) {
|
|
# Add to list only if it's a new unique model
|
|
$modelList.Add([PSCustomObject]@{
|
|
Make = $Make
|
|
Model = $modelName
|
|
# Add other properties like SystemID if needed later, but keep it simple for now
|
|
})
|
|
}
|
|
}
|
|
}
|
|
$platformReader.Close()
|
|
}
|
|
}
|
|
$reader.Close()
|
|
|
|
WriteLog "Successfully parsed $($modelList.Count) unique HP models from PlatformList.xml."
|
|
|
|
}
|
|
catch {
|
|
WriteLog "Error getting HP driver model list: $($_.Exception.Message)"
|
|
# Optionally re-throw or return an empty list/error object
|
|
# For now, just return the potentially partially populated list or empty list
|
|
}
|
|
|
|
# Sort the list alphabetically by Model name before returning
|
|
return $modelList | Sort-Object -Property Model
|
|
}
|
|
# Function to get USB Drives (Moved from BuildFFUVM_UI.ps1)
|
|
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" }
|
|
@{
|
|
IsSelected = $false
|
|
Model = $_.Model.Trim()
|
|
SerialNumber = $serialNumber
|
|
Size = $size
|
|
DriveIndex = $_.Index
|
|
}
|
|
}
|
|
}
|
|
|
|
# --------------------------------------------------------------------------
|
|
# SECTION: Modern Folder Picker (Moved from BuildFFUVM_UI.ps1)
|
|
# --------------------------------------------------------------------------
|
|
|
|
# 1) Define a C# class that uses the correct GUIDs for IFileDialog, IFileOpenDialog, and FileOpenDialog,
|
|
# while omitting conflicting "GetResults/GetSelectedItems" from IFileDialog.
|
|
Add-Type -TypeDefinition @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public static class ModernFolderBrowser
|
|
{
|
|
// Flags for IFileDialog
|
|
[Flags]
|
|
private enum FileDialogOptions : uint
|
|
{
|
|
OverwritePrompt = 0x00000002,
|
|
StrictFileTypes = 0x00000004,
|
|
NoChangeDir = 0x00000008,
|
|
PickFolders = 0x00000020,
|
|
ForceFileSystem = 0x00000040,
|
|
AllNonStorageItems = 0x00000080,
|
|
NoValidate = 0x00000100,
|
|
AllowMultiSelect = 0x00000200,
|
|
PathMustExist = 0x00000800,
|
|
FileMustExist = 0x00001000,
|
|
CreatePrompt = 0x00002000,
|
|
ShareAware = 0x00004000,
|
|
NoReadOnlyReturn = 0x00008000,
|
|
NoTestFileCreate = 0x00010000,
|
|
DontAddToRecent = 0x02000000,
|
|
ForceShowHidden = 0x10000000
|
|
}
|
|
|
|
// IFileDialog (GUID from Windows SDK)
|
|
// - Omitting GetResults / GetSelectedItems to avoid overshadow.
|
|
[ComImport]
|
|
[Guid("42F85136-DB7E-439C-85F1-E4075D135FC8")]
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IFileDialog
|
|
{
|
|
[PreserveSig]
|
|
int Show(IntPtr parent);
|
|
|
|
void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
|
void SetFileTypeIndex(uint iFileType);
|
|
void GetFileTypeIndex(out uint piFileType);
|
|
void Advise(IntPtr pfde, out uint pdwCookie);
|
|
void Unadvise(uint dwCookie);
|
|
void SetOptions(FileDialogOptions fos);
|
|
void GetOptions(out FileDialogOptions pfos);
|
|
void SetDefaultFolder(IShellItem psi);
|
|
void SetFolder(IShellItem psi);
|
|
void GetFolder(out IShellItem ppsi);
|
|
void GetCurrentSelection(out IShellItem ppsi);
|
|
void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
|
void GetFileName(out IntPtr pszName);
|
|
void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
|
|
void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
|
|
void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
|
|
void GetResult(out IShellItem ppsi);
|
|
void AddPlace(IShellItem psi, int fdap);
|
|
void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
|
|
void Close(int hr);
|
|
void SetClientGuid(ref Guid guid);
|
|
void ClearClientData();
|
|
void SetFilter(IntPtr pFilter);
|
|
|
|
// NOTE: We intentionally do NOT define GetResults and GetSelectedItems here,
|
|
// because they cause overshadow warnings in IFileOpenDialog.
|
|
}
|
|
|
|
// IFileOpenDialog extends IFileDialog by adding 2 new methods with the same name,
|
|
// which otherwise cause overshadow warnings. We'll define them only here.
|
|
[ComImport]
|
|
[Guid("D57C7288-D4AD-4768-BE02-9D969532D960")]
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IFileOpenDialog : IFileDialog
|
|
{
|
|
// These two come after the parent's vtable:
|
|
void GetResults(out IntPtr ppenum);
|
|
void GetSelectedItems(out IntPtr ppsai);
|
|
}
|
|
|
|
// The coclass for creating an IFileOpenDialog
|
|
[ComImport]
|
|
[Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
|
|
private class FileOpenDialog
|
|
{
|
|
}
|
|
|
|
// IShellItem
|
|
[ComImport]
|
|
[Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IShellItem
|
|
{
|
|
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
|
void GetParent(out IShellItem ppsi);
|
|
void GetDisplayName(uint sigdnName, out IntPtr ppszName);
|
|
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
void Compare(IShellItem psi, uint hint, out int piOrder);
|
|
}
|
|
|
|
private const uint SIGDN_FILESYSPATH = 0x80058000;
|
|
|
|
public static string ShowDialog(string title, IntPtr parentHandle)
|
|
{
|
|
// Create COM dialog instance
|
|
IFileOpenDialog dialog = (IFileOpenDialog)(new FileOpenDialog());
|
|
|
|
// Get current options
|
|
FileDialogOptions opts;
|
|
dialog.GetOptions(out opts);
|
|
|
|
// Add flags for picking folders
|
|
opts |= FileDialogOptions.PickFolders | FileDialogOptions.PathMustExist | FileDialogOptions.ForceFileSystem;
|
|
dialog.SetOptions(opts);
|
|
|
|
// Set title
|
|
if (!string.IsNullOrEmpty(title))
|
|
{
|
|
dialog.SetTitle(title);
|
|
}
|
|
|
|
// Show the dialog
|
|
int hr = dialog.Show(parentHandle);
|
|
// 0 = S_OK. 1 or 0x800704C7 often means user canceled. Return null if so.
|
|
if (hr != 0)
|
|
{
|
|
if ((uint)hr == 0x800704C7 || hr == 1)
|
|
{
|
|
return null; // Canceled
|
|
}
|
|
else
|
|
{
|
|
Marshal.ThrowExceptionForHR(hr);
|
|
}
|
|
}
|
|
|
|
// Retrieve the selection (IShellItem)
|
|
IShellItem shellItem;
|
|
dialog.GetResult(out shellItem);
|
|
if (shellItem == null) return null;
|
|
|
|
// Convert to file system path
|
|
IntPtr pszPath = IntPtr.Zero;
|
|
shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszPath);
|
|
if (pszPath == IntPtr.Zero) return null;
|
|
|
|
string folderPath = Marshal.PtrToStringAuto(pszPath);
|
|
Marshal.FreeCoTaskMem(pszPath);
|
|
|
|
return folderPath;
|
|
}
|
|
}
|
|
"@ -Language CSharp
|
|
|
|
# 2) Define a PowerShell function that invokes our C# wrapper
|
|
function Show-ModernFolderPicker {
|
|
param(
|
|
[string]$Title = "Select a folder"
|
|
)
|
|
# For a simple test, pass IntPtr.Zero as the parent window handle
|
|
return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero)
|
|
}
|
|
|
|
# --------------------------------------------------------------------------
|
|
# SECTION: Winget Management Functions
|
|
# --------------------------------------------------------------------------
|
|
function Search-WingetPackagesPublic {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Query
|
|
)
|
|
|
|
WriteLog "Searching Winget packages with query: '$Query'"
|
|
try {
|
|
# Call the shared Find-WinGetPackage function
|
|
$results = Find-WinGetPackage -Query $Query -ErrorAction Stop
|
|
WriteLog "Found $($results.Count) packages matching query '$Query'."
|
|
return $results
|
|
}
|
|
catch {
|
|
WriteLog "Error during Winget search: $($_.Exception.Message)"
|
|
# Return an empty array or throw, depending on desired UI behavior
|
|
return @()
|
|
}
|
|
}
|
|
|
|
function Test-WingetCLI {
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
$minVersion = [version]"1.8.1911"
|
|
|
|
# Check Winget CLI
|
|
$wingetCmd = Get-Command -Name winget -ErrorAction SilentlyContinue
|
|
if (-not $wingetCmd) {
|
|
return @{
|
|
Version = "Not installed"
|
|
Status = "Not installed - Install from Microsoft Store"
|
|
}
|
|
}
|
|
|
|
# Get and check version
|
|
$wingetVersion = & winget.exe --version
|
|
if ($wingetVersion -match 'v?(\d+\.\d+.\d+)') {
|
|
$version = [version]$matches[1]
|
|
if ($version -lt $minVersion) {
|
|
return @{
|
|
Version = $version.ToString()
|
|
Status = "Update required - Install from Microsoft Store"
|
|
}
|
|
}
|
|
return @{
|
|
Version = $version.ToString()
|
|
Status = $version.ToString()
|
|
}
|
|
}
|
|
|
|
return @{
|
|
Version = "Unknown"
|
|
Status = "Version check failed"
|
|
}
|
|
}
|
|
|
|
|
|
function Install-WingetComponents {
|
|
[CmdletBinding()]
|
|
param(
|
|
# Add parameter to accept a script block for UI updates
|
|
[Parameter(Mandatory)]
|
|
[scriptblock]$UiUpdateCallback
|
|
)
|
|
|
|
$minVersion = [version]"1.8.1911"
|
|
$module = $null
|
|
|
|
try {
|
|
# Check and update PowerShell Module
|
|
$module = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction SilentlyContinue
|
|
if (-not $module -or $module.Version -lt $minVersion) {
|
|
WriteLog "Winget module needs install/update. Attempting..."
|
|
# Invoke the callback provided by the UI script to update status
|
|
# Note: We don't have the CLI version readily available here, pass a placeholder or adjust if needed.
|
|
& $UiUpdateCallback "Checking..." "Installing..."
|
|
|
|
# Store and modify PSGallery trust setting temporarily if needed
|
|
$PSGalleryTrust = (Get-PSRepository -Name 'PSGallery').InstallationPolicy
|
|
if ($PSGalleryTrust -eq 'Untrusted') {
|
|
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
|
|
}
|
|
|
|
# Install/Update the module
|
|
Install-Module -Name Microsoft.WinGet.Client -Force -Repository 'PSGallery'
|
|
|
|
# Restore original PSGallery trust setting
|
|
if ($PSGalleryTrust -eq 'Untrusted') {
|
|
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Untrusted
|
|
}
|
|
|
|
$module = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction Stop
|
|
}
|
|
|
|
return $module
|
|
}
|
|
catch {
|
|
Write-Error "Failed to install/update Winget PowerShell module: $_"
|
|
throw
|
|
}
|
|
}
|
|
|
|
# Winget Module Check Function (UI Version)
|
|
# Performs checks, triggers install if needed, and reports status back to the UI.
|
|
function Confirm-WingetInstallationUI {
|
|
[CmdletBinding()]
|
|
param(
|
|
# Callback for intermediate UI updates (e.g., "Installing...")
|
|
[Parameter(Mandatory)]
|
|
[scriptblock]$UiUpdateCallback
|
|
)
|
|
|
|
$minVersion = [version]"1.8.1911"
|
|
$result = [PSCustomObject]@{
|
|
Success = $false
|
|
Message = ""
|
|
CliVersion = "Unknown"
|
|
ModuleVersion = "Unknown"
|
|
NeedsUpdate = $false
|
|
UpdateAttempted = $false
|
|
}
|
|
|
|
try {
|
|
# Initial Check
|
|
WriteLog "Confirm-WingetInstallationUI: Starting checks..."
|
|
$cliStatus = Test-WingetCLI
|
|
$module = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction SilentlyContinue
|
|
|
|
$result.CliVersion = $cliStatus.Version
|
|
$result.ModuleVersion = if ($null -ne $module) { $module.Version.ToString() } else { "Not installed" }
|
|
|
|
# Use callback for initial status display
|
|
& $UiUpdateCallback $result.CliVersion $result.ModuleVersion
|
|
|
|
# Determine if install/update is needed
|
|
$needsCliUpdate = $cliStatus.Status -notmatch '^\d+\.\d+\.\d+$' -or ([version]$cliStatus.Version -lt $minVersion)
|
|
$needsModuleUpdate = ($null -eq $module) -or ([version]$module.Version -lt $minVersion)
|
|
$result.NeedsUpdate = $needsCliUpdate -or $needsModuleUpdate
|
|
|
|
if ($result.NeedsUpdate) {
|
|
WriteLog "Confirm-WingetInstallationUI: Update needed. CLI Needs Update: $needsCliUpdate, Module Needs Update: $needsModuleUpdate"
|
|
$result.UpdateAttempted = $true
|
|
|
|
# Use callback to indicate installation attempt
|
|
& $UiUpdateCallback $result.CliVersion "Installing/Updating..."
|
|
|
|
# Call Install-WingetComponents (which also uses the callback internally)
|
|
# Note: Install-WingetComponents currently only installs the module.
|
|
# CLI installation/update might need separate handling or integration here if desired.
|
|
# For now, we focus on the module install triggered by this check.
|
|
$installedModule = Install-WingetComponents -UiUpdateCallback $UiUpdateCallback
|
|
|
|
# Re-check status after attempt
|
|
WriteLog "Confirm-WingetInstallationUI: Re-checking status after update attempt..."
|
|
$cliStatus = Test-WingetCLI
|
|
$result.CliVersion = $cliStatus.Version
|
|
$result.ModuleVersion = if ($null -ne $installedModule) { $installedModule.Version } else { "Install Failed" }
|
|
# Use callback for final status display after update attempt
|
|
& $UiUpdateCallback $result.CliVersion $result.ModuleVersion
|
|
|
|
# Check if update was successful
|
|
$cliOk = $cliStatus.Status -match '^\d+\.\d+\.\d+$' -and ([version]$cliStatus.Version -ge $minVersion)
|
|
$moduleOk = ($null -ne $installedModule) -and ([version]$installedModule.Version -ge $minVersion)
|
|
$result.Success = $cliOk -and $moduleOk
|
|
$result.Message = if ($result.Success) { "Winget components installed/updated successfully." } else { "Winget component installation/update failed or is incomplete." }
|
|
WriteLog "Confirm-WingetInstallationUI: Update attempt finished. Success: $($result.Success). Message: $($result.Message)"
|
|
}
|
|
else {
|
|
# Already up-to-date
|
|
$result.Success = $true
|
|
$result.Message = "Winget components are up-to-date."
|
|
WriteLog "Confirm-WingetInstallationUI: Components already up-to-date."
|
|
}
|
|
}
|
|
catch {
|
|
$result.Success = $false
|
|
$result.Message = "Error during Winget check/install: $($_.Exception.Message)"
|
|
WriteLog "Confirm-WingetInstallationUI: Error - $($result.Message)"
|
|
# Use callback to show error state
|
|
& $UiUpdateCallback $result.CliVersion "Error"
|
|
}
|
|
|
|
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]$WindowsArch,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$OrchestrationPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue # Add queue parameter
|
|
)
|
|
|
|
$appName = $ApplicationItemData.Name
|
|
$appId = $ApplicationItemData.Id
|
|
$source = $ApplicationItemData.Source
|
|
$status = "Checking..." # Initial local status
|
|
$resultCode = -1 # Default to error/unknown
|
|
|
|
# Initial status update
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
|
|
|
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
|
|
# WriteLog "Apps Path: $($AppsPath)"
|
|
# WriteLog "AppList JSON Path: $($AppListJsonPath)"
|
|
# WriteLog "Windows Architecture: $($WindowsArch)"
|
|
# WriteLog "Orchestration Path: $($OrchestrationPath)"
|
|
|
|
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 $appName
|
|
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 = "Error: 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 = "Error: 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 previous Winget download
|
|
if (-not $appFound) {
|
|
# Set environment variable for Get-Application checks (if needed by sub-functions)
|
|
# Set environment variables needed by Get-Application if called within this scope
|
|
# Note: ForEach-Object -Parallel handles variable scoping differently than Runspaces.
|
|
# Ensure Get-Application correctly accesses these if needed, potentially via $using: scope
|
|
# or by passing them as parameters if Get-Application # 2. Check previous Winget download and WinGetWin32Apps.json for duplicate entries
|
|
if (-not $appFound) {
|
|
$wingetWin32jsonFile = Join-Path -Path $OrchestrationPath -ChildPath "WinGetWin32Apps.json"
|
|
if (Test-Path -Path $wingetWin32jsonFile) {
|
|
try {
|
|
$wingetAppsJson = Get-Content -Path $wingetWin32jsonFile -Raw | ConvertFrom-Json
|
|
# Check if app already exists in WinGetWin32Apps.json
|
|
$existingWin32Entry = $wingetAppsJson | Where-Object { $_.Name -eq $appName }
|
|
if ($existingWin32Entry) {
|
|
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName
|
|
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 already in $wingetWin32jsonFile and found in $appFolder"
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
|
WriteLog "Found '$appName' in WinGetWin32Apps.json and content exists in '$appFolder'. Skipping download to prevent duplicate entry."
|
|
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
|
}
|
|
}
|
|
else {
|
|
# App entry exists in WinGetWin32Apps.json but folder is missing
|
|
$appFound = $true
|
|
$status = "Error: App in '$wingetWin32jsonFile' but content folder '$appFolder' not found. Remove entry from WinGetWin32Apps.json or restore content."
|
|
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 '$wingetWin32jsonFile'. Error: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
}
|
|
# For now, assuming Get-Application uses $global variables set in the main script or $using: scope.
|
|
# $global:AppsPath = $AppsPath # Potentially redundant if set globally before parallel call
|
|
# $global:WindowsArch = $WindowsArch # Potentially redundant
|
|
# $global:orchestrationPath = $OrchestrationPath # Potentially redundant
|
|
|
|
$wingetWin32jsonFile = Join-Path -Path $OrchestrationPath -ChildPath "WinGetWin32Apps.json"
|
|
if (Test-Path -Path $wingetWin32jsonFile) {
|
|
try {
|
|
$wingetAppsJson = Get-Content -Path $wingetWin32jsonFile -Raw | ConvertFrom-Json
|
|
$wingetApp = $wingetAppsJson | Where-Object { $_.Name -eq $appName }
|
|
if ($wingetApp) {
|
|
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName
|
|
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 $wingetWin32jsonFile and found in $appFolder"
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
|
WriteLog "Found '$appName' via WinGetWin32Apps.json and content exists in '$appFolder'."
|
|
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Warning: Could not read or parse '$wingetWin32jsonFile'. Error: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check MSStore folder
|
|
if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) {
|
|
$appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $appName
|
|
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 = $appName; 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 = "Error saving AppList.json"
|
|
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 = $WindowsArch # 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 (ensure it's available via dot-sourcing and uses $global:LogFile)
|
|
$resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -ErrorAction Stop
|
|
|
|
# Determine status based on result code
|
|
switch ($resultCode) {
|
|
0 { $status = "Downloaded successfully" }
|
|
1 { $status = "Error: No win32 app installers were found" }
|
|
2 { $status = "Silent install switch could not be found. Did not download." }
|
|
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 = "Error: $($_.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 = "Error: $($_.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 = "Error: 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 status 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
|
|
}
|
|
|
|
# Function to copy a single BYO application (Modified for ForEach-Object -Parallel)
|
|
function Start-CopyBYOApplicationTask {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[PSCustomObject]$ApplicationItemData, # Pass data, not the UI object
|
|
[Parameter(Mandatory)]
|
|
[string]$AppsPath, # Pass necessary path
|
|
[Parameter(Mandatory = $true)]
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue # Add queue parameter
|
|
# REMOVED: UI-related parameters
|
|
)
|
|
|
|
$priority = $ApplicationItemData.Priority
|
|
$appName = $ApplicationItemData.Name
|
|
$commandLine = $ApplicationItemData.CommandLine
|
|
$arguments = $ApplicationItemData.Arguments
|
|
$sourcePath = $ApplicationItemData.Source
|
|
$status = "Starting..." # Initial local status
|
|
$success = $false
|
|
|
|
# Initial status update
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
|
|
if ([string]::IsNullOrWhiteSpace($AppsPath)) {
|
|
$status = "Error: Apps Path not set"
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
WriteLog "Copy error for $($appName): Apps Path not set."
|
|
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($sourcePath)) {
|
|
$status = "No source specified"
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
# This isn't an error, just nothing to do. Consider it success.
|
|
$success = $true
|
|
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
|
|
}
|
|
|
|
if (-not (Test-Path -Path $sourcePath -PathType Container)) {
|
|
$status = "Error: Source path not found"
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
WriteLog "Copy error for $($appName): Source path '$sourcePath' not found."
|
|
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
|
|
}
|
|
|
|
$win32BasePath = Join-Path -Path $AppsPath -ChildPath "Win32"
|
|
$destinationPath = Join-Path -Path $win32BasePath -ChildPath $appName
|
|
|
|
try {
|
|
# Check destination
|
|
if (Test-Path -Path $destinationPath -PathType Container) {
|
|
$folderSize = (Get-ChildItem -Path $destinationPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($folderSize -gt 1MB) {
|
|
$status = "Already copied"
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
WriteLog "Skipping copy for $($appName): Destination '$destinationPath' exists and has content."
|
|
$success = $true
|
|
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
|
|
}
|
|
else {
|
|
WriteLog "Destination '$destinationPath' exists but is empty/small. Proceeding with copy."
|
|
}
|
|
}
|
|
|
|
# Ensure base directory exists
|
|
if (-not (Test-Path -Path $win32BasePath -PathType Container)) {
|
|
New-Item -Path $win32BasePath -ItemType Directory -Force | Out-Null
|
|
WriteLog "Created directory: $win32BasePath"
|
|
}
|
|
|
|
# Perform the copy
|
|
$status = "Copying..."
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
WriteLog "Copying '$sourcePath' to '$destinationPath'..."
|
|
Copy-Item -Path $sourcePath -Destination $destinationPath -Recurse -Force -ErrorAction Stop
|
|
$status = "Copied successfully"
|
|
$success = $true
|
|
WriteLog "Successfully copied '$appName' to '$destinationPath'."
|
|
|
|
# ------------------------------------------------------------------
|
|
# Update (or create) UserAppList.json with the copied application
|
|
# ------------------------------------------------------------------
|
|
try {
|
|
WriteLog "Updating UserAppList.json for '$appName'..."
|
|
$userAppListPath = Join-Path -Path $AppsPath -ChildPath 'UserAppList.json'
|
|
|
|
# Build the new entry
|
|
$newEntry = [pscustomobject]@{
|
|
Priority = $priority
|
|
Name = $appName
|
|
CommandLine = $commandLine
|
|
Arguments = $arguments
|
|
Source = $sourcePath
|
|
}
|
|
|
|
# Load existing list if present, ensuring it's always an array
|
|
if (Test-Path -Path $userAppListPath) {
|
|
try {
|
|
# Attempt to load and ensure it's an array
|
|
$appList = @(Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json -ErrorAction Stop)
|
|
}
|
|
catch {
|
|
WriteLog "Warning: Could not parse '$userAppListPath' or it's not a valid JSON array. Initializing as empty array. Error: $($_.Exception.Message)"
|
|
$appList = @() # Initialize as empty array on error
|
|
}
|
|
}
|
|
else {
|
|
$appList = @() # Initialize as empty array if file doesn't exist
|
|
}
|
|
|
|
# Ensure $appList is an array even if ConvertFrom-Json returned $null or a single object somehow
|
|
if ($null -eq $appList -or $appList -isnot [array]) {
|
|
# If it was a single object, wrap it in an array. Otherwise, start fresh.
|
|
$appList = if ($null -ne $appList) { @($appList) } else { @() }
|
|
}
|
|
|
|
# Skip adding if an entry with the same Name already exists
|
|
if (-not ($appList | Where-Object { $_.Name -eq $newEntry.Name })) {
|
|
# Now $appList is guaranteed to be an array, so += is safe
|
|
$appList += $newEntry
|
|
# Sort by Priority before saving
|
|
$sortedAppList = $appList | Sort-Object Priority
|
|
$sortedAppList | ConvertTo-Json -Depth 10 | Set-Content -Path $userAppListPath -Encoding UTF8
|
|
WriteLog "Added '$($newEntry.Name)' to '$userAppListPath'."
|
|
}
|
|
else {
|
|
WriteLog "'$appName' already exists in '$userAppListPath'."
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Failed to update UserAppList.json for '$appName': $($_.Exception.Message)"
|
|
}
|
|
|
|
}
|
|
catch {
|
|
$errorMessage = $_.Exception.Message
|
|
$status = "Error: $($errorMessage)"
|
|
WriteLog "Copy error for $($appName): $($errorMessage)"
|
|
$success = $false
|
|
# Enqueue error status
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
}
|
|
|
|
# Enqueue final success status if applicable
|
|
if ($success) {
|
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appName -Status $status
|
|
}
|
|
|
|
# Return the final status
|
|
return [PSCustomObject]@{ Name = $appName; Status = $status; Success = $success }
|
|
}
|
|
# Helper function to enqueue progress updates to the UI thread
|
|
function Invoke-ProgressUpdate {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue,
|
|
[Parameter(Mandatory)]
|
|
[string]$Identifier,
|
|
[Parameter(Mandatory)]
|
|
[string]$Status
|
|
)
|
|
$ProgressQueue.Enqueue(@{ Identifier = $Identifier; Status = $Status })
|
|
}
|
|
|
|
# Function to download and extract drivers for a specific Microsoft model (Modified for ForEach-Object -Parallel)
|
|
function Save-MicrosoftDriversTask {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[PSCustomObject]$DriverItemData, # Pass data, not the UI object
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DriversFolder,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$WindowsRelease,
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable]$Headers, # Pass necessary headers
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserAgent, # Pass UserAgent
|
|
[Parameter()] # Made optional
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
|
[Parameter()]
|
|
[bool]$CompressToWim = $false # New parameter for compression
|
|
# REMOVED: UI-related parameters
|
|
)
|
|
|
|
$modelName = $DriverItemData.Model
|
|
$modelLink = $DriverItemData.Link
|
|
$make = $DriverItemData.Make
|
|
$status = "Getting download link..." # Initial local status
|
|
$success = $false
|
|
|
|
# Initial status update
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
|
|
|
|
try {
|
|
# Check if drivers already exist for this model
|
|
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
|
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
|
|
if (Test-Path -Path $modelPath -PathType Container) {
|
|
$folderSize = (Get-ChildItem -Path $modelPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($folderSize -gt 1MB) {
|
|
$status = "Already downloaded"
|
|
WriteLog "Drivers for '$modelName' already exist in '$modelPath'."
|
|
# Enqueue this status before returning
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
# Return success immediately
|
|
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $true }
|
|
}
|
|
else {
|
|
# Status is not set to error here, just log and continue
|
|
WriteLog "Driver folder '$modelPath' for '$modelName' exists but is empty or very small. Re-downloading."
|
|
# Allow the process to continue to re-download
|
|
}
|
|
}
|
|
|
|
### GET THE DOWNLOAD LINK
|
|
$status = "Getting download link..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
WriteLog "Getting download page content for $modelName from $modelLink"
|
|
$OriginalVerbosePreference = $VerbosePreference
|
|
$VerbosePreference = 'SilentlyContinue'
|
|
# Use passed-in UserAgent and Headers
|
|
$downloadPageContent = Invoke-WebRequest -Uri $modelLink -UseBasicParsing -Headers $Headers -UserAgent $UserAgent
|
|
$VerbosePreference = $OriginalVerbosePreference
|
|
WriteLog "Complete"
|
|
|
|
$status = "Parsing download page..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
WriteLog "Parsing download page for file"
|
|
$scriptPattern = '<script>window.__DLCDetails__={(.*?)}<\/script>'
|
|
$scriptMatch = [regex]::Match($downloadPageContent.Content, $scriptPattern)
|
|
|
|
if ($scriptMatch.Success) {
|
|
$scriptContent = $scriptMatch.Groups[1].Value
|
|
# $downloadFilePattern = '"name":"(.*?)",.*?"url":"(.*?)"'
|
|
$downloadFilePattern = '"name":"([^"]+\.(?:msi|zip))",[^}]*?"url":"(.*?)"'
|
|
$downloadFileMatches = [regex]::Matches($scriptContent, $downloadFilePattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
|
|
|
|
$win10Link = $null
|
|
$win10FileName = $null
|
|
$win11Link = $null
|
|
$win11FileName = $null
|
|
|
|
# Iterate through all matches to find potential Win10 and Win11 links
|
|
foreach ($downloadFile in $downloadFileMatches) {
|
|
$currentFileName = $downloadFile.Groups[1].Value
|
|
$fileUrl = $downloadFile.Groups[2].Value
|
|
|
|
if ($currentFileName -match "Win10") {
|
|
$win10Link = $fileUrl
|
|
$win10FileName = $currentFileName
|
|
WriteLog "Found Win10 link: $win10FileName"
|
|
}
|
|
elseif ($currentFileName -match "Win11") {
|
|
$win11Link = $fileUrl
|
|
$win11FileName = $currentFileName
|
|
WriteLog "Found Win11 link: $win11FileName"
|
|
}
|
|
}
|
|
|
|
# Decision logic to select the appropriate download link
|
|
$downloadLink = $null
|
|
$fileName = $null
|
|
$downloadedVersion = $null # Track which version we are actually downloading
|
|
|
|
if ($WindowsRelease -eq 10 -and $win10Link) {
|
|
$downloadLink = $win10Link
|
|
$fileName = $win10FileName
|
|
$downloadedVersion = 10
|
|
WriteLog "Exact match found for Win10."
|
|
}
|
|
elseif ($WindowsRelease -eq 11 -and $win11Link) {
|
|
$downloadLink = $win11Link
|
|
$fileName = $win11FileName
|
|
$downloadedVersion = 11
|
|
WriteLog "Exact match found for Win11."
|
|
}
|
|
elseif (-not $win10Link -and $win11Link) {
|
|
# Only Win11 available, regardless of $WindowsRelease
|
|
$downloadLink = $win11Link
|
|
$fileName = $win11FileName
|
|
$downloadedVersion = 11
|
|
WriteLog "Exact match for Win$($WindowsRelease) not found. Falling back to available Win11 driver."
|
|
}
|
|
elseif ($win10Link -and -not $win11Link) {
|
|
# Only Win10 available, regardless of $WindowsRelease
|
|
$downloadLink = $win10Link
|
|
$fileName = $win10FileName
|
|
$downloadedVersion = 10
|
|
WriteLog "Exact match for Win$($WindowsRelease) not found. Falling back to available Win10 driver."
|
|
}
|
|
# If both Win10 and Win11 links exist, but neither matches $WindowsRelease, $downloadLink remains $null.
|
|
|
|
### DOWNLOAD AND EXTRACT
|
|
if ($downloadLink) {
|
|
WriteLog "Selected Download Link for $modelName (Actual: Windows $downloadedVersion): $downloadLink"
|
|
$status = "Downloading (Win$downloadedVersion)..." # Update status message
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
|
|
# Create directories
|
|
if (-not (Test-Path -Path $DriversFolder)) {
|
|
WriteLog "Creating Drivers folder: $DriversFolder"
|
|
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
|
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
|
|
if (-Not (Test-Path -Path $modelPath)) {
|
|
WriteLog "Creating model folder: $modelPath"
|
|
New-Item -Path $modelPath -ItemType Directory -Force | Out-Null
|
|
}
|
|
else {
|
|
WriteLog "Model folder already exists: $modelPath"
|
|
}
|
|
|
|
### DOWNLOAD
|
|
$filePath = Join-Path -Path $makeDriversPath -ChildPath ($fileName)
|
|
WriteLog "Downloading $modelName driver file to $filePath"
|
|
# Use Start-BitsTransferWithRetry
|
|
Start-BitsTransferWithRetry -Source $downloadLink -Destination $filePath
|
|
WriteLog "Download complete"
|
|
|
|
$fileExtension = [System.IO.Path]::GetExtension($filePath).ToLower()
|
|
|
|
### EXTRACT
|
|
if ($fileExtension -eq ".msi") {
|
|
$status = "Extracting MSI..." # Set initial status
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
|
|
# Loop indefinitely to wait for mutex and handle MSIExec exit codes by catching errors
|
|
while ($true) {
|
|
$mutexClear = $false
|
|
|
|
# 1. Check Mutex
|
|
try {
|
|
$Mutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute")
|
|
$Mutex.Dispose()
|
|
$status = "Waiting for MSIExec..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
WriteLog "Another MSIExec installer is running (Mutex Held). Waiting 5 seconds before rechecking for $modelName..."
|
|
Start-Sleep -Seconds 5
|
|
continue # Go back to start of while loop to re-check mutex
|
|
}
|
|
catch [System.Threading.WaitHandleCannotBeOpenedException] {
|
|
# Mutex is clear, proceed to extraction attempt
|
|
WriteLog "Mutex clear. Proceeding with MSI extraction attempt for $modelName."
|
|
$status = "Extracting MSI..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
$mutexClear = $true
|
|
}
|
|
catch {
|
|
# Handle other potential errors when checking the mutex
|
|
WriteLog "Warning: Error checking MSIExec mutex for $($modelName): $_. Proceeding with caution."
|
|
$status = "Extracting MSI (Mutex Error)..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
$mutexClear = $true # Proceed despite mutex error
|
|
}
|
|
|
|
# 2. Attempt Extraction (only if mutex was clear or error occurred during check)
|
|
if ($mutexClear) {
|
|
WriteLog "Extracting MSI file to $modelPath"
|
|
$arguments = "/a `"$($filePath)`" /qn TARGETDIR=`"$($modelPath)`""
|
|
try {
|
|
# Use Invoke-Process. It will throw an error for any non-zero exit code.
|
|
Invoke-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait $true -ErrorAction Stop | Out-Null
|
|
|
|
# If Invoke-Process succeeded (didn't throw), extraction is complete.
|
|
WriteLog "Extraction complete for $modelName (Exit Code 0)."
|
|
break # Success, exit the while loop
|
|
}
|
|
catch {
|
|
# Catch errors thrown by Invoke-Process
|
|
$errorMessage = $_.Exception.Message
|
|
if ($errorMessage -match 'Process exited with code 1618') {
|
|
# Specific handling for MSIExec busy error (1618)
|
|
WriteLog "MSIExec collision detected (Exit Code 1618) for $modelName. Retrying after wait..."
|
|
$status = "Waiting (MSI Collision)..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
Start-Sleep -Seconds 5 # Wait before retrying
|
|
continue # Go back to start of while loop to re-check mutex/retry
|
|
}
|
|
else {
|
|
# Handle other errors from Invoke-Process (e.g., file not found, permissions, other exit codes)
|
|
WriteLog "Error during MSI extraction process for $($modelName): $errorMessage"
|
|
throw # Re-throw the original exception to be caught by the outer try/catch
|
|
}
|
|
}
|
|
} # End if ($mutexClear)
|
|
} # End while ($true) - Loop runs until break or throw
|
|
}
|
|
elseif ($fileExtension -eq ".zip") {
|
|
$status = "Extracting ZIP..." # Set status before extraction
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
WriteLog "Extracting ZIP file to $modelPath"
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
Expand-Archive -Path $filePath -DestinationPath $modelPath -Force
|
|
$ProgressPreference = 'Continue'
|
|
WriteLog "Extraction complete"
|
|
}
|
|
else {
|
|
WriteLog "Unsupported file type: $fileExtension"
|
|
throw "Unsupported file type: $fileExtension"
|
|
}
|
|
# Remove downloaded file
|
|
$status = "Cleaning up..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
WriteLog "Removing $filePath"
|
|
Remove-Item -Path $filePath -Force
|
|
WriteLog "Cleanup complete." # Changed log message slightly
|
|
|
|
# --- Compress to WIM if requested ---
|
|
if ($CompressToWim) {
|
|
$status = "Compressing..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
$wimFileName = "$($modelName).wim"
|
|
# Corrected WIM path: WIM file should be next to the model folder, not inside it.
|
|
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
|
|
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
|
try {
|
|
# Use the function from the imported common module
|
|
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop
|
|
if ($compressResult) {
|
|
WriteLog "Compression successful for '$modelName'."
|
|
$status = "Completed & Compressed"
|
|
}
|
|
else {
|
|
WriteLog "Compression failed for '$modelName'. Check verbose/error output from Compress-DriverFolderToWim."
|
|
$status = "Completed (Compression Failed)"
|
|
# Don't mark overall success as false, download/extract succeeded
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Error during compression for '$modelName': $($_.Exception.Message)"
|
|
$status = "Completed (Compression Error)"
|
|
# Don't mark overall success as false
|
|
}
|
|
}
|
|
else {
|
|
$status = "Completed" # Final status if not compressing
|
|
}
|
|
# --- End Compression ---
|
|
|
|
$success = $true # Mark success as download/extract was okay
|
|
} # End if/elseif for .msi/.zip
|
|
else {
|
|
WriteLog "No suitable download link found for Windows $WindowsRelease (or fallback) for model $modelName."
|
|
$status = "Error: No Win$($WindowsRelease)/Fallback link"
|
|
$success = $false
|
|
}
|
|
}
|
|
else {
|
|
WriteLog "Failed to parse the download page for the driver file for model $modelName."
|
|
$status = "Error: Parse failed"
|
|
$success = $false
|
|
}
|
|
}
|
|
catch {
|
|
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
|
|
WriteLog "Error saving Microsoft drivers for $($modelName): $($_.Exception.Message)"
|
|
$success = $false
|
|
# Enqueue the error status before returning
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
# Ensure return object is created even on error
|
|
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
|
|
}
|
|
|
|
# Enqueue the final status (success or error) before returning
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
|
|
# Return the final status (this is still used by Receive-Job for final confirmation)
|
|
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
|
|
}
|
|
# Function to download and extract drivers for a specific Dell model (Modified for ForEach-Object -Parallel)
|
|
function Save-DellDriversTask {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[PSCustomObject]$DriverItemData, # Contains Model property
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DriversFolder, # Base drivers folder (e.g., C:\FFUDevelopment\Drivers)
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WindowsArch,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$WindowsRelease,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DellCatalogXmlPath, # Path to the *existing* central XML catalog file
|
|
[Parameter()] # Made optional
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
|
[Parameter()]
|
|
[bool]$CompressToWim = $false # New parameter for compression
|
|
# REMOVED: UI-related parameters, Catalog download/extract params
|
|
)
|
|
|
|
$modelName = $DriverItemData.Model
|
|
$make = "Dell" # Hardcoded for this task
|
|
$status = "Starting..." # Initial local status
|
|
$success = $false
|
|
|
|
# Initial status update
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
|
|
|
|
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
|
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
|
|
|
|
try {
|
|
# 1. Check if drivers already exist for this model (final destination)
|
|
if (Test-Path -Path $modelPath -PathType Container) {
|
|
$folderSize = (Get-ChildItem -Path $modelPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($folderSize -gt 1MB) {
|
|
$status = "Already downloaded"
|
|
WriteLog "Drivers for '$modelName' already exist in '$modelPath'."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $true }
|
|
}
|
|
else {
|
|
WriteLog "Driver folder '$modelPath' for '$modelName' exists but is empty/small. Re-downloading."
|
|
}
|
|
}
|
|
|
|
# 2. REMOVED: Download and Extract Catalog - This is now done centrally in the UI script
|
|
|
|
# 3. Parse the *EXISTING* XML and Find Drivers for *this specific model*
|
|
$status = "Finding drivers..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
|
|
# Check if the provided XML path exists
|
|
if (-not (Test-Path -Path $DellCatalogXmlPath -PathType Leaf)) {
|
|
throw "Dell Catalog XML file not found at specified path: $DellCatalogXmlPath"
|
|
}
|
|
|
|
WriteLog "Parsing existing Dell Catalog XML for model '$modelName' from: $DellCatalogXmlPath"
|
|
[xml]$xmlContent = Get-Content -Path $DellCatalogXmlPath
|
|
# Check if manifest and baseLocation exist before accessing
|
|
if ($null -eq $xmlContent.manifest -or $null -eq $xmlContent.manifest.baseLocation) {
|
|
throw "Invalid Dell Catalog XML format: Missing 'manifest' or 'baseLocation' element in '$DellCatalogXmlPath'."
|
|
}
|
|
$baseLocation = "https://" + $xmlContent.manifest.baseLocation + "/"
|
|
$latestDrivers = @{} # Hashtable to store latest drivers for this model
|
|
|
|
# Ensure SoftwareComponent is iterable
|
|
$softwareComponents = @($xmlContent.Manifest.SoftwareComponent | Where-Object { $_.ComponentType.value -eq "DRVR" })
|
|
$modelSpecificDriversFound = $false
|
|
|
|
WriteLog "Searching $($softwareComponents.Count) DRVR components in '$DellCatalogXmlPath' for model '$modelName'..."
|
|
|
|
foreach ($component in $softwareComponents) {
|
|
# Check if SupportedSystems and Brand exist
|
|
if ($null -eq $component.SupportedSystems -or $null -eq $component.SupportedSystems.Brand) { continue }
|
|
# Ensure Model is iterable
|
|
$componentModels = @($component.SupportedSystems.Brand.Model)
|
|
if ($null -eq $componentModels) { continue }
|
|
|
|
$modelMatch = $false
|
|
foreach ($item in $componentModels) {
|
|
# Check if Display and its CDATA section exist before accessing
|
|
if ($null -ne $item.Display -and $null -ne $item.Display.'#cdata-section' -and $item.Display.'#cdata-section'.Trim() -eq $modelName) {
|
|
$modelMatch = $true
|
|
break
|
|
}
|
|
}
|
|
|
|
if ($modelMatch) {
|
|
# Model matches, now check OS compatibility
|
|
$validOS = $null
|
|
if ($null -ne $component.SupportedOperatingSystems) {
|
|
# Ensure OperatingSystem is always an array/collection
|
|
$osList = @($component.SupportedOperatingSystems.OperatingSystem)
|
|
|
|
if ($null -ne $osList) {
|
|
if ($WindowsRelease -le 11) {
|
|
# Client OS check
|
|
$validOS = $osList | Where-Object { $_.osArch -eq $WindowsArch } | Select-Object -First 1
|
|
}
|
|
else {
|
|
# Server OS check
|
|
$osCodePattern = switch ($WindowsRelease) {
|
|
2016 { "W14" } # Note: Dell uses W14 for Server 2016
|
|
2019 { "W19" }
|
|
2022 { "W22" }
|
|
2025 { "W25" }
|
|
default { "W22" } # Fallback, adjust as needed
|
|
}
|
|
$validOS = $osList | Where-Object { ($_.osArch -eq $WindowsArch) -and ($_.osCode -match $osCodePattern) } | Select-Object -First 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($validOS) {
|
|
$modelSpecificDriversFound = $true # Mark that we found at least one relevant driver component
|
|
$driverPath = $component.path
|
|
$downloadUrl = $baseLocation + $driverPath
|
|
$driverFileName = [System.IO.Path]::GetFileName($driverPath)
|
|
# Check if Name, Display, and CDATA exist
|
|
$name = "UnknownDriver" # Default name
|
|
if ($null -ne $component.Name -and $null -ne $component.Name.Display -and $null -ne $component.Name.Display.'#cdata-section') {
|
|
$name = $component.Name.Display.'#cdata-section'
|
|
$name = $name -replace '[\\\/\:\*\?\"\<\>\| ]', '_' -replace '[\,]', '-'
|
|
}
|
|
# Check if Category, Display, and CDATA exist
|
|
$category = "Uncategorized" # Default category
|
|
if ($null -ne $component.Category -and $null -ne $component.Category.Display -and $null -ne $component.Category.Display.'#cdata-section') {
|
|
$category = $component.Category.Display.'#cdata-section'
|
|
$category = $category -replace '[\\\/\:\*\?\"\<\>\| ]', '_'
|
|
}
|
|
$version = [version]"0.0" # Default version
|
|
if ($null -ne $component.vendorVersion) {
|
|
try { $version = [version]$component.vendorVersion } catch { WriteLog "Warning: Could not parse version '$($component.vendorVersion)' for driver '$name'. Using 0.0." }
|
|
}
|
|
$namePrefix = ($name -split '-')[0] # Group by prefix within category
|
|
|
|
# Store the latest version for each category/prefix combination
|
|
if (-not $latestDrivers.ContainsKey($category)) { $latestDrivers[$category] = @{} }
|
|
if (-not $latestDrivers[$category].ContainsKey($namePrefix) -or $latestDrivers[$category][$namePrefix].Version -lt $version) {
|
|
$latestDrivers[$category][$namePrefix] = [PSCustomObject]@{
|
|
Name = $name
|
|
DownloadUrl = $downloadUrl
|
|
DriverFileName = $driverFileName
|
|
Version = $version
|
|
Category = $category
|
|
}
|
|
}
|
|
}
|
|
} # End if ($modelMatch)
|
|
} # End foreach ($component in $softwareComponents)
|
|
|
|
if (-not $modelSpecificDriversFound) {
|
|
$status = "No drivers found for OS"
|
|
WriteLog "No drivers found for model '$modelName' matching Windows Release '$WindowsRelease' and Arch '$WindowsArch' in '$DellCatalogXmlPath'."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
# Consider this success as the process completed, just no drivers to download
|
|
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $true }
|
|
}
|
|
|
|
# 4. Download and Extract Found Drivers (Logic remains largely the same)
|
|
$totalDriversToProcess = ($latestDrivers.Values | ForEach-Object { $_.Values.Count } | Measure-Object -Sum).Sum
|
|
$driversProcessed = 0
|
|
WriteLog "Found $totalDriversToProcess latest driver packages to download for $modelName."
|
|
|
|
# Ensure base directories exist before loop
|
|
if (-not (Test-Path -Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
|
|
if (-not (Test-Path -Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
|
|
|
|
foreach ($category in $latestDrivers.Keys) {
|
|
foreach ($driver in $latestDrivers[$category].Values) {
|
|
$driversProcessed++
|
|
$status = "Downloading $($driversProcessed)/$($totalDriversToProcess): $($driver.Name)"
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
|
|
$downloadFolder = Join-Path -Path $modelPath -ChildPath $driver.Category
|
|
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName
|
|
$extractFolder = Join-Path -Path $downloadFolder -ChildPath $driver.DriverFileName.TrimEnd($driver.DriverFileName[-4..-1])
|
|
|
|
# Check if already extracted (more robust check)
|
|
if (Test-Path -Path $extractFolder -PathType Container) {
|
|
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($extractSize -gt 1KB) {
|
|
WriteLog "Driver already extracted: $($driver.Name) in $extractFolder. Skipping."
|
|
continue # Skip to next driver
|
|
}
|
|
}
|
|
# Check if download file exists but extraction folder doesn't or is empty
|
|
if (Test-Path -Path $driverFilePath -PathType Leaf) {
|
|
WriteLog "Download file $($driver.DriverFileName) exists, but extraction folder '$extractFolder' is missing or empty. Will attempt extraction."
|
|
# Proceed to extraction logic below
|
|
}
|
|
else {
|
|
# Download the driver
|
|
WriteLog "Downloading driver: $($driver.Name) ($($driver.DriverFileName))"
|
|
if (-not (Test-Path -Path $downloadFolder)) {
|
|
WriteLog "Creating download folder: $downloadFolder"
|
|
New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
WriteLog "Downloading from: $($driver.DownloadUrl) to $driverFilePath"
|
|
try {
|
|
Start-BitsTransferWithRetry -Source $driver.DownloadUrl -Destination $driverFilePath
|
|
WriteLog "Driver downloaded: $($driver.DriverFileName)"
|
|
}
|
|
catch {
|
|
WriteLog "Failed to download driver: $($driver.DownloadUrl). Error: $($_.Exception.Message). Skipping."
|
|
# Update status for this specific driver failure? Maybe too granular.
|
|
continue # Skip to next driver
|
|
}
|
|
}
|
|
|
|
|
|
# Extract the driver
|
|
$status = "Extracting $($driversProcessed)/$($totalDriversToProcess): $($driver.Name)"
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
|
|
# Ensure extraction folder exists before attempting extraction
|
|
if (-not (Test-Path -Path $extractFolder)) {
|
|
WriteLog "Creating extraction folder: $extractFolder"
|
|
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
# Dell uses /e to extact the entire DUP while /drivers to extract only the drivers
|
|
# In many cases /drivers will extract drivers for mutliple OS versions
|
|
# Which can cause many duplicate files and bloat your driver folder
|
|
# /e seems to be better and only extracts what is necessary and has less issues
|
|
# We will default to using /e, but will fall back to /drivers if content cannot be found
|
|
|
|
$arguments = "/s /e=`"$extractFolder`" /l=`"$extractFolder\log.log`""
|
|
$altArguments = "/s /drivers=`"$extractFolder`" /l=`"$extractFolder\log.log`""
|
|
$extractionSuccess = $false
|
|
try {
|
|
# Handle special cases (Chipset/Network) - Check if OS is Server
|
|
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem # Get OS info within the task scope
|
|
$isServer = $osInfo.Caption -match 'server'
|
|
|
|
# Chipset drivers may require killing child processes in some cases
|
|
if ($driver.Category -eq "Chipset") {
|
|
WriteLog "Extracting Chipset driver: $driverFilePath $arguments"
|
|
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false
|
|
Start-Sleep -Seconds 5 # Allow time for extraction
|
|
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
|
|
# Attempt to gracefully close child process if needed (logic from original script)
|
|
$childProcesses = Get-CimInstance Win32_Process -Filter "ParentProcessId = $($process.Id)"
|
|
if ($childProcesses) {
|
|
$latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1
|
|
WriteLog "Stopping child process for Chipset driver: $($latestProcess.Name) (PID: $($latestProcess.ProcessId))"
|
|
Stop-Process -Id $latestProcess.ProcessId -Force -ErrorAction SilentlyContinue
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
}
|
|
# Network drivers on client OS may require killing child processes
|
|
elseif ($driver.Category -eq "Network" -and -not $isServer) {
|
|
WriteLog "Extracting Network driver: $driverFilePath $arguments"
|
|
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait $false
|
|
Start-Sleep -Seconds 5
|
|
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
|
|
if (-not $process.HasExited) {
|
|
$childProcesses = Get-CimInstance Win32_Process -Filter "ParentProcessId = $($process.Id)"
|
|
if ($childProcesses) {
|
|
$latestProcess = $childProcesses | Sort-Object CreationDate -Descending | Select-Object -First 1
|
|
WriteLog "Stopping child process for Network driver: $($latestProcess.Name) (PID: $($latestProcess.ProcessId))"
|
|
Stop-Process -Id $latestProcess.ProcessId -Force -ErrorAction SilentlyContinue
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
WriteLog "Extracting driver: $driverFilePath $arguments"
|
|
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments
|
|
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
|
|
}
|
|
|
|
# Verify extraction (check if folder has content)
|
|
if (Test-Path -Path $extractFolder -PathType Container) {
|
|
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($extractSize -gt 1KB) {
|
|
$extractionSuccess = $true
|
|
WriteLog "Extraction successful (Method 1) for $driverFilePath $arguments"
|
|
}
|
|
}
|
|
|
|
# If primary extraction failed or folder is empty, try alternative
|
|
if (-not $extractionSuccess) {
|
|
# $arguments = "/s /e=`"$extractFolder`""
|
|
# $altArguments = "/s /drivers=`"$extractFolder`""
|
|
WriteLog "Extraction with $arguments failed or resulted in empty folder for $driverFilePath. Retrying with $altArguments"
|
|
# Clean up potentially empty folder before retrying
|
|
Remove-Item -Path $extractFolder -Recurse -Force -ErrorAction SilentlyContinue
|
|
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null # Recreate empty folder
|
|
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $altArguments
|
|
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
|
|
|
|
# Verify extraction again
|
|
if (Test-Path -Path $extractFolder -PathType Container) {
|
|
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($extractSize -gt 1KB) {
|
|
$extractionSuccess = $true
|
|
WriteLog "Extraction successful (Method 2) for $driverFilePath $altArguments"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Error during extraction process for $($driver.DriverFileName): $($_.Exception.Message). Trying alternative method."
|
|
# Try alternative method on any error during the first attempt block
|
|
try {
|
|
if (Test-Path -Path $extractFolder) {
|
|
# Clean up before retry if needed
|
|
Remove-Item -Path $extractFolder -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
|
|
# $arguments = "/s /e=`"$extractFolder`""
|
|
# $altArguments = "/s /drivers=`"$extractFolder`""
|
|
WriteLog "Extracting driver (Method 2): $driverFilePath $altArguments"
|
|
$process = Invoke-Process -FilePath $driverFilePath -ArgumentList $altArguments
|
|
WriteLog "Extraction exited with exit code: $($process.ExitCode)"
|
|
|
|
# Verify extraction again
|
|
if (Test-Path -Path $extractFolder -PathType Container) {
|
|
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse -Exclude *.log | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
if ($extractSize -gt 1KB) {
|
|
$extractionSuccess = $true
|
|
WriteLog "Extraction successful (Method 2) for $driverFilePath."
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Alternative extraction method also failed for $($driver.DriverFileName): $($_.Exception.Message)."
|
|
# Extraction failed completely
|
|
}
|
|
}
|
|
|
|
# Cleanup downloaded file only if extraction was successful
|
|
if ($extractionSuccess) {
|
|
WriteLog "Deleting driver file: $driverFilePath"
|
|
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
|
|
WriteLog "Driver file deleted: $driverFilePath"
|
|
}
|
|
else {
|
|
WriteLog "Extraction failed for $($driver.DriverFileName). Downloaded file kept at $driverFilePath for inspection."
|
|
# Update status to indicate partial failure?
|
|
}
|
|
|
|
} # End foreach ($driver in $latestDrivers)
|
|
} # End foreach ($category in $latestDrivers)
|
|
|
|
# --- Compress to WIM if requested (after all drivers processed) ---
|
|
if ($CompressToWim) {
|
|
$status = "Compressing..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
$wimFileName = "$($modelName).wim"
|
|
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
|
|
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
|
try {
|
|
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop
|
|
if ($compressResult) {
|
|
WriteLog "Compression successful for '$modelName'."
|
|
$status = "Completed & Compressed"
|
|
}
|
|
else {
|
|
WriteLog "Compression failed for '$modelName'. Check verbose/error output from Compress-DriverFolderToWim."
|
|
$status = "Completed (Compression Failed)"
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Error during compression for '$modelName': $($_.Exception.Message)"
|
|
$status = "Completed (Compression Error)"
|
|
}
|
|
}
|
|
else {
|
|
$status = "Completed" # Final status if not compressing
|
|
}
|
|
# --- End Compression ---
|
|
|
|
$success = $true # Mark success as download/extract was okay
|
|
|
|
}
|
|
catch {
|
|
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
|
|
WriteLog "Error saving Dell drivers for $($modelName): $($_.Exception.ToString())" # Log full exception string
|
|
$success = $false
|
|
# Enqueue the error status before returning
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
# Ensure return object is created even on error
|
|
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
|
|
}
|
|
# REMOVED: Finally block that cleaned up temp catalog files
|
|
|
|
# Enqueue the final status (success or error) before returning
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
|
|
|
|
# Return the final status
|
|
return [PSCustomObject]@{ Model = $modelName; Status = $status; Success = $success }
|
|
}
|
|
# Function to download and extract drivers for a specific HP model (Designed for ForEach-Object -Parallel)
|
|
function Save-HPDriversTask {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[PSCustomObject]$DriverItemData, # Contains Make, Model
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DriversFolder,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("x64", "x86", "ARM64")]
|
|
[string]$WindowsArch,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet(10, 11)]
|
|
[int]$WindowsRelease,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WindowsVersion, # e.g., 22H2, 23H2, etc.
|
|
[Parameter()] # Made optional
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
|
[Parameter()]
|
|
[bool]$CompressToWim = $false # New parameter for compression
|
|
)
|
|
|
|
$modelName = $DriverItemData.Model
|
|
$make = $DriverItemData.Make # Should be 'HP'
|
|
$identifier = $modelName # Unique identifier for progress updates
|
|
$hpDriversBaseFolder = Join-Path -Path $DriversFolder -ChildPath $make # Changed variable name for clarity
|
|
$platformListXml = Join-Path -Path $hpDriversBaseFolder -ChildPath "PlatformList.xml"
|
|
$modelSpecificFolder = Join-Path -Path $hpDriversBaseFolder -ChildPath ($modelName -replace '[\\/:"*?<>|]', '_') # Sanitize model name for folder path
|
|
$finalStatus = "" # Initialize final status
|
|
$successState = $true # Assume success unless an operation fails
|
|
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking HP drivers for $modelName..." }
|
|
|
|
# Ensure the base HP folder exists
|
|
if (-not (Test-Path -Path $hpDriversBaseFolder -PathType Container)) {
|
|
try {
|
|
New-Item -Path $hpDriversBaseFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
|
WriteLog "Created base HP driver folder: $hpDriversBaseFolder"
|
|
}
|
|
catch {
|
|
$errMsg = "Failed to create base HP driver folder '$hpDriversBaseFolder': $($_.Exception.Message)"
|
|
WriteLog $errMsg
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Error: Create HP dir failed" }
|
|
return [PSCustomObject]@{ Identifier = $identifier; Status = "Error: Create HP dir failed"; Success = $false }
|
|
}
|
|
}
|
|
|
|
# Check if drivers already exist for this model
|
|
if (Test-Path -Path $modelSpecificFolder -PathType Container) {
|
|
WriteLog "HP drivers for '$identifier' already exist in '$modelSpecificFolder'."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Found existing HP drivers for $identifier. Verifying..." }
|
|
|
|
if ($CompressToWim) {
|
|
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim" # WIM in base HP folder, next to model folder
|
|
|
|
if (Test-Path -Path $wimFilePath -PathType Leaf) {
|
|
$finalStatus = "Already downloaded (WIM exists)"
|
|
WriteLog "WIM file $wimFilePath already exists for $identifier."
|
|
}
|
|
else {
|
|
WriteLog "WIM file $wimFilePath not found for $identifier. Attempting compression of existing folder '$modelSpecificFolder'."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing HP drivers for $identifier..." }
|
|
try {
|
|
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
|
|
$finalStatus = "Already downloaded & Compressed"
|
|
WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath."
|
|
}
|
|
catch {
|
|
$errMsgForLog = "Error compressing existing drivers for $($identifier): $($_.Exception.Message)"
|
|
WriteLog $errMsgForLog
|
|
$finalStatus = "Already downloaded (Compression failed: $($_.Exception.Message.Split([Environment]::NewLine)[0]))"
|
|
# $successState = false # Keep true if folder exists, compression is secondary
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# Not compressing
|
|
$finalStatus = "Already downloaded"
|
|
}
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $finalStatus }
|
|
return [PSCustomObject]@{
|
|
Identifier = $identifier
|
|
Status = $finalStatus
|
|
Success = $successState
|
|
}
|
|
}
|
|
|
|
# If folder does not exist, proceed with download and extraction
|
|
WriteLog "HP drivers for '$identifier' not found locally. Starting download process..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Downloading HP drivers for $identifier..." }
|
|
|
|
try {
|
|
# Ensure PlatformList.xml exists (it should have been downloaded by Get-HPDriversModelList)
|
|
if (-not (Test-Path -Path $platformListXml)) {
|
|
# Attempt to download/extract it again if missing
|
|
WriteLog "PlatformList.xml not found for HP task, attempting download/extract..."
|
|
$platformListUrl = 'https://hpia.hpcloud.hp.com/ref/platformList.cab'
|
|
$platformListCab = Join-Path -Path $hpDriversBaseFolder -ChildPath "platformList.cab"
|
|
# Base folder already checked/created
|
|
Start-BitsTransferWithRetry -Source $platformListUrl -Destination $platformListCab -ErrorAction Stop
|
|
if (Test-Path -Path $platformListXml) { Remove-Item -Path $platformListXml -Force }
|
|
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$platformListCab`"", "`"$platformListXml`"") -ErrorAction Stop | Out-Null
|
|
WriteLog "PlatformList.xml download/extract complete for HP task."
|
|
if (-not (Test-Path -Path $platformListXml)) {
|
|
throw "Failed to obtain PlatformList.xml for HP driver task."
|
|
}
|
|
}
|
|
|
|
# Parse PlatformList.xml to find SystemID and OSReleaseID for the specific model
|
|
WriteLog "Parsing $platformListXml for model '$modelName' details..."
|
|
[xml]$platformListContent = Get-Content -Path $platformListXml -Raw -Encoding UTF8 -ErrorAction Stop
|
|
$platformNode = $platformListContent.ImagePal.Platform | Where-Object { $_.ProductName.'#text' -match "^$([regex]::Escape($modelName))$" } | Select-Object -First 1
|
|
|
|
if ($null -eq $platformNode) {
|
|
throw "Model '$modelName' not found in PlatformList.xml."
|
|
}
|
|
|
|
$systemID = $platformNode.SystemID
|
|
# --- OS Node Selection with Fallback Logic ---
|
|
$selectedOSNode = $null
|
|
$selectedOSVersion = $null
|
|
$selectedOSRelease = $WindowsRelease # Start with the requested release
|
|
|
|
# Complete list of Windows 11 feature-update versions (newest to oldest)
|
|
$win11Versions = @(
|
|
"24H2", "23H2", "22H2", "21H2"
|
|
)
|
|
|
|
# Complete list of Windows 10 feature-update versions (newest to oldest)
|
|
$win10Versions = @(
|
|
"22H2", "21H2", "21H1", "20H2", "2004", "1909", "1903", "1809", "1803", "1709", "1703", "1607", "1511", "1507"
|
|
)
|
|
|
|
# Helper function to find a matching OS node for a given release and version list
|
|
function Find-MatchingOSNode {
|
|
param(
|
|
[int]$ReleaseToSearch,
|
|
[array]$VersionsToSearch
|
|
)
|
|
$osNodesForRelease = $platformNode.OS | Where-Object {
|
|
($ReleaseToSearch -eq 11 -and $_.IsWindows11 -contains 'true') -or
|
|
($ReleaseToSearch -eq 10 -and ($null -eq $_.IsWindows11 -or $_.IsWindows11 -notcontains 'true'))
|
|
}
|
|
|
|
if ($null -eq $osNodesForRelease) { return $null }
|
|
|
|
foreach ($version in $VersionsToSearch) {
|
|
foreach ($osNode in $osNodesForRelease) {
|
|
$releaseIDs = $osNode.OSReleaseIdFileName -replace 'H', 'h' -split ' '
|
|
if ($releaseIDs -contains $version.ToLower()) {
|
|
return @{ Node = $osNode; Version = $version }
|
|
}
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# 1. Attempt Exact Match (Requested Release and Version)
|
|
WriteLog "Attempting to find exact match for Win$($WindowsRelease) ($($WindowsVersion))..."
|
|
$exactMatchResult = Find-MatchingOSNode -ReleaseToSearch $WindowsRelease -VersionsToSearch @($WindowsVersion)
|
|
if ($null -ne $exactMatchResult) {
|
|
$selectedOSNode = $exactMatchResult.Node
|
|
$selectedOSVersion = $exactMatchResult.Version
|
|
WriteLog "Exact match found: Win$($selectedOSRelease) ($($selectedOSVersion))."
|
|
}
|
|
else {
|
|
WriteLog "Exact match not found for Win$($WindowsRelease) ($($WindowsVersion))."
|
|
# 2. Fallback: Same Release, Other Versions (Newest First)
|
|
WriteLog "Attempting fallback within Win$($WindowsRelease)..."
|
|
$versionsForCurrentRelease = if ($WindowsRelease -eq 11) { $win11Versions } else { $win10Versions }
|
|
$fallbackVersions = $versionsForCurrentRelease | Where-Object { $_ -ne $WindowsVersion }
|
|
$fallbackResult = Find-MatchingOSNode -ReleaseToSearch $WindowsRelease -VersionsToSearch $fallbackVersions
|
|
if ($null -ne $fallbackResult) {
|
|
$selectedOSNode = $fallbackResult.Node
|
|
$selectedOSVersion = $fallbackResult.Version
|
|
WriteLog "Fallback successful within Win$($selectedOSRelease). Using version: $($selectedOSVersion)."
|
|
}
|
|
else {
|
|
WriteLog "Fallback within Win$($WindowsRelease) unsuccessful."
|
|
# 3. Fallback: Other Release, Versions (Newest First)
|
|
$otherRelease = if ($WindowsRelease -eq 11) { 10 } else { 11 }
|
|
WriteLog "Attempting fallback to Win$($otherRelease)..."
|
|
$versionsForOtherRelease = if ($otherRelease -eq 11) { $win11Versions } else { $win10Versions }
|
|
$otherFallbackResult = Find-MatchingOSNode -ReleaseToSearch $otherRelease -VersionsToSearch $versionsForOtherRelease
|
|
if ($null -ne $otherFallbackResult) {
|
|
$selectedOSNode = $otherFallbackResult.Node
|
|
$selectedOSVersion = $otherFallbackResult.Version
|
|
$selectedOSRelease = $otherRelease
|
|
WriteLog "Fallback successful to Win$($selectedOSRelease). Using version: $($selectedOSVersion)."
|
|
}
|
|
else {
|
|
WriteLog "Fallback to Win$($otherRelease) also failed."
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($null -eq $selectedOSNode) {
|
|
$allAvailableVersions = @()
|
|
if ($platformNode.OS) {
|
|
foreach ($osNode in $platformNode.OS) {
|
|
$osRel = if ($osNode.IsWindows11 -contains 'true') { 11 } else { 10 }
|
|
$relIDs = $osNode.OSReleaseIdFileName -replace 'H', 'h' -split ' '
|
|
foreach ($id in $relIDs) { $allAvailableVersions += "Win$($osRel) $($id)" }
|
|
}
|
|
}
|
|
$availableVersionsString = ($allAvailableVersions | Select-Object -Unique) -join ', '
|
|
if ([string]::IsNullOrWhiteSpace($availableVersionsString)) { $availableVersionsString = "None" }
|
|
throw "Could not find any suitable OS driver pack for model '$modelName' matching requested or fallback versions (Win$($WindowsRelease) $WindowsVersion). Available: $availableVersionsString"
|
|
}
|
|
|
|
$osReleaseIdFileName = $selectedOSNode.OSReleaseIdFileName -replace 'H', 'h'
|
|
WriteLog "Using SystemID: $systemID and OS Info: Win$($selectedOSRelease) ($($selectedOSVersion)) for '$modelName'"
|
|
$archSuffix = $WindowsArch -replace "^x", ""
|
|
$modelRelease = "$($systemID)_$($archSuffix)_$($selectedOSRelease).0.$($selectedOSVersion.ToLower())"
|
|
$driverCabUrl = "https://hpia.hpcloud.hp.com/ref/$systemID/$modelRelease.cab"
|
|
$driverCabFile = Join-Path -Path $hpDriversBaseFolder -ChildPath "$modelRelease.cab" # Store in base HP folder
|
|
$driverXmlFile = Join-Path -Path $hpDriversBaseFolder -ChildPath "$modelRelease.xml" # Store in base HP folder
|
|
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Downloading driver index..." }
|
|
WriteLog "Downloading HP Driver cab from $driverCabUrl to $driverCabFile"
|
|
Start-BitsTransferWithRetry -Source $driverCabUrl -Destination $driverCabFile -ErrorAction Stop
|
|
WriteLog "Expanding HP Driver cab $driverCabFile to $driverXmlFile"
|
|
if (Test-Path -Path $driverXmlFile) { Remove-Item -Path $driverXmlFile -Force }
|
|
Invoke-Process -FilePath "expand.exe" -ArgumentList @("`"$driverCabFile`"", "`"$driverXmlFile`"") -ErrorAction Stop | Out-Null
|
|
|
|
WriteLog "Parsing driver XML $driverXmlFile"
|
|
[xml]$driverXmlContent = Get-Content -Path $driverXmlFile -Raw -Encoding UTF8 -ErrorAction Stop
|
|
$updates = $driverXmlContent.ImagePal.Solutions.UpdateInfo | Where-Object { $_.Category -match '^Driver' }
|
|
$totalDrivers = ($updates | Measure-Object).Count
|
|
$downloadedCount = 0
|
|
WriteLog "Found $totalDrivers driver updates for $modelName."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Found $totalDrivers drivers. Downloading..." }
|
|
|
|
if (-not (Test-Path -Path $modelSpecificFolder)) {
|
|
New-Item -Path $modelSpecificFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
|
}
|
|
|
|
foreach ($update in $updates) {
|
|
$driverName = $update.Name -replace '[\\/:"*?<>|]', '_'
|
|
$category = $update.Category -replace '[\\/:"*?<>|]', '_'
|
|
$version = $update.Version -replace '[\\/:"*?<>|]', '_'
|
|
$driverUrl = "https://$($update.URL)"
|
|
$driverFileName = Split-Path -Path $driverUrl -Leaf
|
|
$downloadFolder = Join-Path -Path $modelSpecificFolder -ChildPath $category
|
|
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driverFileName
|
|
$extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverName + "_" + $version + "_" + ($driverFileName -replace '\.exe$', ''))
|
|
|
|
$downloadedCount++
|
|
$progressMsg = "($downloadedCount/$totalDrivers) Downloading $driverName..."
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $progressMsg }
|
|
WriteLog "$progressMsg URL: $driverUrl"
|
|
|
|
if (Test-Path -Path $extractFolder) {
|
|
WriteLog "Driver already extracted to $extractFolder, skipping download."
|
|
continue
|
|
}
|
|
if (-not (Test-Path -Path $downloadFolder)) {
|
|
New-Item -Path $downloadFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
|
}
|
|
WriteLog "Downloading driver to: $driverFilePath"
|
|
Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath -ErrorAction Stop
|
|
WriteLog "Driver downloaded: $driverFilePath"
|
|
WriteLog "Creating extraction folder: $extractFolder"
|
|
New-Item -Path $extractFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
|
$arguments = "/s /e /f `"$extractFolder`""
|
|
WriteLog "Extracting driver $driverFilePath with args: $arguments"
|
|
#DEBUG
|
|
# wrap $driverFilePath in quotes to handle spaces
|
|
# $driverFilePath = "`"$driverFilePath`""
|
|
WriteLog "Running HP Driver Extraction Command: $driverFilePath $arguments"
|
|
Invoke-Process -FilePath $driverFilePath -ArgumentList $arguments -ErrorAction Stop | Out-Null
|
|
# Start-Process -FilePath $driverFilePath -ArgumentList $arguments -Wait -NoNewWindow -ErrorAction Stop | Out-Null
|
|
WriteLog "Driver extracted to: $extractFolder"
|
|
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
|
|
WriteLog "Deleted driver installer: $driverFilePath"
|
|
}
|
|
|
|
Remove-Item -Path $driverCabFile, $driverXmlFile -Force -ErrorAction SilentlyContinue
|
|
WriteLog "Cleaned up driver cab and xml files for $modelName"
|
|
|
|
$finalStatus = "Completed"
|
|
if ($CompressToWim) {
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing..." }
|
|
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim"
|
|
WriteLog "Compressing '$modelSpecificFolder' to '$wimFilePath'..."
|
|
try {
|
|
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
|
|
WriteLog "Compression successful for '$identifier'."
|
|
$finalStatus = "Completed & Compressed"
|
|
}
|
|
catch {
|
|
WriteLog "Error during compression for '$identifier': $($_.Exception.Message)"
|
|
$finalStatus = "Completed (Compression Failed)"
|
|
}
|
|
}
|
|
$successState = $true
|
|
}
|
|
catch {
|
|
$errorMessage = "Error saving HP drivers for $($modelName): $($_.Exception.Message)"
|
|
WriteLog $errorMessage
|
|
$finalStatus = "Error: $($_.Exception.Message.Split([Environment]::NewLine)[0])"
|
|
$successState = $false
|
|
if (Test-Path -Path $modelSpecificFolder -PathType Container) {
|
|
WriteLog "Attempting to remove partially created folder $modelSpecificFolder due to error."
|
|
Remove-Item -Path $modelSpecificFolder -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $finalStatus }
|
|
return [PSCustomObject]@{ Identifier = $identifier; Status = $finalStatus; Success = $successState }
|
|
}
|
|
# Function to update status of a specific item in a ListView
|
|
function Update-ListViewItemStatus {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object]$WindowObject, # Changed type to [object]
|
|
[Parameter(Mandatory)]
|
|
[object]$ListView, # Changed type to [object]
|
|
[Parameter(Mandatory)]
|
|
[string]$IdentifierProperty,
|
|
[Parameter(Mandatory)]
|
|
[string]$IdentifierValue,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusProperty,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusValue
|
|
)
|
|
|
|
# Ensure we are in UI mode and objects are of correct WPF types
|
|
if ($WindowObject -is [System.Windows.Window] -and $ListView -is [System.Windows.Controls.ListView]) {
|
|
# Directly update UI elements as this function is now called on the UI thread
|
|
try {
|
|
$itemToUpdate = $ListView.Items | Where-Object { $_.$IdentifierProperty -eq $IdentifierValue } | Select-Object -First 1
|
|
if ($null -ne $itemToUpdate) {
|
|
$itemToUpdate.$StatusProperty = $StatusValue
|
|
$ListView.Items.Refresh() # Refresh the view to show the change
|
|
}
|
|
else {
|
|
# Log if item not found (for debugging)
|
|
WriteLog "Update-ListViewItemStatus: Item with $IdentifierProperty '$IdentifierValue' not found in ListView."
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Update-ListViewItemStatus: Error updating ListView: $($_.Exception.Message)"
|
|
}
|
|
} # End of if ($WindowObject -is [System.Windows.Window]...)
|
|
else {
|
|
# Log if called in non-UI mode or with incorrect types (should not happen if Invoke-ParallelProcessing $isUiMode is correct)
|
|
WriteLog "Update-ListViewItemStatus: Skipped UI update for $IdentifierValue due to non-UI mode or incorrect object types."
|
|
}
|
|
}
|
|
|
|
# Function to update overall progress bar and status text label
|
|
function Update-OverallProgress {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object]$WindowObject, # Changed type to [object]
|
|
[Parameter(Mandatory)]
|
|
[int]$CompletedCount,
|
|
[Parameter(Mandatory)]
|
|
[int]$TotalCount,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusText,
|
|
[Parameter(Mandatory)]
|
|
[string]$ProgressBarName,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusLabelName
|
|
)
|
|
|
|
# Ensure we are in UI mode and WindowObject is of correct WPF type
|
|
if ($WindowObject -is [System.Windows.Window]) {
|
|
# Directly update UI elements as this function is now called on the UI thread
|
|
try {
|
|
# Find controls by name using the $WindowObject
|
|
$pb = $WindowObject.FindName($ProgressBarName)
|
|
$lbl = $WindowObject.FindName($StatusLabelName)
|
|
|
|
if ($null -eq $pb) {
|
|
WriteLog "Update-OverallProgress: ProgressBar '$ProgressBarName' not found."
|
|
return
|
|
}
|
|
if ($null -eq $lbl) {
|
|
WriteLog "Update-OverallProgress: StatusLabel '$StatusLabelName' not found."
|
|
return
|
|
}
|
|
|
|
# Update the progress bar
|
|
if ($TotalCount -gt 0) {
|
|
$percentComplete = ($CompletedCount / $TotalCount) * 100
|
|
$pb.Value = $percentComplete
|
|
}
|
|
else {
|
|
$pb.Value = 0
|
|
}
|
|
|
|
# Update the status label
|
|
$lbl.Text = $StatusText
|
|
|
|
}
|
|
catch {
|
|
WriteLog "Update-OverallProgress: Error updating progress: $($_.Exception.Message)"
|
|
}
|
|
} # End of if ($WindowObject -is [System.Windows.Window])
|
|
else {
|
|
# Log if called in non-UI mode or with incorrect types
|
|
WriteLog "Update-OverallProgress: Skipped UI update ($StatusText) due to non-UI mode or incorrect WindowObject type."
|
|
}
|
|
}
|
|
|
|
# Reusable function to invoke parallel processing with UI updates
|
|
function Invoke-ParallelProcessing {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$ItemsToProcess,
|
|
[Parameter(Mandatory = $false)]
|
|
[object]$ListViewControl = $null, # Changed type to [object]
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$IdentifierProperty = 'Identifier',
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$StatusProperty = 'Status',
|
|
[Parameter(Mandatory)]
|
|
[ValidateSet('WingetDownload', 'CopyBYO', 'DownloadDriverByMake')]
|
|
[string]$TaskType,
|
|
[Parameter()]
|
|
[hashtable]$TaskArguments = @{},
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$CompletedStatusText = "Completed",
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$ErrorStatusPrefix = "Error: ",
|
|
[Parameter(Mandatory = $false)]
|
|
[object]$WindowObject = $null, # Changed type to [object]
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$MainThreadLogPath = $null # New parameter for the log path
|
|
)
|
|
# Check if running in UI mode by verifying the types of the passed objects
|
|
$isUiMode = ($null -ne $WindowObject -and $WindowObject -is [System.Windows.Window] -and $null -ne $ListViewControl -and $ListViewControl -is [System.Windows.Controls.ListView])
|
|
|
|
if ($isUiMode) {
|
|
WriteLog "Invoke-ParallelProcessing started for $($ItemsToProcess.Count) items in ListView '$($ListViewControl.Name)'."
|
|
}
|
|
else {
|
|
WriteLog "Invoke-ParallelProcessing started for $($ItemsToProcess.Count) items (non-UI mode)."
|
|
}
|
|
$resultsCollection = [System.Collections.Generic.List[object]]::new()
|
|
$jobs = @()
|
|
$results = @() # Store results from jobs
|
|
$totalItems = $ItemsToProcess.Count
|
|
$processedCount = 0
|
|
|
|
# Create a thread-safe queue for intermediate progress updates
|
|
$progressQueue = New-Object System.Collections.Concurrent.ConcurrentQueue[hashtable]
|
|
|
|
# Define common paths locally within this function's scope
|
|
$coreModulePath = $MyInvocation.MyCommand.Module.Path
|
|
$coreModuleDirectory = Split-Path -Path $coreModulePath -Parent
|
|
$ffuDevelopmentRoot = Split-Path -Path $coreModuleDirectory -Parent
|
|
|
|
# Paths to other modules needed by the parallel threads
|
|
$commonCoreModulePathForJob = Join-Path -Path $ffuDevelopmentRoot -ChildPath "common\FFU.Common.Core.psm1"
|
|
$commonWingetModulePathForJob = Join-Path -Path $ffuDevelopmentRoot -ChildPath "common\FFU.Common.Winget.psm1"
|
|
$commonDriversModulePathForJob = Join-Path -Path $ffuDevelopmentRoot -ChildPath "common\FFU.Common.Drivers.psm1"
|
|
|
|
# Use the explicitly passed MainThreadLogPath for the parallel jobs.
|
|
# If not provided (e.g., older calls or direct module use without this param), it might be null.
|
|
# The parallel job's Set-CommonCoreLogPath will handle null/empty paths by warning.
|
|
$currentLogFilePathForJob = $MainThreadLogPath
|
|
|
|
$jobScopeVariables = $TaskArguments.Clone()
|
|
$jobScopeVariables['_thisCoreModulePath'] = $coreModulePath # Path to FFUUI.Core.psm1 itself
|
|
$jobScopeVariables['_commonCoreModulePath'] = $commonCoreModulePathForJob
|
|
$jobScopeVariables['_commonWingetModulePath'] = $commonWingetModulePathForJob
|
|
$jobScopeVariables['_commonDriversModulePath'] = $commonDriversModulePathForJob
|
|
$jobScopeVariables['_currentLogFilePathForJob'] = $currentLogFilePathForJob # Pass the determined log path
|
|
$jobScopeVariables['_progressQueue'] = $progressQueue
|
|
|
|
# The $TaskScriptBlock parameter is already a local variable in this scope
|
|
|
|
# Initial UI update needs to happen *before* starting the jobs
|
|
# Update all items to a static "Processing..." status
|
|
if ($isUiMode) {
|
|
# Use the new $isUiMode flag
|
|
foreach ($item in $ItemsToProcess) {
|
|
$identifierValue = $item.$IdentifierProperty
|
|
$initialStaticStatus = "Queued..."
|
|
try {
|
|
# Update the UI on the main thread to show the item is being queued for processing
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action] {
|
|
Update-ListViewItemStatus -WindowObject $WindowObject -ListView $ListViewControl -IdentifierProperty $IdentifierProperty -IdentifierValue $identifierValue -StatusProperty $StatusProperty -StatusValue $initialStaticStatus
|
|
})
|
|
}
|
|
catch {
|
|
WriteLog "Error setting initial status for item '$identifierValue': $($_.Exception.Message)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Queue items and start jobs using the pipeline and $using:
|
|
try {
|
|
# $jobScopeVariables and $TaskType are local here
|
|
# Inside the -Parallel scriptblock, we access them with $using:
|
|
$jobs = $ItemsToProcess | ForEach-Object -Parallel {
|
|
# Access the current item via pipeline variable $_
|
|
$currentItem = $_
|
|
# Access the combined arguments hashtable from the calling scope using $using:
|
|
$localJobArgs = $using:jobScopeVariables
|
|
# Access the task type string from the calling scope using $using:
|
|
$localTaskType = $using:TaskType
|
|
# Access the progress queue using $using:
|
|
$localProgressQueue = $localJobArgs['_progressQueue']
|
|
|
|
# Initialize result hashtable
|
|
$taskResult = $null
|
|
$resultIdentifier = $null
|
|
$resultStatus = "Error: Task type '$localTaskType' not recognized"
|
|
$resultCode = 1 # Default to error
|
|
|
|
try {
|
|
# Import the common core module first
|
|
Import-Module $localJobArgs['_commonCoreModulePath']
|
|
# Set the log path for this parallel thread
|
|
Set-CommonCoreLogPath -Path $localJobArgs['_currentLogFilePathForJob']
|
|
|
|
# Set other global variables if tasks rely on them (prefer passing as parameters)
|
|
$global:AppsPath = $localJobArgs['AppsPath']
|
|
$global:WindowsArch = $localJobArgs['WindowsArch']
|
|
if ($localJobArgs.ContainsKey('OrchestrationPath')) {
|
|
$global:OrchestrationPath = $localJobArgs['OrchestrationPath']
|
|
}
|
|
|
|
# Import other necessary modules. Their WriteLog calls will use the path set above.
|
|
Import-Module $localJobArgs['_thisCoreModulePath'] # FFUUI.Core.psm1
|
|
Import-Module $localJobArgs['_commonWingetModulePath']
|
|
Import-Module $localJobArgs['_commonDriversModulePath']
|
|
|
|
# Execute the appropriate background task based on $localTaskType
|
|
switch ($localTaskType) {
|
|
'WingetDownload' {
|
|
# Pass the progress queue to the task function
|
|
$taskResult = Start-WingetAppDownloadTask -ApplicationItemData $currentItem `
|
|
-AppListJsonPath $localJobArgs['AppListJsonPath'] `
|
|
-AppsPath $localJobArgs['AppsPath'] `
|
|
-WindowsArch $localJobArgs['WindowsArch'] `
|
|
-OrchestrationPath $localJobArgs['OrchestrationPath'] `
|
|
-ProgressQueue $localProgressQueue
|
|
if ($null -ne $taskResult) {
|
|
$resultIdentifier = $taskResult.Id
|
|
$resultStatus = $taskResult.Status
|
|
$resultCode = $taskResult.ResultCode
|
|
}
|
|
else {
|
|
$resultIdentifier = $currentItem.Id # Fallback
|
|
$resultStatus = "Error: WingetDownload task returned null"
|
|
$resultCode = 1
|
|
WriteLog $resultStatus
|
|
}
|
|
}
|
|
'CopyBYO' {
|
|
# Pass the progress queue to the task function
|
|
$taskResult = Start-CopyBYOApplicationTask -ApplicationItemData $currentItem `
|
|
-AppsPath $localJobArgs['AppsPath'] `
|
|
-ProgressQueue $localProgressQueue
|
|
if ($null -ne $taskResult) {
|
|
$resultIdentifier = $taskResult.Name
|
|
$resultStatus = $taskResult.Status
|
|
$resultCode = if ($taskResult.Success) { 0 } else { 1 }
|
|
}
|
|
else {
|
|
$resultIdentifier = $currentItem.Name # Fallback
|
|
$resultStatus = "Error: CopyBYO task returned null"
|
|
$resultCode = 1
|
|
WriteLog $resultStatus
|
|
}
|
|
}
|
|
'DownloadDriverByMake' {
|
|
$make = $currentItem.Make
|
|
# Ensure $resultIdentifier is set before the switch, using the main IdentifierProperty
|
|
# This is crucial if a Make is unsupported or a task fails to return a result.
|
|
$resultIdentifier = $currentItem.$($using:IdentifierProperty)
|
|
|
|
switch ($make) {
|
|
'Microsoft' {
|
|
$taskResult = Save-MicrosoftDriversTask -DriverItemData $currentItem `
|
|
-DriversFolder $localJobArgs['DriversFolder'] `
|
|
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
|
-Headers $localJobArgs['Headers'] `
|
|
-UserAgent $localJobArgs['UserAgent'] `
|
|
-ProgressQueue $localProgressQueue `
|
|
-CompressToWim $localJobArgs['CompressToWim']
|
|
}
|
|
'Dell' {
|
|
# DellCatalogXmlPath might be null if catalog prep failed; Save-DellDriversTask should handle this.
|
|
$taskResult = Save-DellDriversTask -DriverItemData $currentItem `
|
|
-DriversFolder $localJobArgs['DriversFolder'] `
|
|
-WindowsArch $localJobArgs['WindowsArch'] `
|
|
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
|
-DellCatalogXmlPath $localJobArgs['DellCatalogXmlPath'] `
|
|
-ProgressQueue $localProgressQueue `
|
|
-CompressToWim $localJobArgs['CompressToWim']
|
|
}
|
|
'HP' {
|
|
$taskResult = Save-HPDriversTask -DriverItemData $currentItem `
|
|
-DriversFolder $localJobArgs['DriversFolder'] `
|
|
-WindowsArch $localJobArgs['WindowsArch'] `
|
|
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
|
-WindowsVersion $localJobArgs['WindowsVersion'] `
|
|
-ProgressQueue $localProgressQueue `
|
|
-CompressToWim $localJobArgs['CompressToWim']
|
|
}
|
|
'Lenovo' {
|
|
$taskResult = Save-LenovoDriversTask -DriverItemData $currentItem `
|
|
-DriversFolder $localJobArgs['DriversFolder'] `
|
|
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
|
-Headers $localJobArgs['Headers'] `
|
|
-UserAgent $localJobArgs['UserAgent'] `
|
|
-ProgressQueue $localProgressQueue `
|
|
-CompressToWim $localJobArgs['CompressToWim']
|
|
}
|
|
default {
|
|
$unsupportedMakeMessage = "Error: Unsupported Make '$make' for driver download."
|
|
WriteLog $unsupportedMakeMessage
|
|
$resultStatus = $unsupportedMakeMessage
|
|
$resultCode = 1
|
|
# $resultIdentifier is already set from $currentItem.$($using:IdentifierProperty)
|
|
$localProgressQueue.Enqueue(@{ Identifier = $resultIdentifier; Status = $resultStatus })
|
|
# $taskResult remains null, handled below
|
|
}
|
|
}
|
|
|
|
# Consolidate result handling for 'DownloadDriverByMake'
|
|
if ($null -ne $taskResult) {
|
|
# $resultIdentifier is already $currentItem.$($using:IdentifierProperty)
|
|
# We use the task's returned Model/Identifier for logging/status if needed,
|
|
# but the primary identifier for UI updates should be consistent.
|
|
$taskSpecificIdentifier = $null
|
|
if ($taskResult.PSObject.Properties.Name -contains 'Model') { $taskSpecificIdentifier = $taskResult.Model }
|
|
elseif ($taskResult.PSObject.Properties.Name -contains 'Identifier') { $taskSpecificIdentifier = $taskResult.Identifier }
|
|
|
|
$resultStatus = $taskResult.Status
|
|
if ($taskResult.PSObject.Properties.Name -contains 'Success') {
|
|
# Dell, Microsoft, Lenovo
|
|
$resultCode = if ($taskResult.Success) { 0 } else { 1 }
|
|
}
|
|
elseif ($taskResult.Status -like 'Completed*') {
|
|
# HP success
|
|
$resultCode = 0
|
|
}
|
|
elseif ($taskResult.Status -like 'Error*') {
|
|
# HP error
|
|
$resultCode = 1
|
|
}
|
|
else {
|
|
# Default for HP if status is unexpected, or if 'Success' property is missing but status isn't 'Completed*' or 'Error*'
|
|
WriteLog "Unexpected status or missing 'Success' property from task for '$taskSpecificIdentifier': $($taskResult.Status)"
|
|
$resultCode = 1 # Assume error
|
|
}
|
|
}
|
|
elseif ($make -in ('Microsoft', 'Dell', 'HP', 'Lenovo')) {
|
|
# This means a specific Make case was hit, but $taskResult was unexpectedly null
|
|
$nullTaskResultMessage = "Error: Task for Make '$make' returned null."
|
|
WriteLog $nullTaskResultMessage
|
|
$resultStatus = $nullTaskResultMessage
|
|
$resultCode = 1
|
|
# $resultIdentifier is already set
|
|
}
|
|
# If it was an unsupported Make, $resultStatus and $resultCode are already set from the 'default' case.
|
|
}
|
|
Default {
|
|
# This handles unknown $localTaskType values
|
|
$resultStatus = "Error: Task type '$localTaskType' not recognized"
|
|
$resultCode = 1
|
|
if ($currentItem -is [pscustomobject] -and $currentItem.PSObject.Properties.Name -match $using:IdentifierProperty) {
|
|
$resultIdentifier = $currentItem.$($using:IdentifierProperty)
|
|
}
|
|
else {
|
|
$resultIdentifier = "UnknownItem"
|
|
}
|
|
WriteLog "Error in parallel job: Unknown TaskType '$localTaskType' provided for item '$resultIdentifier'."
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
# Catch errors within the parallel task execution
|
|
$resultStatus = "Error: $($_.Exception.Message)"
|
|
$resultCode = 1
|
|
# Try to get an identifier
|
|
if ($currentItem -is [pscustomobject] -and $currentItem.PSObject.Properties.Name -match $using:IdentifierProperty) {
|
|
$resultIdentifier = $currentItem.$($using:IdentifierProperty)
|
|
}
|
|
else {
|
|
$resultIdentifier = "UnknownItemOnError"
|
|
}
|
|
WriteLog "Exception during parallel task '$localTaskType' for item '$resultIdentifier': $($_.Exception.ToString())"
|
|
# Enqueue the error status from the catch block
|
|
$localProgressQueue.Enqueue(@{ Identifier = $resultIdentifier; Status = $resultStatus })
|
|
}
|
|
|
|
# Return a consistent hashtable structure (final result)
|
|
return @{
|
|
Identifier = $resultIdentifier
|
|
Status = $resultStatus # Return the final status
|
|
ResultCode = $resultCode
|
|
}
|
|
|
|
} -ThrottleLimit 5 -AsJob
|
|
}
|
|
catch {
|
|
# Catch errors during the *creation* of the parallel jobs (e.g., module loading in main thread failed)
|
|
WriteLog "Error initiating ForEach-Object -Parallel: $($_.Exception.Message)"
|
|
# Update all items to show a general startup error
|
|
$errorStatus = "$ErrorStatusPrefix Failed to start processing"
|
|
foreach ($item in $ItemsToProcess) {
|
|
$identifier = $item.$IdentifierProperty
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action] { # Use $WindowObject
|
|
Update-ListViewItemStatus -WindowObject $WindowObject -ListView $ListViewControl -IdentifierProperty $IdentifierProperty -IdentifierValue $identifier -StatusProperty $StatusProperty -StatusValue $errorStatus # Pass $WindowObject
|
|
})
|
|
}
|
|
# Exit the function as processing cannot proceed
|
|
return
|
|
}
|
|
|
|
# Check if any jobs failed to start immediately (e.g., module loading issues within the job)
|
|
$failedJobs = $jobs | Where-Object { $_.State -eq 'Failed' -and $_.JobStateInfo.Reason }
|
|
foreach ($failedJob in $failedJobs) {
|
|
WriteLog "Job $($failedJob.Id) failed to start or failed early: $($failedJob.JobStateInfo.Reason)"
|
|
# We don't easily know which item failed here without more complex mapping
|
|
# Update overall status maybe?
|
|
$processedCount++
|
|
}
|
|
# Filter out jobs that failed immediately
|
|
$jobs = $jobs | Where-Object { $_.State -ne 'Failed' }
|
|
|
|
# Process job results and intermediate status updates without blocking the UI thread
|
|
while ($jobs.Count -gt 0 -or -not $progressQueue.IsEmpty) {
|
|
# Continue while jobs are running OR queue has messages
|
|
|
|
# 1. Process intermediate status updates from the queue
|
|
$statusUpdate = $null
|
|
while ($progressQueue.TryDequeue([ref]$statusUpdate)) {
|
|
if ($null -ne $statusUpdate) {
|
|
$intermediateIdentifier = $statusUpdate.Identifier
|
|
$intermediateStatus = $statusUpdate.Status
|
|
if ($isUiMode) {
|
|
# Use the new $isUiMode flag
|
|
# Update the UI with the intermediate status
|
|
try {
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action] {
|
|
Update-ListViewItemStatus -WindowObject $WindowObject -ListView $ListViewControl -IdentifierProperty $IdentifierProperty -IdentifierValue $intermediateIdentifier -StatusProperty $StatusProperty -StatusValue $intermediateStatus
|
|
})
|
|
}
|
|
catch {
|
|
WriteLog "Error setting intermediate status for item '$intermediateIdentifier': $($_.Exception.Message)"
|
|
}
|
|
}
|
|
else {
|
|
# Log intermediate status if not in UI mode
|
|
WriteLog "Intermediate Status for '$intermediateIdentifier': $intermediateStatus"
|
|
}
|
|
}
|
|
}
|
|
|
|
# 2. Check for completed jobs
|
|
$completedJobs = $jobs | Where-Object { $_.State -in 'Completed', 'Failed', 'Stopped' }
|
|
|
|
if ($completedJobs) {
|
|
foreach ($completedJob in $completedJobs) {
|
|
$finalIdentifier = "UnknownJob" # Placeholder if we can't get result
|
|
$finalStatus = "$ErrorStatusPrefix Job $($completedJob.Id) ended unexpectedly"
|
|
$finalResultCode = 1 # Assume error
|
|
|
|
if ($completedJob.State -eq 'Failed') {
|
|
WriteLog "Job $($completedJob.Id) failed: $($completedJob.Error)"
|
|
# Try to get identifier from job name if possible (less reliable)
|
|
# $finalIdentifier = ... logic to parse job name or map ID ...
|
|
$finalStatus = "$ErrorStatusPrefix Job Failed"
|
|
$processedCount++ # Count failed job as processed
|
|
}
|
|
elseif ($completedJob.HasMoreData) {
|
|
# Receive final results specifically from the completed job
|
|
$jobResults = $completedJob | Receive-Job
|
|
foreach ($result in $jobResults) {
|
|
# Should only be one result per job in this setup
|
|
if ($null -ne $result -and $result -is [hashtable] -and $result.ContainsKey('Identifier')) {
|
|
$finalIdentifier = $result.Identifier
|
|
$status = $result.Status # This is the FINAL status returned by the task
|
|
$finalResultCode = $result.ResultCode
|
|
|
|
# Determine final status text based on the result code
|
|
if ($finalResultCode -eq 0) {
|
|
# Assuming 0 means success
|
|
# Use the specific status returned by the successful job
|
|
# This handles cases like "Already downloaded" correctly
|
|
$finalStatus = $status
|
|
}
|
|
else {
|
|
$finalStatus = "$($ErrorStatusPrefix)$($status)" # Use status from result for error message
|
|
}
|
|
$processedCount++
|
|
}
|
|
else {
|
|
WriteLog "Warning: Received unexpected final job result format: $($result | Out-String)"
|
|
$finalStatus = "$ErrorStatusPrefix Invalid Result Format"
|
|
$processedCount++ # Count as processed to avoid loop issues
|
|
}
|
|
# Add the received result (even if format was unexpected, for logging)
|
|
if ($null -ne $result) { $resultsCollection.Add($result) }
|
|
break # Only process first result from this job
|
|
}
|
|
}
|
|
else {
|
|
# Job completed but had no data
|
|
if ($completedJob.State -ne 'Failed') {
|
|
WriteLog "Job $($completedJob.Id) completed with state '$($completedJob.State)' but had no data."
|
|
# $finalIdentifier = ... logic to parse job name or map ID ...
|
|
$finalStatus = "$ErrorStatusPrefix No Result Data"
|
|
$processedCount++
|
|
}
|
|
# If it was 'Failed', it was handled above
|
|
}
|
|
|
|
# Update the specific item in the ListView with its FINAL status
|
|
if ($isUiMode) {
|
|
# Use the new $isUiMode flag
|
|
try {
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action] {
|
|
Update-ListViewItemStatus -WindowObject $WindowObject -ListView $ListViewControl -IdentifierProperty $IdentifierProperty -IdentifierValue $finalIdentifier -StatusProperty $StatusProperty -StatusValue $finalStatus
|
|
})
|
|
}
|
|
catch {
|
|
WriteLog "Error setting FINAL status for item '$finalIdentifier': $($_.Exception.Message)"
|
|
}
|
|
|
|
# Update overall progress after processing a job's results
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action] {
|
|
Update-OverallProgress -WindowObject $WindowObject -CompletedCount $processedCount -TotalCount $totalItems -StatusText "Processed $processedCount of $totalItems..." -ProgressBarName "progressBar" -StatusLabelName "txtStatus"
|
|
})
|
|
}
|
|
else {
|
|
# Log final status if not in UI mode
|
|
WriteLog "Final Status for '$finalIdentifier': $finalStatus (ResultCode: $finalResultCode)"
|
|
}
|
|
|
|
# Remove the completed/failed job from the list and clean it up
|
|
$jobs = $jobs | Where-Object { $_.Id -ne $completedJob.Id }
|
|
Remove-Job -Job $completedJob -Force -ErrorAction SilentlyContinue
|
|
} # End foreach completedJob
|
|
} # End if ($completedJobs)
|
|
|
|
# 3. Allow UI events to process and sleep briefly
|
|
if ($isUiMode) {
|
|
# Use the new $isUiMode flag
|
|
# Only sleep if jobs are still running AND the queue is empty (to avoid delaying UI updates)
|
|
if ($jobs.Count -gt 0 -and $progressQueue.IsEmpty) {
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action] { }) | Out-Null
|
|
Start-Sleep -Milliseconds 100
|
|
}
|
|
elseif (-not $progressQueue.IsEmpty) {
|
|
# If queue has messages, process them immediately without sleeping
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action] { }) | Out-Null
|
|
}
|
|
}
|
|
else {
|
|
# Non-UI mode, just sleep if jobs are running
|
|
if ($jobs.Count -gt 0) {
|
|
Start-Sleep -Milliseconds 100
|
|
}
|
|
}
|
|
# If jobs are done AND queue is empty, the loop condition will terminate
|
|
|
|
} # End while ($jobs.Count -gt 0 -or -not $progressQueue.IsEmpty)
|
|
|
|
# Final cleanup of any remaining jobs (shouldn't be necessary with this loop logic, but good practice)
|
|
if ($jobs.Count -gt 0) {
|
|
WriteLog "Cleaning up $($jobs.Count) remaining jobs after loop exit."
|
|
Remove-Job -Job $jobs -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
if ($isUiMode) {
|
|
# Use the new $isUiMode flag
|
|
WriteLog "Invoke-ParallelProcessing finished for ListView '$($ListViewControl.Name)'."
|
|
# Final overall progress update
|
|
$WindowObject.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action] {
|
|
Update-OverallProgress -WindowObject $WindowObject -CompletedCount $processedCount -TotalCount $totalItems -StatusText "Processing complete. Processed $processedCount of $totalItems." -ProgressBarName "progressBar" -StatusLabelName "txtStatus"
|
|
})
|
|
}
|
|
else {
|
|
WriteLog "Invoke-ParallelProcessing finished (non-UI mode). Processed $processedCount of $totalItems."
|
|
}
|
|
|
|
# Return all collected final results from jobs
|
|
return $resultsCollection
|
|
}
|
|
# --------------------------------------------------------------------------
|
|
# SECTION: Module Export
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Export only the functions intended for public use by the UI script
|
|
Export-ModuleMember -Function Get-VMSwitchData,
|
|
Get-WindowsSettingsDefaults,
|
|
Get-AvailableWindowsReleases,
|
|
Get-AvailableWindowsVersions,
|
|
Get-GeneralDefaults,
|
|
Get-DellDriversModelList,
|
|
Get-HPDriversModelList,
|
|
Get-MicrosoftDriversModelList,
|
|
Get-LenovoDriversModelList,
|
|
Get-USBDrives,
|
|
Show-ModernFolderPicker,
|
|
Test-WingetCLI,
|
|
Install-WingetComponents,
|
|
Confirm-WingetInstallationUI,
|
|
Search-WingetPackagesPublic,
|
|
Start-WingetAppDownloadTask,
|
|
Start-CopyBYOApplicationTask,
|
|
Save-MicrosoftDriversTask,
|
|
Save-DellDriversTask,
|
|
Save-HPDriversTask,
|
|
Save-LenovoDriversTask,
|
|
Invoke-ProgressUpdate,
|
|
Invoke-ParallelProcessing,
|
|
Update-ListViewItemStatus,
|
|
Update-OverallProgress,
|
|
Compress-DriverFolderToWim
|