Private/Invoke-AppDownload.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Downloads a single queued application using Get-EvergreenApp + Save-EvergreenApp.
 
.DESCRIPTION
    Processes one item from $syncHash.DownloadQueue. Calls Get-EvergreenApp to
    resolve the download URI for the named app, matches the item's filter criteria
    (Architecture, Channel, Platform, Version), then calls Save-EvergreenApp to
    write the file to the configured output path.
 
    Status is updated in-place on the queue item via Dispatcher.Invoke so the
    bound ListView updates live without needing a collection change event.
 
    Designed to run inside a runspace created by New-WpfRunspace. Pass $syncHash
    in via the runspace's SessionStateProxy or InitialSessionState variable.
 
.PARAMETER SyncHash
    Shared synchronised hashtable. Reads:
        DownloadQueue - List[PSCustomObject] of pending items
        Config - PSCustomObject with OutputPath property
 
.PARAMETER QueueItem
    A single PSCustomObject from DownloadQueue matching the Download Queue Item
    Schema (AppName, Version, Architecture, Channel, Platform, Uri, Status).
 
.NOTES
    Queue item Status values:
        'Pending' - not yet started
        'Downloading' - in progress
        'Done' - completed successfully
        'Failed' - error occurred (check log for details)
#>

function Invoke-AppDownload {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.Hashtable]$SyncHash,

        [Parameter(Mandatory)]
        [PSCustomObject]$QueueItem
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    # Helper: update Status on the queue item from the UI thread.
    # PSCustomObject has no WPF thread affinity, so the Status assignment and the
    # Where-Object counts are computed here on the background thread. Only the
    # pure UI-property writes are dispatched, keeping the [action] delegate free of
    # PS cmdlet pipelines (which deadlock when executed via Dispatcher.Invoke).
    $setStatus = {
        param([string]$NewStatus)

        # Update the data object on the background thread.
        $QueueItem.Status = $NewStatus

        # Pre-compute queue counts on the background thread.
        $pending = @($SyncHash.DownloadQueue | Where-Object { $_.Status -eq 'Pending' }).Count
        $done    = @($SyncHash.DownloadQueue | Where-Object { $_.Status -eq 'Done' }).Count
        $failed  = @($SyncHash.DownloadQueue | Where-Object { $_.Status -eq 'Failed' }).Count
        $total   = $SyncHash.DownloadQueue.Count
        $queueText = "Queue: $total items (Pending: $pending, Done: $done, Failed: $failed)"

        # Dispatch only simple .NET property writes to the UI thread.
        $SyncHash.Window.Dispatcher.Invoke([action] {
                if ($null -ne $SyncHash.DownloadQueueListView) {
                    $SyncHash.DownloadQueueListView.Items.Refresh()
                }
                if ($null -ne $SyncHash.QueueCountLabel) {
                    $SyncHash.QueueCountLabel.Text = $queueText
                }
            }, 'Normal')
    }

    & $setStatus 'Downloading'
    Write-UILog -SyncHash $SyncHash -Message "Downloading: $($QueueItem.AppName) $($QueueItem.Version)..." -Level Info

    try {
        if ([string]::IsNullOrWhiteSpace($QueueItem.Uri)) {
            throw "No download URI stored for $($QueueItem.AppName) $($QueueItem.Version)."
        }

        $baseOutput = if ($SyncHash.Config.OutputPath) { $SyncHash.Config.OutputPath } else { $env:TEMP }
        $outputPath = Join-Path -Path $baseOutput -ChildPath $QueueItem.AppName

        $downloadObj = [PSCustomObject]@{
            URI          = $QueueItem.Uri
            Version      = $QueueItem.Version
            Architecture = $QueueItem.Architecture
            Channel      = $QueueItem.Channel
            Platform     = $QueueItem.Platform
        }

        $saved = $downloadObj | Save-EvergreenApp -Path $outputPath -ErrorAction Stop

        $savedPath = if ($saved -and $saved.FullName) { $saved.FullName } else { $outputPath }
        Write-UILog -SyncHash $SyncHash -Message "Saved: $savedPath" -Level Info

        & $setStatus 'Done'
    }
    catch {
        Write-UILog -SyncHash $SyncHash -Message "Download failed for $($QueueItem.AppName): $_" -Level Error
        & $setStatus 'Failed'
    }
}