Refactor Winget functions into a dedicated module

Extracts Winget-related functionalities, including search, list management, installation, and download tasks, from `BuildFFUVM_UI.ps1` and `FFUUI.Core.psm1`.
Consolidates these features into a new module, `FFUUI.Core.Winget.psm1`, to improve code organization and maintainability.
This commit is contained in:
rbalsleyMSFT
2025-06-13 10:34:46 -07:00
parent 7babad8262
commit d1835c5c06
4 changed files with 695 additions and 680 deletions
-138
View File
@@ -1763,144 +1763,6 @@ $window.Add_Loaded({
})
# Function to search for Winget apps
function Search-WingetApps {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$searchQuery = $State.Controls.txtWingetSearch.Text
if ([string]::IsNullOrWhiteSpace($searchQuery)) { return }
# Get current items from the ListView
$currentItemsInListView = @()
if ($null -ne $State.Controls.lstWingetResults.ItemsSource) {
$currentItemsInListView = @($State.Controls.lstWingetResults.ItemsSource)
}
elseif ($State.Controls.lstWingetResults.HasItems) {
$currentItemsInListView = @($State.Controls.lstWingetResults.Items)
}
# Store selected apps from the current view
$selectedAppsFromView = @($currentItemsInListView | Where-Object { $_.IsSelected })
# Search for new apps
$searchedAppResults = Search-WingetPackagesPublic -Query $searchQuery | ForEach-Object {
[PSCustomObject]@{
IsSelected = $false # New items are not selected by default
Name = $_.Name
Id = $_.Id
Version = $_.Version
Source = $_.Source
DownloadStatus = ""
}
}
$finalAppList = [System.Collections.Generic.List[object]]::new()
$addedAppIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
# Add previously selected apps first
foreach ($app in $selectedAppsFromView) {
$finalAppList.Add($app)
$addedAppIds.Add($app.Id) | Out-Null
}
# Add new search results, avoiding duplicates of already added (selected) apps
foreach ($result in $searchedAppResults) {
if (-not $addedAppIds.Contains($result.Id)) {
$finalAppList.Add($result)
$addedAppIds.Add($result.Id) | Out-Null # Track added IDs to prevent duplicates from search results themselves
}
}
# Update the ListView's ItemsSource
$script:uiState.Controls.lstWingetResults.ItemsSource = $finalAppList.ToArray()
}
catch {
[System.Windows.MessageBox]::Show("Error searching for apps: $_", "Error", "OK", "Error")
}
}
# Function to save selected apps to JSON
function Save-WingetList {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$selectedApps = $State.Controls.lstWingetResults.Items | Where-Object { $_.IsSelected }
if (-not $selectedApps) {
[System.Windows.MessageBox]::Show("No apps selected to save.", "Warning", "OK", "Warning")
return
}
$appList = @{
apps = @($selectedApps | ForEach-Object {
[ordered]@{
name = $_.Name
id = $_.Id
source = $_.Source.ToLower()
}
})
}
$sfd = New-Object System.Windows.Forms.SaveFileDialog
$sfd.Filter = "JSON files (*.json)|*.json"
$sfd.Title = "Save App List"
$sfd.InitialDirectory = $AppsPath
$sfd.FileName = "AppList.json"
if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$appList | ConvertTo-Json -Depth 10 | Set-Content $sfd.FileName -Encoding UTF8
[System.Windows.MessageBox]::Show("App list saved successfully.", "Success", "OK", "Information")
}
}
catch {
[System.Windows.MessageBox]::Show("Error saving app list: $_", "Error", "OK", "Error")
}
}
# Function to import app list from JSON
function Import-WingetList {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.Filter = "JSON files (*.json)|*.json"
$ofd.Title = "Import App List"
$ofd.InitialDirectory = $AppsPath
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$importedAppsData = Get-Content $ofd.FileName -Raw | ConvertFrom-Json
$newAppListForItemsSource = [System.Collections.Generic.List[object]]::new()
if ($null -ne $importedAppsData.apps) {
foreach ($appInfo in $importedAppsData.apps) {
$newAppListForItemsSource.Add([PSCustomObject]@{
IsSelected = $true # Imported apps are marked as selected
Name = $appInfo.name
Id = $appInfo.id
Version = "" # Will be populated when searching or if data exists
Source = $appInfo.source
DownloadStatus = ""
})
}
}
$State.Controls.lstWingetResults.ItemsSource = $newAppListForItemsSource.ToArray()
[System.Windows.MessageBox]::Show("App list imported successfully.", "Success", "OK", "Information")
}
}
catch {
[System.Windows.MessageBox]::Show("Error importing app list: $_", "Error", "OK", "Error")
}
}
# Function to remove application and reorder priorities
function Remove-Application {
param(
@@ -0,0 +1,693 @@
# FFU UI Core Logic Module - Winget Functionality
# Contains UI-layer logic for the Winget functionality in the "Applications" tab.
# -----------------------------------------------------------------------------
# SECTION: Winget UI Functions (Moved from BuildFFUVM_UI.ps1)
# -----------------------------------------------------------------------------
# Function to search for Winget apps
function Search-WingetApps {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$searchQuery = $State.Controls.txtWingetSearch.Text
if ([string]::IsNullOrWhiteSpace($searchQuery)) { return }
# Get current items from the ListView
$currentItemsInListView = @()
if ($null -ne $State.Controls.lstWingetResults.ItemsSource) {
$currentItemsInListView = @($State.Controls.lstWingetResults.ItemsSource)
}
elseif ($State.Controls.lstWingetResults.HasItems) {
$currentItemsInListView = @($State.Controls.lstWingetResults.Items)
}
# Store selected apps from the current view
$selectedAppsFromView = @($currentItemsInListView | Where-Object { $_.IsSelected })
# Search for new apps
$searchedAppResults = Search-WingetPackagesPublic -Query $searchQuery | ForEach-Object {
[PSCustomObject]@{
IsSelected = $false # New items are not selected by default
Name = $_.Name
Id = $_.Id
Version = $_.Version
Source = $_.Source
DownloadStatus = ""
}
}
$finalAppList = [System.Collections.Generic.List[object]]::new()
$addedAppIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
# Add previously selected apps first
foreach ($app in $selectedAppsFromView) {
$finalAppList.Add($app)
$addedAppIds.Add($app.Id) | Out-Null
}
# Add new search results, avoiding duplicates of already added (selected) apps
foreach ($result in $searchedAppResults) {
if (-not $addedAppIds.Contains($result.Id)) {
$finalAppList.Add($result)
$addedAppIds.Add($result.Id) | Out-Null # Track added IDs to prevent duplicates from search results themselves
}
}
# Update the ListView's ItemsSource using the passed-in State object
$State.Controls.lstWingetResults.ItemsSource = $finalAppList.ToArray()
}
catch {
[System.Windows.MessageBox]::Show("Error searching for apps: $_", "Error", "OK", "Error")
}
}
# Function to save selected apps to JSON
function Save-WingetList {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$selectedApps = $State.Controls.lstWingetResults.Items | Where-Object { $_.IsSelected }
if (-not $selectedApps) {
[System.Windows.MessageBox]::Show("No apps selected to save.", "Warning", "OK", "Warning")
return
}
$appList = @{
apps = @($selectedApps | ForEach-Object {
[ordered]@{
name = $_.Name
id = $_.Id
source = $_.Source.ToLower()
}
})
}
$sfd = New-Object System.Windows.Forms.SaveFileDialog
$sfd.Filter = "JSON files (*.json)|*.json"
$sfd.Title = "Save App List"
# Correctly get the path from the UI control via the State object
$sfd.InitialDirectory = $State.Controls.txtApplicationPath.Text
$sfd.FileName = "AppList.json"
if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$appList | ConvertTo-Json -Depth 10 | Set-Content $sfd.FileName -Encoding UTF8
[System.Windows.MessageBox]::Show("App list saved successfully.", "Success", "OK", "Information")
}
}
catch {
[System.Windows.MessageBox]::Show("Error saving app list: $_", "Error", "OK", "Error")
}
}
# Function to import app list from JSON
function Import-WingetList {
param(
[Parameter(Mandatory = $true)]
[psobject]$State
)
try {
$ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.Filter = "JSON files (*.json)|*.json"
$ofd.Title = "Import App List"
# Correctly get the path from the UI control via the State object
$ofd.InitialDirectory = $State.Controls.txtApplicationPath.Text
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$importedAppsData = Get-Content $ofd.FileName -Raw | ConvertFrom-Json
$newAppListForItemsSource = [System.Collections.Generic.List[object]]::new()
if ($null -ne $importedAppsData.apps) {
foreach ($appInfo in $importedAppsData.apps) {
$newAppListForItemsSource.Add([PSCustomObject]@{
IsSelected = $true # Imported apps are marked as selected
Name = $appInfo.name
Id = $appInfo.id
Version = "" # Will be populated when searching or if data exists
Source = $appInfo.source
DownloadStatus = ""
})
}
}
$State.Controls.lstWingetResults.ItemsSource = $newAppListForItemsSource.ToArray()
[System.Windows.MessageBox]::Show("App list imported successfully.", "Success", "OK", "Information")
}
}
catch {
[System.Windows.MessageBox]::Show("Error importing app list: $_", "Error", "OK", "Error")
}
}
# --------------------------------------------------------------------------
# SECTION: Winget Management Functions (Moved from FFUUI.Core.psm1)
# --------------------------------------------------------------------------
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 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
}
# --------------------------------------------------------------------------
# SECTION: Module Export
# --------------------------------------------------------------------------
Export-ModuleMember -Function *
+2 -1
View File
@@ -71,7 +71,8 @@ NestedModules = @('FFUUI.Core.Shared.psm1',
'FFUUI.Core.Drivers.Dell.psm1',
'FFUUI.Core.Drivers.HP.psm1',
'FFUUI.Core.Drivers.Lenovo.psm1',
'FFUUI.Core.Drivers.Microsoft.psm1')
'FFUUI.Core.Drivers.Microsoft.psm1',
'FFUUI.Core.Winget.psm1')
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = '*'
-541
View File
@@ -482,547 +482,6 @@ function Get-USBDrives {
}
}
# --------------------------------------------------------------------------
# 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()]