Adds exit-code overrides and UI for winget apps

Adds per-app control for additional accepted exit codes and ignoring non‑zero exit codes to improve handling of installers with nonstandard returns.

Exposes editable fields in the app list UI, persists them across search defaults, import/export, and pre-download save, and applies overrides during app resolution to honor configured behavior.
This commit is contained in:
rbalsleyMSFT
2025-09-18 15:15:00 -07:00
parent d1ca123104
commit d9c0c9c68e
4 changed files with 157 additions and 30 deletions
@@ -426,12 +426,16 @@ function Get-Apps {
$overrideMap = @{} $overrideMap = @{}
foreach ($app in $apps.apps) { foreach ($app in $apps.apps) {
if ($app.source -in @('winget', 'msstore')) { if ($app.source -in @('winget', 'msstore')) {
$hasCmd = ($app.PSObject.Properties['CommandLine'] -and -not [string]::IsNullOrWhiteSpace($app.CommandLine)) $hasCmd = ($app.PSObject.Properties['CommandLine'] -and -not [string]::IsNullOrWhiteSpace($app.CommandLine))
$hasArgs = ($app.PSObject.Properties['Arguments'] -and -not [string]::IsNullOrWhiteSpace($app.Arguments)) $hasArgs = ($app.PSObject.Properties['Arguments'] -and -not [string]::IsNullOrWhiteSpace($app.Arguments))
if ($hasCmd -or $hasArgs) { $hasAdd = ($app.PSObject.Properties['AdditionalExitCodes'] -and -not [string]::IsNullOrWhiteSpace($app.AdditionalExitCodes))
$hasIgnore = ($app.PSObject.Properties['IgnoreNonZeroExitCodes'])
if ($hasCmd -or $hasArgs -or $hasAdd -or $hasIgnore) {
$overrideMap[$app.name] = @{ $overrideMap[$app.name] = @{
CommandLine = if ($hasCmd) { $app.CommandLine } else { $null } CommandLine = if ($hasCmd) { $app.CommandLine } else { $null }
Arguments = if ($hasArgs) { $app.Arguments } else { $null } Arguments = if ($hasArgs) { $app.Arguments } else { $null }
AdditionalExitCodes = if ($hasAdd) { $app.AdditionalExitCodes } else { $null }
IgnoreNonZeroExitCodes = if ($hasIgnore) { [bool]$app.IgnoreNonZeroExitCodes } else { $null }
} }
} }
} }
@@ -455,6 +459,16 @@ function Get-Apps {
$entry.Arguments = $ov.Arguments $entry.Arguments = $ov.Arguments
$changed = $true $changed = $true
} }
if ($ov.ContainsKey('AdditionalExitCodes') -and $null -ne $ov.AdditionalExitCodes) {
WriteLog "Override (AppList.json) AdditionalExitCodes for $($entry.Name)"
$entry | Add-Member -NotePropertyName AdditionalExitCodes -NotePropertyValue $ov.AdditionalExitCodes -Force
$changed = $true
}
if ($ov.ContainsKey('IgnoreNonZeroExitCodes') -and $null -ne $ov.IgnoreNonZeroExitCodes) {
WriteLog "Override (AppList.json) IgnoreNonZeroExitCodes for $($entry.Name)"
$entry | Add-Member -NotePropertyName IgnoreNonZeroExitCodes -NotePropertyValue ([bool]$ov.IgnoreNonZeroExitCodes) -Force
$changed = $true
}
} }
} }
if ($changed) { if ($changed) {
@@ -885,13 +885,15 @@ function Import-ConfigSupplementalAssets {
if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch }
} }
$appsBuffer.Add([PSCustomObject]@{ $appsBuffer.Add([PSCustomObject]@{
IsSelected = $true IsSelected = $true
Name = $appInfo.name Name = $appInfo.name
Id = $appInfo.id Id = $appInfo.id
Version = "" Version = ""
Source = $appInfo.source Source = $appInfo.source
Architecture = $arch Architecture = $arch
DownloadStatus = "" AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($appInfo.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$appInfo.IgnoreNonZeroExitCodes } else { $false }
DownloadStatus = ""
}) })
} }
$State.Controls.lstWingetResults.ItemsSource = $appsBuffer.ToArray() $State.Controls.lstWingetResults.ItemsSource = $appsBuffer.ToArray()
@@ -446,6 +446,75 @@ function Initialize-DynamicUIElements {
$wingetGridView.Columns.Add($archColumn) $wingetGridView.Columns.Add($archColumn)
# --- END: Add Architecture Column --- # --- END: Add Architecture Column ---
# --- START: Add Additional Exit Codes Column ---
$exitCodesColumn = New-Object System.Windows.Controls.GridViewColumn
$exitCodesHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$exitCodesHeader.Tag = "AdditionalExitCodes"
$exitCodesHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$exitHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Additional Exit Codes")
$exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5,2,5,2)))
$exitHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$exitHeaderTemplate = New-Object System.Windows.DataTemplate
$exitHeaderTemplate.VisualTree = $exitHeaderTextFactory
$exitCodesHeader.ContentTemplate = $exitHeaderTemplate
$exitCodesColumn.Header = $exitCodesHeader
$exitCodesColumn.Width = 140
$exitCodesCellTemplate = New-Object System.Windows.DataTemplate
$exitCodesTextBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBox])
$exitBinding = New-Object System.Windows.Data.Binding("AdditionalExitCodes")
$exitBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
$exitCodesTextBoxFactory.SetBinding([System.Windows.Controls.TextBox]::TextProperty, $exitBinding)
$exitCodesCellTemplate.VisualTree = $exitCodesTextBoxFactory
$exitCodesColumn.CellTemplate = $exitCodesCellTemplate
$wingetGridView.Columns.Add($exitCodesColumn)
# --- END: Add Additional Exit Codes Column ---
# --- START: Add Ignore Non-Zero Exit Codes Column ---
$ignoreColumn = New-Object System.Windows.Controls.GridViewColumn
$ignoreHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$ignoreHeader.Tag = "IgnoreNonZeroExitCodes"
$ignoreHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
$ignoreHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Ignore Exit Codes")
$ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5,2,5,2)))
$ignoreHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$ignoreHeaderTemplate = New-Object System.Windows.DataTemplate
$ignoreHeaderTemplate.VisualTree = $ignoreHeaderTextFactory
$ignoreHeader.ContentTemplate = $ignoreHeaderTemplate
$ignoreColumn.Header = $ignoreHeader
$ignoreColumn.Width = 140
$ignoreCellTemplate = New-Object System.Windows.DataTemplate
# Center the checkbox in the cell
$ignoreCellGridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid])
$ignoreCellGridFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)
$ignoreCellGridFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch)
$ignoreCheckFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
$ignoreCheckFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
$ignoreCheckFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$ignoreBinding = New-Object System.Windows.Data.Binding("IgnoreNonZeroExitCodes")
$ignoreBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
$ignoreCheckFactory.SetBinding([System.Windows.Controls.Primitives.ToggleButton]::IsCheckedProperty, $ignoreBinding)
# Build the visual tree: Grid -> CheckBox
$ignoreCellGridFactory.AppendChild($ignoreCheckFactory)
$ignoreCellTemplate.VisualTree = $ignoreCellGridFactory
$ignoreColumn.CellTemplate = $ignoreCellTemplate
$wingetGridView.Columns.Add($ignoreColumn)
# --- END: Add Ignore Non-Zero Exit Codes Column ---
Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left
$State.Controls.lstWingetResults.AddHandler( $State.Controls.lstWingetResults.AddHandler(
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent, [System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
@@ -98,10 +98,12 @@ function Save-WingetList {
$appList = @{ $appList = @{
apps = @($selectedApps | ForEach-Object { apps = @($selectedApps | ForEach-Object {
[ordered]@{ [ordered]@{
name = (ConvertTo-SafeName -Name $_.Name) name = (ConvertTo-SafeName -Name $_.Name)
id = $_.Id id = $_.Id
source = $_.Source.ToLower() source = $_.Source.ToLower()
architecture = $_.Architecture architecture = $_.Architecture
AdditionalExitCodes = if ($_.PSObject.Properties['AdditionalExitCodes']) { $_.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($_.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$_.IgnoreNonZeroExitCodes } else { $false }
} }
}) })
} }
@@ -148,13 +150,15 @@ function Import-WingetList {
foreach ($appInfo in $importedAppsData.apps) { foreach ($appInfo in $importedAppsData.apps) {
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else { if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } } $arch = if ($appInfo.source -eq 'msstore') { 'NA' } else { if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } }
$newAppListForItemsSource.Add([PSCustomObject]@{ $newAppListForItemsSource.Add([PSCustomObject]@{
IsSelected = $true # Imported apps are marked as selected IsSelected = $true # Imported apps are marked as selected
Name = $appInfo.name Name = $appInfo.name
Id = $appInfo.id Id = $appInfo.id
Version = "" # Will be populated when searching or if data exists Version = "" # Will be populated when searching or if data exists
Source = $appInfo.source Source = $appInfo.source
Architecture = $arch Architecture = $arch
DownloadStatus = "" AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($appInfo.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$appInfo.IgnoreNonZeroExitCodes } else { $false }
DownloadStatus = ""
}) })
} }
} }
@@ -191,13 +195,15 @@ function Search-WingetPackagesPublic {
$output = $results | ForEach-Object -Parallel { $output = $results | ForEach-Object -Parallel {
$arch = if ($_.Source -eq 'msstore') { 'NA' } else { $using:DefaultArchitecture } $arch = if ($_.Source -eq 'msstore') { 'NA' } else { $using:DefaultArchitecture }
[PSCustomObject]@{ [PSCustomObject]@{
IsSelected = [bool]$false IsSelected = [bool]$false
Name = [string]$_.Name Name = [string]$_.Name
Id = [string]$_.Id Id = [string]$_.Id
Version = [string]$_.Version Version = [string]$_.Version
Source = [string]$_.Source Source = [string]$_.Source
Architecture = [string]$arch Architecture = [string]$arch
DownloadStatus = [string]::Empty AdditionalExitCodes = [string]::Empty
IgnoreNonZeroExitCodes = [bool]$false
DownloadStatus = [string]::Empty
} }
} -ThrottleLimit 20 } -ThrottleLimit 20
WriteLog "Winget search completed. Created $($output.Count) output objects." WriteLog "Winget search completed. Created $($output.Count) output objects."
@@ -724,6 +730,42 @@ function Invoke-WingetDownload {
# Select only necessary properties before passing to Invoke-ParallelProcessing # Select only necessary properties before passing to Invoke-ParallelProcessing
$itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version, Architecture # Include Version and Architecture if needed $itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version, Architecture # Include Version and Architecture if needed
# Before downloading, persist the selected apps to AppList.json including exit-code fields (parity with Save-WingetList)
try {
# Determine AppList.json path; default if empty
if ([string]::IsNullOrWhiteSpace($localAppListJsonPath)) {
$localAppListJsonPath = Join-Path -Path $localAppsPath -ChildPath "AppList.json"
$taskArguments.AppListJsonPath = $localAppListJsonPath
WriteLog "AppListJsonPath was empty. Defaulting to: $localAppListJsonPath"
}
# Build apps payload from current selection, preserving AdditionalExitCodes/IgnoreNonZeroExitCodes
$appListToSave = @{
apps = @($selectedApps | ForEach-Object {
[ordered]@{
name = (ConvertTo-SafeName -Name $_.Name)
id = $_.Id
source = $_.Source.ToLower()
architecture = $_.Architecture
AdditionalExitCodes = if ($_.PSObject.Properties['AdditionalExitCodes']) { $_.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($_.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$_.IgnoreNonZeroExitCodes } else { $false }
}
})
}
# Ensure destination directory exists and write AppList.json
$destDir = Split-Path -Parent $localAppListJsonPath
if (-not (Test-Path -LiteralPath $destDir)) {
[void][System.IO.Directory]::CreateDirectory($destDir)
}
$appListToSave | ConvertTo-Json -Depth 10 | Set-Content -Path $localAppListJsonPath -Encoding UTF8
WriteLog "Persisted AppList.json with selected apps and exit-code fields to: $localAppListJsonPath"
}
catch {
WriteLog "Warning: Failed to persist AppList.json prior to download. Error: $($_.Exception.Message)"
}
# Invoke the centralized parallel processing function # Invoke the centralized parallel processing function
# Pass task type and task-specific arguments # Pass task type and task-specific arguments
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess ` Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `