diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index f353c0f..a878ff7 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -5262,7 +5262,7 @@ if ($InstallApps) { # If there are no existing apps, use the original AppList.json directly if (-not $hasExistingApps) { WriteLog "No existing applications found. Using original AppList.json for all apps." - Get-Apps -AppList $AppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath + Get-Apps -AppList $AppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads } else { # Compare apps in AppList.json with existing installations @@ -5334,7 +5334,7 @@ if ($InstallApps) { # Download missing apps WriteLog "Downloading missing applications" - Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath + Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads # Cleanup modified app list Remove-Item -Path $modifiedAppListPath -Force diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 index b53f5b6..dd982a8 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 @@ -156,7 +156,7 @@ function Invoke-ParallelProcessing { # Execute the appropriate background task based on $localTaskType switch ($localTaskType) { 'WingetDownload' { - # Pass the progress queue to the task function + # Pass the progress queue and SkipWin32Json to the task function $wingetTaskArgs = @{ ApplicationItemData = $currentItem AppListJsonPath = $localJobArgs['AppListJsonPath'] @@ -164,6 +164,7 @@ function Invoke-ParallelProcessing { OrchestrationPath = $localJobArgs['OrchestrationPath'] ProgressQueue = $localProgressQueue WindowsArch = $localJobArgs['WindowsArch'] + SkipWin32Json = [bool]$localJobArgs['SkipWin32Json'] } $taskResult = Start-WingetAppDownloadTask @wingetTaskArgs if ($null -ne $taskResult) { diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 index ae5c509..7f891a6 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 @@ -339,6 +339,324 @@ function Get-Application { return $overallResult } +# Function to handle downloading a winget application in parallel +# This function is called by Invoke-ParallelProcessing for each app +function Start-WingetAppDownloadTask { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [PSCustomObject]$ApplicationItemData, + [Parameter(Mandatory = $true)] + [string]$AppListJsonPath, + [Parameter(Mandatory = $true)] + [string]$AppsPath, + [Parameter(Mandatory = $true)] + [string]$OrchestrationPath, + [Parameter(Mandatory = $true)] + [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue, + [string]$WindowsArch, + [switch]$SkipWin32Json + ) + + $appName = $ApplicationItemData.Name + $appId = $ApplicationItemData.Id + $source = $ApplicationItemData.Source + $status = "Checking..." + $resultCode = -1 + $sanitizedAppName = ConvertTo-SafeName -Name $appName + + # Initial status update + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + + WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)." + + try { + # Define paths + $userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json" + $appFound = $false + + # 1. Check UserAppList.json and content + if (Test-Path -Path $userAppListPath) { + try { + $userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json + $userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName } + + if ($userAppEntry) { + $appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName + if (Test-Path -Path $appFolder -PathType Container) { + $folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum + if ($folderSize -gt 1MB) { + $appFound = $true + $status = "Not Downloaded: App in $userAppListPath and found in $appFolder" + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + WriteLog "Found '$appName' in $userAppListPath and content exists in '$appFolder'." + return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 } + } + else { + $appFound = $true + $status = "App in '$userAppListPath' but content missing/small in '$appFolder'. Copy content or remove from UserAppList.json." + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + WriteLog $status + return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 } + } + } + else { + $appFound = $true + $status = "App in '$userAppListPath' but content folder '$appFolder' not found. Copy content or remove from UserAppList.json." + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + WriteLog $status + return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 } + } + } + } + catch { + WriteLog "Warning: Could not read or parse '$userAppListPath'. Error: $($_.Exception.Message)" + } + } + + # 2. Check existing downloaded Win32 content (folder-based) + if (-not $appFound -and $source -eq 'winget') { + $appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName + if (Test-Path -Path $appFolder -PathType Container) { + $contentFound = $false + if ($ApplicationItemData.Architecture -eq 'x86 x64') { + $x86Folder = Join-Path -Path $appFolder -ChildPath "x86" + $x64Folder = Join-Path -Path $appFolder -ChildPath "x64" + if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) { + $x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum + $x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum + if ($x86Size -gt 1MB -and $x64Size -gt 1MB) { + $contentFound = $true + } + } + } + else { + $folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum + if ($folderSize -gt 1MB) { + $contentFound = $true + } + } + if ($contentFound) { + $appFound = $true + $status = "Not Downloaded: Existing content found in $appFolder" + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry." + return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 } + } + } + } + + # Check MSStore folder + if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) { + $appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName + if (Test-Path -Path $appFolder -PathType Container) { + $folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum + if ($folderSize -gt 1MB) { + $appFound = $true + $status = "Already downloaded (MSStore)" + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + WriteLog "Found '$appName' content in '$appFolder'." + return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 } + } + } + } + + # 3. If not found locally, add to AppList.json and download + if (-not $appFound) { + # Add to AppList.json with mutex lock for thread safety + $appListContent = $null + $appListDir = Split-Path -Path $AppListJsonPath -Parent + if (-not (Test-Path -Path $appListDir -PathType Container)) { + New-Item -Path $appListDir -ItemType Directory -Force | Out-Null + } + if (Test-Path -Path $AppListJsonPath) { + try { + $appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json + if (-not $appListContent.PSObject.Properties['apps']) { + $appListContent = @{ apps = @() } + } + } + catch { + WriteLog "Warning: Could not read or parse '$AppListJsonPath'. Creating new structure. Error: $($_.Exception.Message)" + $appListContent = @{ apps = @() } + } + } + else { + $appListContent = @{ apps = @() } + } + + $appExistsInAppList = $false + if ($appListContent.apps) { + foreach ($app in $appListContent.apps) { + if ($app.id -eq $appId) { + $appExistsInAppList = $true + break + } + } + } + + if (-not $appExistsInAppList) { + $newApp = @{ name = $sanitizedAppName; id = $appId; source = $source } + if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() } + $appListContent.apps += $newApp + try { + # Use a mutex lock to prevent race conditions when writing to the same file + $lockName = "AppListJsonLock" + $lock = New-Object System.Threading.Mutex($false, $lockName) + try { + $lock.WaitOne() | Out-Null + # Re-read content inside lock to ensure latest version + if (Test-Path -Path $AppListJsonPath) { + $currentAppListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json + if (-not ($currentAppListContent.apps | Where-Object { $_.id -eq $appId })) { + $currentAppListContent.apps += $newApp + $currentAppListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 + WriteLog "Added '$appName' to '$AppListJsonPath'." + } + else { + WriteLog "'$appName' already exists in '$AppListJsonPath' (checked inside lock)." + } + } + else { + # File doesn't exist, write the initial content + $appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 + WriteLog "Created '$AppListJsonPath' and added '$appName'." + } + } + finally { + $lock.ReleaseMutex() + $lock.Dispose() + } + } + catch { + WriteLog "Error saving '$AppListJsonPath'. Error: $($_.Exception.Message)" + $status = "Failed to save AppList.json: $($_.Exception.Message)" + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 } + } + } + else { + WriteLog "'$appName' already exists in '$AppListJsonPath'." + } + + # Proceed with download + $status = "Downloading..." + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + + # Ensure necessary folders exist + WriteLog "Orchestration Path: $($OrchestrationPath)" + if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) { + New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null + } + $win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32" + if ($source -eq "winget" -and -not (Test-Path -Path $win32Folder -PathType Container)) { + New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null + } + $storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore" + if ($source -eq "msstore" -and -not (Test-Path -Path $storeAppsFolder -PathType Container)) { + New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null + } + + try { + # Call Get-Application to perform the actual download + # Pass SkipWin32Json based on caller context (UI mode skips, CLI mode creates) + $getAppParams = @{ + AppName = $appName + AppId = $appId + Source = $source + AppsPath = $AppsPath + ApplicationArch = $ApplicationItemData.Architecture + WindowsArch = $WindowsArch + OrchestrationPath = $OrchestrationPath + ErrorAction = 'Stop' + } + if ($SkipWin32Json) { + $getAppParams['SkipWin32Json'] = $true + } + $resultCode = Get-Application @getAppParams + + # Determine status based on result code + switch ($resultCode) { + 0 { $status = "Downloaded successfully" } + 1 { $status = "Error: No app installers were found" } + 2 { $status = "Silent install switch could not be found. Did not download." } + 3 { $status = "Error: Publisher does not support download" } + 4 { $status = "Skipped: Use 'msstore' source instead." } + default { $status = "Downloaded with status: $resultCode" } + } + + # Remove app from AppList.json if silent install switch could not be found (resultCode 2) + if ($resultCode -eq 2) { + try { + if (Test-Path -Path $AppListJsonPath) { + $appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json + if ($appListContent.apps) { + $filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId }) + $appListContent.apps = $filteredApps + $appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 + WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to missing silent install switch." + } + } + } + catch { + WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)" + } + } + } + catch { + $status = $_.Exception.Message + WriteLog "Download error for $($appName): $($_.Exception.Message)" + $resultCode = 1 + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + + # Remove app from AppList.json if publisher does not support download + if ($_.Exception.Message -match "does not support downloads by the publisher") { + try { + if (Test-Path -Path $AppListJsonPath) { + $appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json + if ($appListContent.apps) { + $filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId }) + $appListContent.apps = $filteredApps + $appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 + WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to publisher download restriction." + } + } + } + catch { + WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)" + } + } + } + } + } + catch { + $status = $_.Exception.Message + WriteLog "Unexpected error in Start-WingetAppDownloadTask for $($appName): $($_.Exception.Message)" + $resultCode = 1 + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + } + finally { + # Ensure status is not empty before returning + if ([string]::IsNullOrEmpty($status)) { + $status = "Unknown failure" + WriteLog "Status was empty for $appName ($appId), setting to default error." + if ($resultCode -ne 0 -and $resultCode -ne 1 -and $resultCode -ne 2) { + $resultCode = -1 + } + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + } + elseif ($resultCode -ne 0) { + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + } + else { + Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status + } + } + + # Return the final status and result code + return @{ Id = $appId; Status = $status; ResultCode = $resultCode } +} + function Get-Apps { [CmdletBinding()] param ( @@ -349,28 +667,31 @@ function Get-Apps { [Parameter(Mandatory = $true)] [string]$WindowsArch, [Parameter(Mandatory = $true)] - [string]$OrchestrationPath + [string]$OrchestrationPath, + [Parameter(Mandatory = $false)] + [string]$LogFilePath, + [Parameter(Mandatory = $false)] + [int]$ThrottleLimit = 5 ) # Load and validate app list $apps = Get-Content -Path $AppList -Raw | ConvertFrom-Json - if (-not $apps) { + if (-not $apps -or -not $apps.apps -or $apps.apps.Count -eq 0) { WriteLog "No apps were specified in AppList.json file." return } - # Process WinGet apps + # Log app list summary $wingetApps = $apps.apps | Where-Object { $_.source -eq "winget" } if ($wingetApps) { WriteLog 'Winget apps to be installed:' $wingetApps | ForEach-Object { WriteLog $_.Name } } - # Process Store apps - $StoreApps = $apps.apps | Where-Object { $_.source -eq "msstore" } - if ($StoreApps) { + $storeApps = $apps.apps | Where-Object { $_.source -eq "msstore" } + if ($storeApps) { WriteLog 'Store apps to be installed:' - $StoreApps | ForEach-Object { WriteLog $_.Name } + $storeApps | ForEach-Object { WriteLog $_.Name } } # Ensure WinGet is available @@ -379,45 +700,52 @@ function Get-Apps { # Create necessary folders $win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32" $storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore" - - # Process WinGet apps - if ($wingetApps) { - if (-not (Test-Path -Path $win32Folder -PathType Container)) { - WriteLog "Creating folder for Winget Win32 apps: $win32Folder" - New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null - WriteLog "Folder created successfully." - } - - foreach ($wingetApp in $wingetApps) { - try { - $appArch = if ($wingetApp.PSObject.Properties['architecture']) { $wingetApp.architecture } else { $WindowsArch } - Get-Application -AppName $wingetApp.Name -AppId $wingetApp.Id -Source 'winget' -AppsPath $AppsPath -ApplicationArch $appArch -OrchestrationPath $OrchestrationPath - } - catch { - WriteLog "Error occurred while processing $($wingetApp.Name): $_" - throw $_ - } + + if (-not (Test-Path -Path $win32Folder -PathType Container)) { + WriteLog "Creating folder for Winget Win32 apps: $win32Folder" + New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null + } + if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) { + WriteLog "Creating folder for MSStore apps: $storeAppsFolder" + New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null + } + + # Transform apps into the format expected by Invoke-ParallelProcessing + $itemsToProcess = $apps.apps | ForEach-Object { + $appArch = if ($_.PSObject.Properties['architecture']) { $_.architecture } else { $WindowsArch } + [PSCustomObject]@{ + Name = $_.name + Id = $_.id + Source = $_.source + Architecture = $appArch } } - # Process Store apps - if ($StoreApps) { - if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) { - New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null - } - - foreach ($storeApp in $StoreApps) { - try { - $appArch = if ($storeApp.PSObject.Properties['architecture']) { $storeApp.architecture } else { $WindowsArch } - Get-Application -AppName $storeApp.Name -AppId $storeApp.Id -Source 'msstore' -AppsPath $AppsPath -ApplicationArch $appArch -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath - } - catch { - WriteLog "Error occurred while processing $($storeApp.Name): $_" - throw $_ - } - } + WriteLog "Starting parallel download of $($itemsToProcess.Count) applications with ThrottleLimit: $ThrottleLimit" + + # Build task arguments for Invoke-ParallelProcessing + # CLI builds should create WinGetWin32Apps.json, so SkipWin32Json is false + $taskArguments = @{ + AppsPath = $AppsPath + AppListJsonPath = $AppList + OrchestrationPath = $OrchestrationPath + WindowsArch = $WindowsArch + SkipWin32Json = $false } + # Invoke parallel processing in non-UI mode (no WindowObject or ListViewControl) + Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess ` + -IdentifierProperty 'Id' ` + -StatusProperty 'DownloadStatus' ` + -TaskType 'WingetDownload' ` + -TaskArguments $taskArguments ` + -CompletedStatusText "Completed" ` + -ErrorStatusPrefix "Error: " ` + -MainThreadLogPath $LogFilePath ` + -ThrottleLimit $ThrottleLimit + + WriteLog "Parallel download of applications completed." + # Post-processing: Override CommandLine / Arguments from AppList.json if provided # Users may supply custom silent install commands or arguments. These optional # properties (CommandLine, Arguments) in AppList.json replace the auto-generated @@ -749,4 +1077,4 @@ function Add-Win32SilentInstallCommand { # -------------------------------------------------------------------------- # Export functions needed by both BuildFFUVM and the UI Core module -Export-ModuleMember -Function Get-Application, Get-Apps, Confirm-WinGetInstallation, Add-Win32SilentInstallCommand, Install-Winget \ No newline at end of file +Export-ModuleMember -Function Get-Application, Get-Apps, Start-WingetAppDownloadTask, Confirm-WinGetInstallation, Add-Win32SilentInstallCommand, Install-Winget \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 index 91e1058..7a8c8f9 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 @@ -378,324 +378,9 @@ function Confirm-WingetInstallationUI { return $result } -# Function to handle downloading a winget application (Modified for ForEach-Object -Parallel) -function Start-WingetAppDownloadTask { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [PSCustomObject]$ApplicationItemData, # Pass data, not the UI object - [Parameter(Mandatory = $true)] - [string]$AppListJsonPath, - [Parameter(Mandatory = $true)] - [string]$AppsPath, # Pass necessary paths - [Parameter(Mandatory = $true)] - [string]$OrchestrationPath, - [Parameter(Mandatory = $true)] - [System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue, # Add queue parameter - [string]$WindowsArch - ) - - $appName = $ApplicationItemData.Name - $appId = $ApplicationItemData.Id - $source = $ApplicationItemData.Source - $status = "Checking..." # Initial local status - $resultCode = -1 # Default to error/unknown - $sanitizedAppName = ConvertTo-SafeName -Name $appName - - # Initial status update - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - - WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)." - try { - # Define paths - $userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json" - $appFound = $false # Flag to track if the app is found locally - # WriteLog "UserAppList Path: $($userAppListPath)" - # WriteLog "Checking for existing app in UserAppList.json and content folder." - - # 1. Check UserAppList.json and content - if (Test-Path -Path $userAppListPath) { - # WriteLog "UserAppList.json found at $($userAppListPath). Checking for app entry." - try { - $userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json - $userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName } - - if ($userAppEntry) { - $appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName - if (Test-Path -Path $appFolder -PathType Container) { - $folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum - if ($folderSize -gt 1MB) { - $appFound = $true - $status = "Not Downloaded: App in $userAppListPath and found in $appFolder" - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - WriteLog "Found '$appName' in $userAppListPath and content exists in '$appFolder'." - return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 } - } - else { - $appFound = $true - $status = "App in '$userAppListPath' but content missing/small in '$appFolder'. Copy content or remove from UserAppList.json." - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - WriteLog $status - return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 } - } - } - else { - $appFound = $true - $status = "App in '$userAppListPath' but content folder '$appFolder' not found. Copy content or remove from UserAppList.json." - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - WriteLog $status - return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 } - } - } - } - catch { - WriteLog "Warning: Could not read or parse '$userAppListPath'. Error: $($_.Exception.Message)" - } - } - - # 2. Check existing downloaded Win32 content (folder-based; no WinGetWin32Apps.json dependency) - if (-not $appFound -and $source -eq 'winget') { - $appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName - if (Test-Path -Path $appFolder -PathType Container) { - $contentFound = $false - if ($ApplicationItemData.Architecture -eq 'x86 x64') { - $x86Folder = Join-Path -Path $appFolder -ChildPath "x86" - $x64Folder = Join-Path -Path $appFolder -ChildPath "x64" - if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) { - $x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum - $x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum - if ($x86Size -gt 1MB -and $x64Size -gt 1MB) { - $contentFound = $true - } - } - } - else { - $folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum - if ($folderSize -gt 1MB) { - $contentFound = $true - } - } - if ($contentFound) { - $appFound = $true - $status = "Not Downloaded: Existing content found in $appFolder" - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry." - return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 } - } - } - } - - # Check MSStore folder - if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) { - $appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName - if (Test-Path -Path $appFolder -PathType Container) { - $folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum - if ($folderSize -gt 1MB) { - $appFound = $true - $status = "Already downloaded (MSStore)" - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - WriteLog "Found '$appName' content in '$appFolder'." - return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 } - } - } - } - - # 3. If not found locally, add to AppList.json and download - if (-not $appFound) { - # Add to AppList.json - $appListContent = $null - $appListDir = Split-Path -Path $AppListJsonPath -Parent - if (-not (Test-Path -Path $appListDir -PathType Container)) { - New-Item -Path $appListDir -ItemType Directory -Force | Out-Null - } - if (Test-Path -Path $AppListJsonPath) { - try { - $appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json - if (-not $appListContent.PSObject.Properties['apps']) { - $appListContent = @{ apps = @() } - } - } - catch { - WriteLog "Warning: Could not read or parse '$AppListJsonPath'. Creating new structure. Error: $($_.Exception.Message)" - $appListContent = @{ apps = @() } - } - } - else { - $appListContent = @{ apps = @() } - } - - $appExistsInAppList = $false - if ($appListContent.apps) { - foreach ($app in $appListContent.apps) { - if ($app.id -eq $appId) { - $appExistsInAppList = $true - break - } - } - } - - if (-not $appExistsInAppList) { - $newApp = @{ name = $sanitizedAppName; id = $appId; source = $source } - if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() } - $appListContent.apps += $newApp - try { - # Use a lock to prevent race conditions when writing to the same file - $lockName = "AppListJsonLock" - $lock = New-Object System.Threading.Mutex($false, $lockName) - try { - $lock.WaitOne() | Out-Null - # Re-read content inside lock to ensure latest version - if (Test-Path -Path $AppListJsonPath) { - $currentAppListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json - if (-not ($currentAppListContent.apps | Where-Object { $_.id -eq $appId })) { - $currentAppListContent.apps += $newApp - $currentAppListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 - WriteLog "Added '$appName' to '$AppListJsonPath'." - } - else { - WriteLog "'$appName' already exists in '$AppListJsonPath' (checked inside lock)." - } - } - else { - # File doesn't exist, write the initial content - $appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 - WriteLog "Created '$AppListJsonPath' and added '$appName'." - } - } - finally { - $lock.ReleaseMutex() - $lock.Dispose() - } - } - catch { - WriteLog "Error saving '$AppListJsonPath'. Error: $($_.Exception.Message)" - $status = "Failed to save AppList.json: $($_.Exception.Message)" - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 } - } - } - else { - WriteLog "'$appName' already exists in '$AppListJsonPath'." - } - - # Proceed with download - $status = "Downloading..." - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - - # Ensure variables needed by Get-Application are accessible - # (Assuming they are available via $using: scope or global scope from main script) - # $global:AppsPath = $AppsPath # Potentially redundant - # $global:WindowsArch = $ApplicationItemData.Architecture # Potentially redundant - # $global:orchestrationPath = $OrchestrationPath # Potentially redundant" - WriteLog "Orchestration Path: $($OrchestrationPath)" - if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) { - New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null - } - $win32Folder = Join-Path -Path $AppsPath -ChildPath "Win32" - if ($source -eq "winget" -and -not (Test-Path -Path $win32Folder -PathType Container)) { - New-Item -Path $win32Folder -ItemType Directory -Force | Out-Null - } - $storeAppsFolder = Join-Path -Path $AppsPath -ChildPath "MSStore" - if ($source -eq "msstore" -and -not (Test-Path -Path $storeAppsFolder -PathType Container)) { - New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null - } - - try { - # Call Get-Application - $resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -ApplicationArch $ApplicationItemData.Architecture -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -SkipWin32Json -ErrorAction Stop - - # Determine status based on result code - switch ($resultCode) { - 0 { $status = "Downloaded successfully" } - 1 { $status = "Error: No app installers were found" } - 2 { $status = "Silent install switch could not be found. Did not download." } - 3 { $status = "Error: Publisher does not support download" } - 4 { $status = "Skipped: Use 'msstore' source instead." } - default { $status = "Downloaded with status: $resultCode" } # Should not happen with current Get-Application - } - - # Remove app from AppList.json if silent install switch could not be found (resultCode 2) - if ($resultCode -eq 2) { - try { - if (Test-Path -Path $AppListJsonPath) { - $appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json - if ($appListContent.apps) { - $filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId }) - $appListContent.apps = $filteredApps - $appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 - WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to missing silent install switch." - } - } - } - catch { - WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)" - } - } - } - catch { - $status = $_.Exception.Message - WriteLog "Download error for $($appName): $($_.Exception.Message)" - $resultCode = 1 # Indicate error - # Enqueue error status - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - - # Remove app from AppList.json if publisher does not support download - if ($_.Exception.Message -match "does not support downloads by the publisher") { - try { - if (Test-Path -Path $AppListJsonPath) { - $appListContent = Get-Content -Path $AppListJsonPath -Raw | ConvertFrom-Json - if ($appListContent.apps) { - $filteredApps = @($appListContent.apps | Where-Object { $_.id -ne $appId }) - $appListContent.apps = $filteredApps - $appListContent | ConvertTo-Json -Depth 10 | Set-Content -Path $AppListJsonPath -Encoding UTF8 - WriteLog "Removed '$appName' ($appId) from '$AppListJsonPath' due to publisher download restriction." - } - } - } - catch { - WriteLog "Failed to remove '$appName' from '$AppListJsonPath': $($_.Exception.Message)" - } - } - - } - } # End if (-not $appFound) - - } - catch { - $status = $_.Exception.Message - WriteLog "Unexpected error in Start-WingetAppDownloadTask for $($appName): $($_.Exception.Message)" - $resultCode = 1 # Indicate error - # Enqueue error status - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - } - finally { - # Ensure status is not empty before returning - if ([string]::IsNullOrEmpty($status)) { - $status = "Unknown failure" # Provide a default error status - WriteLog "Status was empty for $appName ($appId), setting to default error." - if ($resultCode -ne 0 -and $resultCode -ne 1 -and $resultCode -ne 2) { - $resultCode = -1 # Ensure resultCode reflects an error if it was empty - } - # Enqueue the final (error) status if it was previously empty - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - } - elseif ($resultCode -ne 0) { - # Enqueue the final status if it's an error (already set in try/catch) - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - } - else { - # Enqueue the final success status - Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status - } - } - - # Prepare the return object as a Hashtable - $returnObject = @{ Id = $appId; Status = $status; ResultCode = $resultCode } - - # Return the final status and result code as a Hashtable - return $returnObject -} +# Note: Start-WingetAppDownloadTask has been moved to FFU.Common.Winget.psm1 +# to enable code reuse between UI and CLI builds. It is imported via the FFU.Common module. function Invoke-WingetDownload { param( @@ -721,11 +406,13 @@ function Invoke-WingetDownload { $localOrchestrationPath = Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath "Orchestration" # Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing + # UI downloads skip WinGetWin32Apps.json creation - it's generated at build time $taskArguments = @{ AppsPath = $localAppsPath AppListJsonPath = $localAppListJsonPath OrchestrationPath = $localOrchestrationPath WindowsArch = $localWindowsArch + SkipWin32Json = $true } # Select only necessary properties before passing to Invoke-ParallelProcessing