From defd744ef0cdd87612198934aafee6205a76ab7e Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:28:12 -0700 Subject: [PATCH] Refactor winget download logic and error handling Improves the reliability and clarity of the application download process. - Introduces specific error codes for download failures, such as publisher restrictions from the Microsoft Store. - Blocks downloading the Company Portal app from the `winget` source due to packaging issues and provides a user-friendly message. - Refines status messages in the UI for better user feedback. - Installs the WinGet PowerShell module for all users to prevent context-related issues. --- .../AppList_InboxAppsSample_Win11_24H2.json | 149 ++++++++++++++++++ .../FFU.Common/FFU.Common.Winget.psm1 | 42 ++--- .../FFUUI.Core/FFUUI.Core.Winget.psm1 | 28 ++-- 3 files changed, 187 insertions(+), 32 deletions(-) create mode 100644 FFUDevelopment/Apps/AppList_InboxAppsSample_Win11_24H2.json diff --git a/FFUDevelopment/Apps/AppList_InboxAppsSample_Win11_24H2.json b/FFUDevelopment/Apps/AppList_InboxAppsSample_Win11_24H2.json new file mode 100644 index 0000000..8e84aac --- /dev/null +++ b/FFUDevelopment/Apps/AppList_InboxAppsSample_Win11_24H2.json @@ -0,0 +1,149 @@ +{ + "apps": [ + { + "name": "Windows Notepad", + "id": "9MSMLRH6LZF3", + "source": "msstore" + }, + { + "name": "Windows Client Web Experience", + "id": "9MSSGKG348SP", + "source": "msstore" + }, + { + "name": "Screen Sketch", + "id": "9MZ95KL8MR0L", + "source": "msstore" + }, + { + "name": "Windows Terminal", + "id": "9N0DX20HK701", + "source": "msstore" + }, + { + "name": "VP9 Video Extensions", + "id": "9N4D0MSMP0PT", + "source": "msstore" + }, + { + "name": "MPEG2 Video Extension", + "id": "9N95Q1ZZPMH4", + "source": "msstore" + }, + { + "name": "Store Purchase App", + "id": "9NBLGGH4LS1F", + "source": "msstore" + }, + { + "name": "Windows Feedback Hub", + "id": "9NBLGGH4R32N", + "source": "msstore" + }, + { + "name": "Todos", + "id": "9NBLGGH5R558", + "source": "msstore" + }, + { + "name": "Raw Image Extension", + "id": "9NCTDW2W1BH8", + "source": "msstore" + }, + { + "name": "Power Automate Desktop", + "id": "9NFTCH6J7FHV", + "source": "msstore" + }, + { + "name": "Xbox TCUI", + "id": "9NKNC0LD5NN6", + "source": "msstore" + }, + { + "name": "Your Phone", + "id": "9NMPJ99VJBWV", + "source": "msstore" + }, + { + "name": "Xbox Gaming Overlay", + "id": "9NZKPSTSNW4P", + "source": "msstore" + }, + { + "name": "Clipchamp", + "id": "9P1J8S7CCWWT", + "source": "msstore" + }, + { + "name": "Paint", + "id": "9PCFS5B6T72H", + "source": "msstore" + }, + { + "name": "Webp Image Extension", + "id": "9PG2DK419DRG", + "source": "msstore" + }, + { + "name": "Get Help", + "id": "9PKDZBMV1H3T", + "source": "msstore" + }, + { + "name": "HEIF Image Extension", + "id": "9PMMSR1CGPWG", + "source": "msstore" + }, + { + "name": "Xbox Identity Provider", + "id": "9WZDNCRD1HKW", + "source": "msstore" + }, + { + "name": "Microsoft Office Hub", + "id": "9WZDNCRD29V9", + "source": "msstore" + }, + { + "name": "Bing News", + "id": "9WZDNCRFHVFW", + "source": "msstore" + }, + { + "name": "Windows Sound Recorder", + "id": "9WZDNCRFHWKN", + "source": "msstore" + }, + { + "name": "Windows Alarms", + "id": "9WZDNCRFJ3PR", + "source": "msstore" + }, + { + "name": "Zune Music", + "id": "9WZDNCRFJ3PT", + "source": "msstore" + }, + { + "name": "Bing Weather", + "id": "9WZDNCRFJ3Q2", + "source": "msstore" + }, + { + "name": "Windows Camera", + "id": "9WZDNCRFJBBG", + "source": "msstore" + }, + { + "name": "Windows Photos", + "id": "9WZDNCRFJBH4", + "source": "msstore" + }, + { + "name": "Windows Store", + "id": "9WZDNCRFJBMP", + "source": "msstore" + } + ] +} \ No newline at end of file diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 index 9804978..0319ffe 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 @@ -25,6 +25,13 @@ function Get-Application { [string]$OrchestrationPath ) + # Block Company Portal from winget source + # I refuse to code around the poor packaging of this app + if ($AppId -eq 'Microsoft.CompanyPortal' -and $Source -eq 'winget') { + WriteLog "Skipping download of Company Portal from the 'winget' source. This version has packaging inconsistencies. Please use the 'msstore' source instead." + return 4 # Return specific error code for this case + } + # Determine base folder path for checking existence $appIsWin32ForCheck = ($Source -eq 'msstore' -and $AppId.StartsWith("XP")) $appBaseFolderPathForCheck = "" @@ -100,32 +107,29 @@ function Get-Application { if ($wingetDownloadResult.status -eq 'NoApplicableInstallers' -or $wingetDownloadResult.status -eq 'NoApplicableInstallerFound') { WriteLog "No installer found for $arch architecture. Attempting to download without specifying architecture..." $wingetDownloadResult = Export-WinGetPackage -id $AppId -DownloadDirectory $appFolderPath -Source $Source - if ($wingetDownloadResult.status -eq 'Ok') { - WriteLog "Downloaded $AppName without specifying architecture." - } - else { - WriteLog "ERROR: No installer found for $AppName. Exiting" - Remove-Item -Path $appFolderPath -Recurse -Force - return 1 # Return error code - } } - # Handle Store-specific errors - elseif ($Source -eq 'msstore') { - # If download not supported by publisher - if ($wingetDownloadResult.ExtendedErrorCode -match '0x8A150084') { - $errorMessage = "ERROR: The Microsoft Store app $AppName does not support downloads by the publisher. Please remove it from the AppList.json. If there's a winget source version of the application, try using that instead. Exiting." + + # Re-evaluate status after potential second attempt + if ($wingetDownloadResult.status -ne 'Ok') { + # Handle Store-specific publisher restriction error + if ($Source -eq 'msstore' -and $wingetDownloadResult.ExtendedErrorCode -match '0x8A150084') { + $errorMessage = "The Microsoft Store app $AppName does not support downloads by the publisher. Please remove it from the AppList.json. If there's a winget source version of the application, try using that instead. Exiting." WriteLog $errorMessage Remove-Item -Path $appFolderPath -Recurse -Force Write-Error $errorMessage - return 1 # Return error code + return 3 # Return specific error code for publisher restriction + } + # Handle other download failures + else { + $errormsg = "Download failed for $AppName with status: $($wingetDownloadResult.status) $($wingetDownloadResult.ExtendedErrorCode)" + WriteLog $errormsg + Remove-Item -Path $appFolderPath -Recurse -Force + Write-Error $errormsg + return 1 # Return generic error code } } else { - $errormsg = "ERROR: Download failed for $AppName with status: $($wingetDownloadResult.status) $($wingetDownloadResult.ExtendedErrorCode)" - WriteLog $errormsg - Remove-Item -Path $appFolderPath -Recurse -Force - Write-Error $errormsg - return 1 # Return error code + WriteLog "Downloaded $AppName without specifying architecture." } } diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 index 70eec49..9a0cf69 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 @@ -245,7 +245,7 @@ function Install-WingetComponents { } # Install/Update the module - Install-Module -Name Microsoft.WinGet.Client -Force -Repository 'PSGallery' + Install-Module -Name Microsoft.WinGet.Client -Force -Repository 'PSGallery' -Scope AllUsers # Restore original PSGallery trust setting if ($PSGalleryTrust -eq 'Untrusted') { @@ -400,7 +400,7 @@ function Start-WingetAppDownloadTask { } else { $appFound = $true - $status = "Error: App in '$userAppListPath' but content missing/small in '$appFolder'. Copy content or remove from UserAppList.json." + $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 } @@ -408,7 +408,7 @@ function Start-WingetAppDownloadTask { } else { $appFound = $true - $status = "Error: App in '$userAppListPath' but content folder '$appFolder' not found. Copy content or remove from UserAppList.json." + $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 } @@ -430,10 +430,10 @@ function Start-WingetAppDownloadTask { # Check if app already exists in WinGetWin32Apps.json # For multi-arch apps, there might be entries like "AppName (x86)" and "AppName (x64)" $existingWin32Entries = @($wingetAppsJson | Where-Object { - $_.Name -eq $appName -or - $_.Name -eq "$appName (x86)" -or - $_.Name -eq "$appName (x64)" - }) + $_.Name -eq $appName -or + $_.Name -eq "$appName (x86)" -or + $_.Name -eq "$appName (x64)" + }) if ($existingWin32Entries.Count -gt 0) { $appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName @@ -473,7 +473,7 @@ function Start-WingetAppDownloadTask { else { # App entry exists in WinGetWin32Apps.json but folder is missing or incomplete $appFound = $true - $status = "Error: App in '$wingetWin32jsonFile' but content folder '$appFolder' not found or incomplete. Remove entry from WinGetWin32Apps.json or restore content." + $status = "App in '$wingetWin32jsonFile' but content folder '$appFolder' not found or incomplete. 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 } @@ -571,7 +571,7 @@ function Start-WingetAppDownloadTask { } catch { WriteLog "Error saving '$AppListJsonPath'. Error: $($_.Exception.Message)" - $status = "Error saving AppList.json" + $status = "Failed to save AppList.json: $($_.Exception.Message)" Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 } } @@ -609,8 +609,10 @@ function Start-WingetAppDownloadTask { # Determine status based on result code switch ($resultCode) { 0 { $status = "Downloaded successfully" } - 1 { $status = "Error: No win32 app installers were found" } + 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 } @@ -633,7 +635,7 @@ function Start-WingetAppDownloadTask { } } catch { - $status = "Error: $($_.Exception.Message)" + $status = $_.Exception.Message WriteLog "Download error for $($appName): $($_.Exception.Message)" $resultCode = 1 # Indicate error # Enqueue error status @@ -662,7 +664,7 @@ function Start-WingetAppDownloadTask { } catch { - $status = "Error: $($_.Exception.Message)" + $status = $_.Exception.Message WriteLog "Unexpected error in Start-WingetAppDownloadTask for $($appName): $($_.Exception.Message)" $resultCode = 1 # Indicate error # Enqueue error status @@ -671,7 +673,7 @@ function Start-WingetAppDownloadTask { finally { # Ensure status is not empty before returning if ([string]::IsNullOrEmpty($status)) { - $status = "Error: Unknown failure" # Provide a default error 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