Files
FFU/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
T
rbalsleyMSFT 7babad8262 Refactor driver management into dedicated modules
Relocates driver-specific download, parsing, and management logic from the main UI script and the FFUUI.Core module into new, dedicated modules for each manufacturer (Dell, HP, Lenovo, Microsoft). This improves modularity and code organization.

Additionally, centralizes common HTTP headers and user agent strings in the FFUUI.Core module, accessible via a new helper function.
2025-06-12 15:47:46 -07:00

1451 lines
74 KiB
PowerShell

# FFU UI Core Logic Module
# Contains non-UI specific helper functions, data retrieval, and core processing logic.
#Requires -Modules BitsTransfer
# --------------------------------------------------------------------------
# SECTION: Module Variables (Static Data & State)
# --------------------------------------------------------------------------
#Microsoft sites will intermittently fail on downloads. These headers and user agent are to help with that.
$script:Headers = @{
"Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
"Accept-Encoding" = "gzip, deflate, br, zstd"
"Accept-Language" = "en-US,en;q=0.9"
"Priority" = "u=0, i"
"Sec-Ch-Ua" = "`"Microsoft Edge`";v=`"125`", `"Chromium`";v=`"125`", `"Not.A/Brand`";v=`"24`""
"Sec-Ch-Ua-Mobile" = "?0"
"Sec-Ch-Ua-Platform" = "`"Windows`""
"Sec-Fetch-Dest" = "document"
"Sec-Fetch-Mode" = "navigate"
"Sec-Fetch-Site" = "none"
"Sec-Fetch-User" = "?1"
"Upgrade-Insecure-Requests" = "1"
}
$script:UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0'
$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',
'Enterprise 2016 LTSB',
'Enterprise N 2016 LTSB',
'Enterprise LTSC',
'Enterprise N LTSC',
'IoT Enterprise LTSC',
'IoT Enterprise N LTSC',
'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 },
[PSCustomObject]@{ Display = "Windows 10 LTSB 2016"; Value = 2016 }, # Changed Value from 1607
[PSCustomObject]@{ Display = "Windows 10 LTSC 2019"; Value = 2019 }, # Changed Value from 1809
[PSCustomObject]@{ Display = "Windows 10 LTSC 2021"; Value = 2021 },
[PSCustomObject]@{ Display = "Windows 10 LTSC 2024"; Value = 2024 }
)
$script:mctWindowsReleases = @(
[PSCustomObject]@{ Display = "Windows 10"; Value = 10 },
[PSCustomObject]@{ Display = "Windows 11"; Value = 11 }
)
$script:windowsVersionMap = @{
10 = @("22H2")
11 = @("22H2", "23H2", "24H2")
2016 = @("1607") # Windows 10 LTSB 2016 & Server 2016
2019 = @("1809") # Windows 10 LTSC 2019 & Server 2019
# Note: Server 2016 and LTSB 2016 now share the key 2016, mapping to version "1607"
# Note: Server 2019 and LTSC 2019 now share the key 2019, mapping to version "1809"
2021 = @("21H2") # LTSC 2021
2022 = @("21H2") # Server 2022
2024 = @("24H2") # LTSC 2024
2025 = @("24H2") # Server 2025
}
# SKU Groups
$script:clientSKUs = @(
'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'
)
$script:serverSKUs = @(
'Standard',
'Standard (Desktop Experience)',
'Datacenter',
'Datacenter (Desktop Experience)'
)
$script:ltsc2016SKUs = @(
'Enterprise 2016 LTSB',
'Enterprise N 2016 LTSB'
)
$script:ltscGenericSKUs = @( # For LTSC 2019, 2021, 2024
'Enterprise LTSC',
'Enterprise N LTSC'
)
$script:iotLtscSKUs = @(
'IoT Enterprise LTSC',
'IoT Enterprise N LTSC'
# Note: IoT SKUs are often specialized and might have different edition IDs.
# This list is a general representation. Actual ISOs might be needed for specific IoT LTSC editions.
)
# Map Windows Release Values to their corresponding SKU lists
$script:windowsReleaseSkuMap = @{
10 = $script:clientSKUs # Windows 10 Client
11 = $script:clientSKUs # Windows 11 Client
2016 = $script:serverSKUs # Windows Server 2016 (LTSB 2016 handled by Get-AvailableSkusForRelease)
2019 = $script:serverSKUs # Windows Server 2019 (LTSC 2019 handled by Get-AvailableSkusForRelease)
2022 = $script:serverSKUs # Windows Server 2022
2025 = $script:serverSKUs # Windows Server 2025
2021 = $script:ltscGenericSKUs + $script:iotLtscSKUs # Windows 10 LTSC 2021
2024 = $script:ltscGenericSKUs + $script:iotLtscSKUs # Windows 10 LTSC 2024
# Note: LTSC 2016 and LTSC 2019 SKUs are now conditionally returned by Get-AvailableSkusForRelease
}
function Get-CoreStaticVariables {
[CmdletBinding()]
param()
return @{
Headers = $script:Headers
UserAgent = $script:UserAgent
}
}
# Function to get VM Switch names and associated IP addresses
function Get-VMSwitchData {
[CmdletBinding()]
param()
$switchMap = @{}
$switchNames = @()
try {
$allSwitches = Get-VMSwitch -ErrorAction SilentlyContinue
if ($null -ne $allSwitches) {
foreach ($sw in $allSwitches) {
$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 {
WriteLog "No valid non-APIPA IPv4 address found for vSwitch '$($sw.Name)' (Adapter: $($netAdapter.Name)). Skipping from list."
}
}
else {
WriteLog "Could not find a network adapter matching pattern '$adapterNamePattern' for vSwitch '$($sw.Name)'. Skipping from list."
}
}
}
else {
WriteLog "No Hyper-V virtual switches found on this system."
}
}
catch {
WriteLog "Error occurred while getting VM Switch data: $($_.Exception.Message)"
}
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
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,
[Parameter(Mandatory = $true)]
[psobject]$State
)
if ([string]::IsNullOrEmpty($IsoPath)) {
return $State.Defaults.GeneralDefaults.MctWindowsReleases
}
else {
return $State.Defaults.GeneralDefaults.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,
[Parameter(Mandatory = $true)]
[psobject]$State
)
$result = [PSCustomObject]@{
Versions = @()
DefaultVersion = $null
IsEnabled = $false
}
if (-not $State.Defaults.GeneralDefaults.WindowsVersionMap.ContainsKey($SelectedRelease)) {
return $result
}
$validVersions = $State.Defaults.GeneralDefaults.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]
}
$result.IsEnabled = $true
}
return $result
}
# Function to get available SKUs for a given Windows Release value and display name
function Get-AvailableSkusForRelease {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[int]$SelectedReleaseValue,
[Parameter(Mandatory)]
[string]$SelectedReleaseDisplayName,
[Parameter(Mandatory = $true)]
[psobject]$State
)
WriteLog "Get-AvailableSkusForRelease: Getting SKUs for Release Value '$SelectedReleaseValue', Display Name '$SelectedReleaseDisplayName'."
# Handle LTSC 2016 specifically
if ($SelectedReleaseValue -eq 2016 -and $SelectedReleaseDisplayName -like '*LTSB*') {
WriteLog "Get-AvailableSkusForRelease: Matched LTSB 2016. Returning LTSC 2016 SKUs."
return $State.Defaults.GeneralDefaults.Ltsc2016SKUs
}
# Handle LTSC 2019 specifically
# Ensure "Server" is not in the display name to avoid matching "Windows Server 2019"
elseif ($SelectedReleaseValue -eq 2019 -and $SelectedReleaseDisplayName -like '*LTSC*' -and $SelectedReleaseDisplayName -notlike '*Server*') {
WriteLog "Get-AvailableSkusForRelease: Matched LTSC 2019. Returning generic LTSC SKUs (including IoT)."
# Assuming LTSC 2019 uses the generic LTSC SKUs + IoT LTSC SKUs
return ($State.Defaults.GeneralDefaults.LtscGenericSKUs + $State.Defaults.GeneralDefaults.IotLtscSKUs | Select-Object -Unique)
}
# For all other cases, use the main SKU map
elseif ($State.Defaults.GeneralDefaults.WindowsReleaseSkuMap.ContainsKey($SelectedReleaseValue)) {
$availableSkus = $State.Defaults.GeneralDefaults.WindowsReleaseSkuMap[$SelectedReleaseValue]
WriteLog "Get-AvailableSkusForRelease: Found $($availableSkus.Count) SKUs for Release '$SelectedReleaseValue' using standard map."
return $availableSkus
}
else {
WriteLog "Get-AvailableSkusForRelease: Warning - Release Value '$SelectedReleaseValue' not found in SKU map. Returning default client SKUs."
# Fallback to a default list (e.g., client SKUs) or an empty list
return $State.Defaults.GeneralDefaults.ClientSKUs
}
}
# Function to return general default settings for various UI elements
function Get-GeneralDefaults {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$FFUDevelopmentPath
)
# 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"
$driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.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 = $true
Optimize = $true
AllowVHDXCaching = $false
CreateCaptureMedia = $true
CreateDeploymentMedia = $true
AllowExternalHardDiskMedia = $false
PromptExternalHardDiskMedia = $true
SelectSpecificUSBDrives = $false
CopyAutopilot = $false
CopyUnattend = $false
CopyPPKG = $false
CleanupAppsISO = $true
CleanupCaptureISO = $true
CleanupDeployISO = $true
CleanupDrivers = $false
RemoveFFU = $false
RemoveApps = $false
RemoveUpdates = $false
# Hyper-V Settings Defaults
VMHostIPAddress = ""
DiskSizeGB = 30
MemoryGB = 4
Processors = 4
VMLocation = $vmLocationPath
VMNamePrefix = "_FFU"
LogicalSectorSize = 512
# Updates Tab Defaults
UpdateLatestCU = $true
UpdateLatestNet = $true
UpdateLatestDefender = $true
UpdateEdge = $true
UpdateOneDrive = $true
UpdateLatestMSRT = $true
UpdateLatestMicrocode = $false
UpdatePreviewCU = $false
# Applications Tab Defaults
InstallApps = $false
ApplicationPath = $appsPath
AppListJsonPath = $appListJsonPath
InstallWingetApps = $false
BringYourOwnApps = $false
# M365 Apps/Office Tab Defaults
InstallOffice = $true
OfficePath = $officePath
CopyOfficeConfigXML = $false
OfficeConfigXMLFilePath = ""
# Drivers Tab Defaults
DriversFolder = $driversPath
PEDriversFolder = $peDriversPath
DriversJsonPath = $driversJsonPath
DownloadDrivers = $false
InstallDrivers = $false
CopyDrivers = $false
CopyPEDrivers = $false
UpdateADK = $true
# Static Data Lists/Maps
AllowedFeatures = $script:allowedFeatures
SkuList = $script:skuList
AllowedLanguages = $script:allowedLangs
AllWindowsReleases = $script:allWindowsReleases
MctWindowsReleases = $script:mctWindowsReleases
WindowsVersionMap = $script:windowsVersionMap
ClientSKUs = $script:clientSKUs
ServerSKUs = $script:serverSKUs
Ltsc2016SKUs = $script:ltsc2016SKUs
LtscGenericSKUs = $script:ltscGenericSKUs
IotLtscSKUs = $script:iotLtscSKUs
WindowsReleaseSkuMap = $script:windowsReleaseSkuMap
}
}
# 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: 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..."
# Attempt to install/update Winget CLI and module
$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) {
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 }
}
# --------------------------------------------------------------------------
# SECTION: UI Configuration
# --------------------------------------------------------------------------
function Get-UIConfig {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
# Create hash to store configuration
$config = [ordered]@{
AllowExternalHardDiskMedia = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
AllowVHDXCaching = $State.Controls.chkAllowVHDXCaching.IsChecked
AppListPath = $State.Controls.txtAppListJsonPath.Text
AppsPath = $State.Controls.txtApplicationPath.Text
AppsScriptVariables = if ($State.Controls.chkDefineAppsScriptVariables.IsChecked) {
$vars = @{}
foreach ($item in $State.Data.appsScriptVariablesDataList) {
$vars[$item.Key] = $item.Value
}
if ($vars.Count -gt 0) { $vars } else { $null }
}
else { $null }
BuildUSBDrive = $State.Controls.chkBuildUSBDriveEnable.IsChecked
CleanupAppsISO = $State.Controls.chkCleanupAppsISO.IsChecked
CleanupCaptureISO = $State.Controls.chkCleanupCaptureISO.IsChecked
CleanupDeployISO = $State.Controls.chkCleanupDeployISO.IsChecked
CleanupDrivers = $State.Controls.chkCleanupDrivers.IsChecked
CompactOS = $State.Controls.chkCompactOS.IsChecked
CompressDownloadedDriversToWim = $State.Controls.chkCompressDriversToWIM.IsChecked
CopyAutopilot = $State.Controls.chkCopyAutopilot.IsChecked
CopyDrivers = $State.Controls.chkCopyDrivers.IsChecked
CopyOfficeConfigXML = $State.Controls.chkCopyOfficeConfigXML.IsChecked
CopyPEDrivers = $State.Controls.chkCopyPEDrivers.IsChecked
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
CustomFFUNameTemplate = $State.Controls.txtCustomFFUNameTemplate.Text
Disksize = [int64]$State.Controls.txtDiskSize.Text * 1GB
DownloadDrivers = $State.Controls.chkDownloadDrivers.IsChecked
DriversFolder = $State.Controls.txtDriversFolder.Text
DriversJsonPath = $State.Controls.txtDriversJsonPath.Text
FFUCaptureLocation = $State.Controls.txtFFUCaptureLocation.Text
FFUDevelopmentPath = $State.Controls.txtFFUDevPath.Text
FFUPrefix = $State.Controls.txtVMNamePrefix.Text
InstallApps = $State.Controls.chkInstallApps.IsChecked
InstallDrivers = $State.Controls.chkInstallDrivers.IsChecked
InstallOffice = $State.Controls.chkInstallOffice.IsChecked
InstallWingetApps = $State.Controls.chkInstallWingetApps.IsChecked
ISOPath = $State.Controls.txtISOPath.Text
LogicalSectorSizeBytes = [int]$State.Controls.cmbLogicalSectorSize.SelectedItem.Content
Make = $State.Controls.cmbMake.SelectedItem
MediaType = $State.Controls.cmbMediaType.SelectedItem
Memory = [int64]$State.Controls.txtMemory.Text * 1GB
Model = if ($State.Controls.chkDownloadDrivers.IsChecked) {
$selectedModels = $State.Controls.lstDriverModels.Items | Where-Object { $_.IsSelected }
if ($selectedModels.Count -ge 1) {
$selectedModels[0].Model
}
else {
$null
}
}
else {
$null
}
OfficeConfigXMLFile = $State.Controls.txtOfficeConfigXMLFilePath.Text
OfficePath = $State.Controls.txtOfficePath.Text
Optimize = $State.Controls.chkOptimize.IsChecked
OptionalFeatures = $State.Controls.txtOptionalFeatures.Text
OrchestrationPath = "$($State.Controls.txtApplicationPath.Text)\Orchestration"
PEDriversFolder = $State.Controls.txtPEDriversFolder.Text
Processors = [int]$State.Controls.txtProcessors.Text
ProductKey = $State.Controls.txtProductKey.Text
PromptExternalHardDiskMedia = $State.Controls.chkPromptExternalHardDiskMedia.IsChecked
RemoveApps = $State.Controls.chkRemoveApps.IsChecked
RemoveFFU = $State.Controls.chkRemoveFFU.IsChecked
RemoveUpdates = $State.Controls.chkRemoveUpdates.IsChecked
ShareName = $State.Controls.txtShareName.Text
UpdateADK = $State.Controls.chkUpdateADK.IsChecked
UpdateEdge = $State.Controls.chkUpdateEdge.IsChecked
UpdateLatestCU = $State.Controls.chkUpdateLatestCU.IsChecked
UpdateLatestDefender = $State.Controls.chkUpdateLatestDefender.IsChecked
UpdateLatestMicrocode = $State.Controls.chkUpdateLatestMicrocode.IsChecked
UpdateLatestMSRT = $State.Controls.chkUpdateLatestMSRT.IsChecked
UpdateLatestNet = $State.Controls.chkUpdateLatestNet.IsChecked
UpdateOneDrive = $State.Controls.chkUpdateOneDrive.IsChecked
UpdatePreviewCU = $State.Controls.chkUpdatePreviewCU.IsChecked
UserAppListPath = "$($State.Controls.txtApplicationPath.Text)\UserAppList.json"
USBDriveList = @{}
Username = $State.Controls.txtUsername.Text
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
VMLocation = $State.Controls.txtVMLocation.Text
VMSwitchName = if ($State.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
$State.Controls.txtCustomVMSwitchName.Text
}
else {
$State.Controls.cmbVMSwitchName.SelectedItem
}
WindowsArch = $State.Controls.cmbWindowsArch.SelectedItem
WindowsLang = $State.Controls.cmbWindowsLang.SelectedItem
WindowsRelease = [int]$State.Controls.cmbWindowsRelease.SelectedItem.Value
WindowsSKU = $State.Controls.cmbWindowsSKU.SelectedItem
WindowsVersion = $State.Controls.cmbWindowsVersion.SelectedItem
}
$State.Controls.lstUSBDrives.Items | Where-Object { $_.IsSelected } | ForEach-Object {
$config.USBDriveList[$_.Model] = $_.SerialNumber
}
return $config
}
# --------------------------------------------------------------------------
# SECTION: UI Initialization Functions
# --------------------------------------------------------------------------
function Initialize-UIControls {
param([PSCustomObject]$State)
WriteLog "Initializing UI control references..."
$window = $State.Window
# Find all controls ONCE and store them in the state object
$State.Controls.cmbWindowsRelease = $window.FindName('cmbWindowsRelease')
$State.Controls.cmbWindowsVersion = $window.FindName('cmbWindowsVersion')
$State.Controls.txtISOPath = $window.FindName('txtISOPath')
$State.Controls.btnBrowseISO = $window.FindName('btnBrowseISO')
$State.Controls.cmbWindowsArch = $window.FindName('cmbWindowsArch')
$State.Controls.cmbWindowsLang = $window.FindName('cmbWindowsLang')
$State.Controls.cmbWindowsSKU = $window.FindName('cmbWindowsSKU')
$State.Controls.cmbMediaType = $window.FindName('cmbMediaType')
$State.Controls.txtOptionalFeatures = $window.FindName('txtOptionalFeatures')
$State.Controls.featuresPanel = $window.FindName('stackFeaturesContainer')
$State.Controls.chkDownloadDrivers = $window.FindName('chkDownloadDrivers')
$State.Controls.cmbMake = $window.FindName('cmbMake')
$State.Controls.spMakeSection = $window.FindName('spMakeSection')
$State.Controls.btnGetModels = $window.FindName('btnGetModels')
$State.Controls.spModelFilterSection = $window.FindName('spModelFilterSection')
$State.Controls.txtModelFilter = $window.FindName('txtModelFilter')
$State.Controls.lstDriverModels = $window.FindName('lstDriverModels')
$State.Controls.spDriverActionButtons = $window.FindName('spDriverActionButtons')
$State.Controls.btnSaveDriversJson = $window.FindName('btnSaveDriversJson')
$State.Controls.btnImportDriversJson = $window.FindName('btnImportDriversJson')
$State.Controls.btnDownloadSelectedDrivers = $window.FindName('btnDownloadSelectedDrivers')
$State.Controls.btnClearDriverList = $window.FindName('btnClearDriverList')
$State.Controls.chkInstallOffice = $window.FindName('chkInstallOffice')
$State.Controls.chkInstallApps = $window.FindName('chkInstallApps')
$State.Controls.OfficePathStackPanel = $window.FindName('OfficePathStackPanel')
$State.Controls.OfficePathGrid = $window.FindName('OfficePathGrid')
$State.Controls.CopyOfficeConfigXMLStackPanel = $window.FindName('CopyOfficeConfigXMLStackPanel')
$State.Controls.OfficeConfigurationXMLFileStackPanel = $window.FindName('OfficeConfigurationXMLFileStackPanel')
$State.Controls.OfficeConfigurationXMLFileGrid = $window.FindName('OfficeConfigurationXMLFileGrid')
$State.Controls.chkCopyOfficeConfigXML = $window.FindName('chkCopyOfficeConfigXML')
$State.Controls.chkLatestCU = $window.FindName('chkUpdateLatestCU')
$State.Controls.chkPreviewCU = $window.FindName('chkUpdatePreviewCU')
$State.Controls.btnCheckUSBDrives = $window.FindName('btnCheckUSBDrives')
$State.Controls.lstUSBDrives = $window.FindName('lstUSBDrives')
$State.Controls.chkSelectAllUSBDrives = $window.FindName('chkSelectAllUSBDrives')
$State.Controls.chkBuildUSBDriveEnable = $window.FindName('chkBuildUSBDriveEnable')
$State.Controls.usbSection = $window.FindName('usbDriveSection')
$State.Controls.chkSelectSpecificUSBDrives = $window.FindName('chkSelectSpecificUSBDrives')
$State.Controls.usbSelectionPanel = $window.FindName('usbDriveSelectionPanel')
$State.Controls.chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia')
$State.Controls.chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia')
$State.Controls.chkInstallWingetApps = $window.FindName('chkInstallWingetApps')
$State.Controls.wingetPanel = $window.FindName('wingetPanel')
$State.Controls.btnCheckWingetModule = $window.FindName('btnCheckWingetModule')
$State.Controls.txtWingetVersion = $window.FindName('txtWingetVersion')
$State.Controls.txtWingetModuleVersion = $window.FindName('txtWingetModuleVersion')
$State.Controls.applicationPathPanel = $window.FindName('applicationPathPanel')
$State.Controls.appListJsonPathPanel = $window.FindName('appListJsonPathPanel')
$State.Controls.btnBrowseApplicationPath = $window.FindName('btnBrowseApplicationPath')
$State.Controls.btnBrowseAppListJsonPath = $window.FindName('btnBrowseAppListJsonPath')
$State.Controls.chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps')
$State.Controls.byoApplicationPanel = $window.FindName('byoApplicationPanel')
$State.Controls.wingetSearchPanel = $window.FindName('wingetSearchPanel')
$State.Controls.txtWingetSearch = $window.FindName('txtWingetSearch')
$State.Controls.btnWingetSearch = $window.FindName('btnWingetSearch')
$State.Controls.lstWingetResults = $window.FindName('lstWingetResults')
$State.Controls.btnSaveWingetList = $window.FindName('btnSaveWingetList')
$State.Controls.btnImportWingetList = $window.FindName('btnImportWingetList')
$State.Controls.btnClearWingetList = $window.FindName('btnClearWingetList')
$State.Controls.btnDownloadSelected = $window.FindName('btnDownloadSelected')
$State.Controls.btnBrowseAppSource = $window.FindName('btnBrowseAppSource')
$State.Controls.btnBrowseFFUDevPath = $window.FindName('btnBrowseFFUDevPath')
$State.Controls.btnBrowseFFUCaptureLocation = $window.FindName('btnBrowseFFUCaptureLocation')
$State.Controls.btnBrowseOfficePath = $window.FindName('btnBrowseOfficePath')
$State.Controls.btnBrowseDriversFolder = $window.FindName('btnBrowseDriversFolder')
$State.Controls.btnBrowsePEDriversFolder = $window.FindName('btnBrowsePEDriversFolder')
$State.Controls.txtAppName = $window.FindName('txtAppName')
$State.Controls.txtAppCommandLine = $window.FindName('txtAppCommandLine')
$State.Controls.txtAppArguments = $window.FindName('txtAppArguments')
$State.Controls.txtAppSource = $window.FindName('txtAppSource')
$State.Controls.btnAddApplication = $window.FindName('btnAddApplication')
$State.Controls.btnSaveBYOApplications = $window.FindName('btnSaveBYOApplications')
$State.Controls.btnLoadBYOApplications = $window.FindName('btnLoadBYOApplications')
$State.Controls.btnClearBYOApplications = $window.FindName('btnClearBYOApplications')
$State.Controls.btnCopyBYOApps = $window.FindName('btnCopyBYOApps')
$State.Controls.lstApplications = $window.FindName('lstApplications')
$State.Controls.btnMoveTop = $window.FindName('btnMoveTop')
$State.Controls.btnMoveUp = $window.FindName('btnMoveUp')
$State.Controls.btnMoveDown = $window.FindName('btnMoveDown')
$State.Controls.btnMoveBottom = $window.FindName('btnMoveBottom')
$State.Controls.txtStatus = $window.FindName('txtStatus')
$State.Controls.pbOverallProgress = $window.FindName('progressBar')
$State.Controls.txtOverallStatus = $window.FindName('txtStatus')
$State.Controls.cmbVMSwitchName = $window.FindName('cmbVMSwitchName')
$State.Controls.txtVMHostIPAddress = $window.FindName('txtVMHostIPAddress')
$State.Controls.txtCustomVMSwitchName = $window.FindName('txtCustomVMSwitchName')
$State.Controls.txtFFUDevPath = $window.FindName('txtFFUDevPath')
$State.Controls.txtCustomFFUNameTemplate = $window.FindName('txtCustomFFUNameTemplate')
$State.Controls.txtFFUCaptureLocation = $window.FindName('txtFFUCaptureLocation')
$State.Controls.txtShareName = $window.FindName('txtShareName')
$State.Controls.txtUsername = $window.FindName('txtUsername')
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
$State.Controls.chkCreateCaptureMedia = $window.FindName('chkCreateCaptureMedia')
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
$State.Controls.chkCopyPPKG = $window.FindName('chkCopyPPKG')
$State.Controls.chkCleanupAppsISO = $window.FindName('chkCleanupAppsISO')
$State.Controls.chkCleanupCaptureISO = $window.FindName('chkCleanupCaptureISO')
$State.Controls.chkCleanupDeployISO = $window.FindName('chkCleanupDeployISO')
$State.Controls.chkCleanupDrivers = $window.FindName('chkCleanupDrivers')
$State.Controls.chkRemoveFFU = $window.FindName('chkRemoveFFU')
$State.Controls.txtDiskSize = $window.FindName('txtDiskSize')
$State.Controls.txtMemory = $window.FindName('txtMemory')
$State.Controls.txtProcessors = $window.FindName('txtProcessors')
$State.Controls.txtVMLocation = $window.FindName('txtVMLocation')
$State.Controls.txtVMNamePrefix = $window.FindName('txtVMNamePrefix')
$State.Controls.cmbLogicalSectorSize = $window.FindName('cmbLogicalSectorSize')
$State.Controls.txtProductKey = $window.FindName('txtProductKey')
$State.Controls.txtOfficePath = $window.FindName('txtOfficePath')
$State.Controls.txtOfficeConfigXMLFilePath = $window.FindName('txtOfficeConfigXMLFilePath')
$State.Controls.txtDriversFolder = $window.FindName('txtDriversFolder')
$State.Controls.txtPEDriversFolder = $window.FindName('txtPEDriversFolder')
$State.Controls.chkCopyPEDrivers = $window.FindName('chkCopyPEDrivers')
$State.Controls.chkUpdateLatestCU = $window.FindName('chkUpdateLatestCU')
$State.Controls.chkUpdateLatestNet = $window.FindName('chkUpdateLatestNet')
$State.Controls.chkUpdateLatestDefender = $window.FindName('chkUpdateLatestDefender')
$State.Controls.chkUpdateEdge = $window.FindName('chkUpdateEdge')
$State.Controls.chkUpdateOneDrive = $window.FindName('chkUpdateOneDrive')
$State.Controls.chkUpdateLatestMSRT = $window.FindName('chkUpdateLatestMSRT')
$State.Controls.chkUpdatePreviewCU = $window.FindName('chkUpdatePreviewCU')
$State.Controls.txtApplicationPath = $window.FindName('txtApplicationPath')
$State.Controls.txtAppListJsonPath = $window.FindName('txtAppListJsonPath')
$State.Controls.chkInstallDrivers = $window.FindName('chkInstallDrivers')
$State.Controls.chkCopyDrivers = $window.FindName('chkCopyDrivers')
$State.Controls.chkCompressDriversToWIM = $window.FindName('chkCompressDriversToWIM')
$State.Controls.chkRemoveApps = $window.FindName('chkRemoveApps')
$State.Controls.chkRemoveUpdates = $window.FindName('chkRemoveUpdates')
$State.Controls.chkUpdateLatestMicrocode = $window.FindName('chkUpdateLatestMicrocode')
$State.Controls.chkDefineAppsScriptVariables = $window.FindName('chkDefineAppsScriptVariables')
$State.Controls.appsScriptVariablesPanel = $window.FindName('appsScriptVariablesPanel')
$State.Controls.txtAppsScriptKey = $window.FindName('txtAppsScriptKey')
$State.Controls.txtAppsScriptValue = $window.FindName('txtAppsScriptValue')
$State.Controls.btnAddAppsScriptVariable = $window.FindName('btnAddAppsScriptVariable')
$State.Controls.lstAppsScriptVariables = $window.FindName('lstAppsScriptVariables')
$State.Controls.btnRemoveSelectedAppsScriptVariables = $window.FindName('btnRemoveSelectedAppsScriptVariables')
$State.Controls.btnClearAppsScriptVariables = $window.FindName('btnClearAppsScriptVariables')
$State.Controls.txtDriversJsonPath = $window.FindName('txtDriversJsonPath')
$State.Controls.btnBrowseDriversJsonPath = $window.FindName('btnBrowseDriversJsonPath')
$State.Controls.chkUpdateADK = $window.FindName('chkUpdateADK')
}
# --------------------------------------------------------------------------
# SECTION: Module Export
# --------------------------------------------------------------------------
# Export only the functions intended for public use by the UI script
Export-ModuleMember -Function *