From 8d7e4d106620761d0ae1a5133f6d6ba301131471 Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:10:39 -0700 Subject: [PATCH] Refactor config loading and improve error handling Extracts the logic for importing supplemental assets (Winget, BYO, Drivers) into a new reusable function. This function is now called by both the manual and automatic configuration loaders, reducing code duplication. Enhances the manual configuration loading process with more robust error handling. It now provides specific user-facing error messages for file read failures, empty files, and invalid JSON, improving the user experience when loading a malformed configuration. When loading a configuration, if optional supplemental files like AppList.json are referenced but not found, an informational message is now displayed to the user instead of failing silently. --- .../FFUUI.Core/FFUUI.Core.Config.psm1 | 279 ++++++++++-------- 1 file changed, 162 insertions(+), 117 deletions(-) diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 index 9ba53ad..68f536b 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 @@ -243,19 +243,39 @@ function Invoke-LoadConfiguration { WriteLog "Load configuration cancelled by user." return } - WriteLog "Loading configuration from: $filePath" - $configContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json - + $raw = $null + try { + $raw = Get-Content -Path $filePath -Raw -ErrorAction Stop + } + catch { + WriteLog "LoadConfig Error: Failed reading file $filePath : $($_.Exception.Message)" + [System.Windows.MessageBox]::Show("Failed to read the configuration file.`n$($_.Exception.Message)", "Load Error", "OK", "Error") + return + } + if ([string]::IsNullOrWhiteSpace($raw)) { + WriteLog "LoadConfig Error: File $filePath is empty." + [System.Windows.MessageBox]::Show("The selected configuration file is empty.", "Load Error", "OK", "Error") + return + } + $configContent = $null + try { + $configContent = $raw | ConvertFrom-Json -ErrorAction Stop + } + catch { + WriteLog "LoadConfig Error: JSON parse failure for $filePath : $($_.Exception.Message)" + [System.Windows.MessageBox]::Show("Failed to parse the configuration file (invalid JSON).`n$($_.Exception.Message)", "Load Error", "OK", "Error") + return + } if ($null -eq $configContent) { - WriteLog "LoadConfig Error: configContent is null after parsing $filePath. File might be empty or malformed." - [System.Windows.MessageBox]::Show("Failed to parse the configuration file. It might be empty or not valid JSON.", "Load Error", "OK", "Error") + WriteLog "LoadConfig Error: Parsed config object is null after $filePath." + [System.Windows.MessageBox]::Show("Parsed configuration object was null.", "Load Error", "OK", "Error") return } WriteLog "LoadConfig: Successfully parsed config file. Top-level keys: $($configContent.PSObject.Properties.Name -join ', ')" - - # Apply the configuration to the UI Update-UIFromConfig -ConfigContent $configContent -State $State + $State.Data.lastConfigFilePath = $filePath + Import-ConfigSupplementalAssets -ConfigContent $configContent -State $State -ShowWarnings:$true } catch { WriteLog "LoadConfig FATAL Error: $($_.Exception.ToString())" @@ -792,55 +812,69 @@ function Invoke-AutoLoadPreviousEnvironment { [Parameter(Mandatory = $true)] [psobject]$State ) - try { $ffuDevRoot = $State.FFUDevelopmentPath if ([string]::IsNullOrWhiteSpace($ffuDevRoot)) { WriteLog "AutoLoad: FFUDevelopmentPath not set; skipping." return } - $configPath = Join-Path $ffuDevRoot "config\FFUConfig.json" if (-not (Test-Path -LiteralPath $configPath)) { WriteLog "AutoLoad: No existing FFUConfig.json found at $configPath." return } - WriteLog "AutoLoad: Found config file at $configPath. Parsing..." + $raw = Get-Content -Path $configPath -Raw -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($raw)) { + WriteLog "AutoLoad: Config file empty; aborting." + return + } $configContent = $null try { - $raw = Get-Content -Path $configPath -Raw -ErrorAction Stop - if ([string]::IsNullOrWhiteSpace($raw)) { - WriteLog "AutoLoad: Config file is empty; aborting auto-load." - return - } $configContent = $raw | ConvertFrom-Json -ErrorAction Stop } catch { - WriteLog "AutoLoad: Failed to parse config JSON: $($_.Exception.Message)" + WriteLog "AutoLoad: JSON parse failed: $($_.Exception.Message)" return } - if ($null -eq $configContent) { - WriteLog "AutoLoad: Parsed configContent is null; aborting." + WriteLog "AutoLoad: Parsed object null; aborting." return } - - WriteLog "AutoLoad: Applying configuration to UI." + WriteLog "AutoLoad: Applying core configuration." Update-UIFromConfig -ConfigContent $configContent -State $State + $State.Data.lastConfigFilePath = $configPath + Import-ConfigSupplementalAssets -ConfigContent $configContent -State $State -ShowWarnings:$false + WriteLog "AutoLoad: Completed supplemental import with warnings disabled." + } + catch { + WriteLog "AutoLoad: Unexpected failure: $($_.Exception.ToString())" + } +} - # Track which supplemental assets we successfully load - $loadedWinget = $false - $loadedBYO = $false - $loadedDrivers = $false +function Import-ConfigSupplementalAssets { + param( + [Parameter(Mandatory = $true)] + [psobject]$ConfigContent, + [Parameter(Mandatory = $true)] + [psobject]$State, + [Parameter()] + [bool]$ShowWarnings = $false + ) + WriteLog "SupplementalImport: Starting import of helper assets." + $loadedWinget = $false + $loadedBYO = $false + $loadedDrivers = $false + $missing = New-Object System.Collections.Generic.List[string] - # --- Winget AppList --- - $appListPath = $null - if ($configContent.PSObject.Properties.Match('AppListPath').Count -gt 0) { - $appListPath = $configContent.AppListPath - } - if (-not [string]::IsNullOrWhiteSpace($appListPath) -and (Test-Path -LiteralPath $appListPath)) { - WriteLog "AutoLoad: Loading Winget AppList from $appListPath." + # Winget AppList + $appListPath = $null + if ($ConfigContent.PSObject.Properties.Match('AppListPath').Count -gt 0) { + $appListPath = $ConfigContent.AppListPath + } + if (-not [string]::IsNullOrWhiteSpace($appListPath)) { + if (Test-Path -LiteralPath $appListPath) { + WriteLog "SupplementalImport: Loading Winget AppList from $appListPath" try { $importedAppsData = Get-Content -Path $appListPath -Raw | ConvertFrom-Json -ErrorAction Stop if ($null -ne $importedAppsData -and $null -ne $importedAppsData.apps) { @@ -862,35 +896,39 @@ function Invoke-AutoLoadPreviousEnvironment { } $State.Controls.lstWingetResults.ItemsSource = $appsBuffer.ToArray() $loadedWinget = $true - WriteLog "AutoLoad: Winget AppList loaded with $($appsBuffer.Count) entries." - # Ensure Winget search/list panel is visible when apps are present if ($null -ne $State.Controls.wingetSearchPanel) { $State.Controls.wingetSearchPanel.Visibility = 'Visible' } - # Update the Select All header checkbox state if present if ($null -ne $State.Controls.chkSelectAllWingetResults -and (Get-Command -Name Update-SelectAllHeaderCheckBoxState -ErrorAction SilentlyContinue)) { Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstWingetResults -HeaderCheckBox $State.Controls.chkSelectAllWingetResults } + WriteLog "SupplementalImport: Winget list loaded with $($appsBuffer.Count) entries." } else { - WriteLog "AutoLoad: AppList JSON did not contain an 'apps' array." + WriteLog "SupplementalImport: Winget AppList missing 'apps' array." } } catch { - WriteLog "AutoLoad: Failed loading Winget AppList ($appListPath): $($_.Exception.Message)" + WriteLog "SupplementalImport: Failed loading Winget AppList ($appListPath): $($_.Exception.Message)" } } else { - WriteLog "AutoLoad: AppListPath not set or file missing." + WriteLog "SupplementalImport: Winget AppList file missing: $appListPath" + $missing.Add("Winget AppList (AppListPath): $appListPath") } + } + else { + WriteLog "SupplementalImport: AppListPath not defined in config." + } - # --- BYO UserAppList (UserAppList.json) --- - $userAppListPath = $null - if ($configContent.PSObject.Properties.Match('UserAppListPath').Count -gt 0) { - $userAppListPath = $configContent.UserAppListPath - } - if (-not [string]::IsNullOrWhiteSpace($userAppListPath) -and (Test-Path -LiteralPath $userAppListPath)) { - WriteLog "AutoLoad: Loading UserAppList from $userAppListPath." + # UserAppList (BYO) + $userAppListPath = $null + if ($ConfigContent.PSObject.Properties.Match('UserAppListPath').Count -gt 0) { + $userAppListPath = $ConfigContent.UserAppListPath + } + if (-not [string]::IsNullOrWhiteSpace($userAppListPath)) { + if (Test-Path -LiteralPath $userAppListPath) { + WriteLog "SupplementalImport: Loading UserAppList from $userAppListPath" try { $applications = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json -ErrorAction Stop if ($applications) { @@ -912,7 +950,6 @@ function Invoke-AutoLoadPreviousEnvironment { CopyStatus = "" }) } - # Reorder priorities sequentially if (Get-Command -Name Update-ListViewPriorities -ErrorAction SilentlyContinue) { Update-ListViewPriorities -ListView $listView } @@ -923,27 +960,33 @@ function Invoke-AutoLoadPreviousEnvironment { Update-BYOAppsActionButtonsState -State $State } $loadedBYO = $true - WriteLog "AutoLoad: UserAppList loaded with $($listView.Items.Count) entries." + WriteLog "SupplementalImport: UserAppList loaded with $($listView.Items.Count) entries." } else { - WriteLog "AutoLoad: UserAppList JSON empty or null." + WriteLog "SupplementalImport: UserAppList JSON empty." } } catch { - WriteLog "AutoLoad: Failed loading UserAppList ($userAppListPath): $($_.Exception.Message)" + WriteLog "SupplementalImport: Failed loading UserAppList ($userAppListPath): $($_.Exception.Message)" } } else { - WriteLog "AutoLoad: UserAppListPath not set or file missing." + WriteLog "SupplementalImport: UserAppList file missing: $userAppListPath" + $missing.Add("UserAppList (UserAppListPath): $userAppListPath") } + } + else { + WriteLog "SupplementalImport: UserAppListPath not defined in config." + } - # --- Drivers (DriversJsonPath) --- - $driversJsonPath = $null - if ($configContent.PSObject.Properties.Match('DriversJsonPath').Count -gt 0) { - $driversJsonPath = $configContent.DriversJsonPath - } - if (-not [string]::IsNullOrWhiteSpace($driversJsonPath) -and (Test-Path -LiteralPath $driversJsonPath)) { - WriteLog "AutoLoad: Loading Drivers JSON from $driversJsonPath." + # Drivers JSON + $driversJsonPath = $null + if ($ConfigContent.PSObject.Properties.Match('DriversJsonPath').Count -gt 0) { + $driversJsonPath = $ConfigContent.DriversJsonPath + } + if (-not [string]::IsNullOrWhiteSpace($driversJsonPath)) { + if (Test-Path -LiteralPath $driversJsonPath) { + WriteLog "SupplementalImport: Loading Drivers JSON from $driversJsonPath" try { $rawDrivers = Get-Content -Path $driversJsonPath -Raw | ConvertFrom-Json -ErrorAction Stop if ($rawDrivers -and $rawDrivers.PSObject.Properties.Count -gt 0) { @@ -951,16 +994,13 @@ function Invoke-AutoLoadPreviousEnvironment { foreach ($makeProp in $rawDrivers.PSObject.Properties) { $makeName = $makeProp.Name $makeObject = $makeProp.Value - if ($null -eq $makeObject -or -not ($makeObject.PSObject.Properties['Models'])) { - continue - } + if ($null -eq $makeObject -or -not ($makeObject.PSObject.Properties['Models'])) { continue } $models = $makeObject.Models if ($models -and ($models -is [System.Collections.IEnumerable])) { foreach ($modelEntry in $models) { if ($null -eq $modelEntry -or -not ($modelEntry.PSObject.Properties['Name'])) { continue } $modelName = $modelEntry.Name if ([string]::IsNullOrWhiteSpace($modelName)) { continue } - $driverObj = [PSCustomObject]@{ IsSelected = $true Make = $makeName @@ -971,7 +1011,6 @@ function Invoke-AutoLoadPreviousEnvironment { MachineType = if ($modelEntry.PSObject.Properties['MachineType']) { $modelEntry.MachineType } else { $null } Id = if ($modelEntry.PSObject.Properties['Id']) { $modelEntry.Id } else { $null } } - $State.Data.allDriverModels.Add($driverObj) } } @@ -983,76 +1022,82 @@ function Invoke-AutoLoadPreviousEnvironment { Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstDriverModels -HeaderCheckBox $headerChk } } - $loadedDrivers = $true - WriteLog "AutoLoad: Loaded $($State.Data.allDriverModels.Count) driver model entries." - - # Ensure driver-related panels are visible since models were auto-loaded - if ($null -ne $State.Controls.spModelFilterSection) { $State.Controls.spModelFilterSection.Visibility = 'Visible' } - if ($null -ne $State.Controls.lstDriverModels) { $State.Controls.lstDriverModels.Visibility = 'Visible' } - if ($null -ne $State.Controls.spDriverActionButtons) { $State.Controls.spDriverActionButtons.Visibility = 'Visible' } - - # Optionally set Make combo to the first make represented in the loaded list (if none selected yet) - try { - if ($State.Controls.cmbMake.SelectedIndex -lt 0 -and $State.Data.allDriverModels.Count -gt 0) { - $firstMake = ($State.Data.allDriverModels | Select-Object -First 1).Make - if (-not [string]::IsNullOrWhiteSpace($firstMake)) { - $makeItem = $State.Controls.cmbMake.Items | Where-Object { $_ -eq $firstMake } | Select-Object -First 1 - if ($makeItem) { $State.Controls.cmbMake.SelectedItem = $makeItem } + if ($State.Data.allDriverModels.Count -gt 0) { + if ($null -ne $State.Controls.spModelFilterSection) { $State.Controls.spModelFilterSection.Visibility = 'Visible' } + if ($null -ne $State.Controls.lstDriverModels) { $State.Controls.lstDriverModels.Visibility = 'Visible' } + if ($null -ne $State.Controls.spDriverActionButtons) { $State.Controls.spDriverActionButtons.Visibility = 'Visible' } + try { + if ($State.Controls.cmbMake.SelectedIndex -lt 0 -and $State.Data.allDriverModels.Count -gt 0) { + $firstMake = ($State.Data.allDriverModels | Select-Object -First 1).Make + if (-not [string]::IsNullOrWhiteSpace($firstMake)) { + $makeItem = $State.Controls.cmbMake.Items | Where-Object { $_ -eq $firstMake } | Select-Object -First 1 + if ($makeItem) { $State.Controls.cmbMake.SelectedItem = $makeItem } + } } } + catch { + WriteLog "SupplementalImport: Non-fatal error selecting first Make: $($_.Exception.Message)" + } } - catch { - WriteLog "AutoLoad: Non-fatal error setting Make selection: $($_.Exception.Message)" - } + $loadedDrivers = $true + WriteLog "SupplementalImport: Loaded $($State.Data.allDriverModels.Count) driver models." } else { - WriteLog "AutoLoad: Drivers JSON empty or did not contain expected structure." + WriteLog "SupplementalImport: Drivers JSON empty or structure unexpected." } } catch { - WriteLog "AutoLoad: Failed loading Drivers JSON ($driversJsonPath): $($_.Exception.Message)" + WriteLog "SupplementalImport: Failed loading Drivers JSON ($driversJsonPath): $($_.Exception.Message)" } } else { - WriteLog "AutoLoad: DriversJsonPath not set or file missing." + WriteLog "SupplementalImport: Drivers JSON file missing: $driversJsonPath" + $missing.Add("Drivers (DriversJsonPath): $driversJsonPath") } - - # Set checkboxes based on what was loaded - if ($loadedWinget -or $loadedBYO) { - $State.Controls.chkInstallApps.IsChecked = $true - } - if ($loadedWinget) { - $State.Controls.chkInstallWingetApps.IsChecked = $true - } - if ($loadedBYO) { - $State.Controls.chkBringYourOwnApps.IsChecked = $true - } - if ($loadedDrivers) { - $State.Controls.chkDownloadDrivers.IsChecked = $true - } - - # Re-run panel visibility/state helpers - if (Get-Command -Name Update-ApplicationPanelVisibility -ErrorAction SilentlyContinue) { - Update-ApplicationPanelVisibility -State $State -TriggeringControlName 'AutoLoad' - } - if (Get-Command -Name Update-DriverDownloadPanelVisibility -ErrorAction SilentlyContinue) { - Update-DriverDownloadPanelVisibility -State $State - } - if (Get-Command -Name Update-DriverCheckboxStates -ErrorAction SilentlyContinue) { - Update-DriverCheckboxStates -State $State - } - if (Get-Command -Name Update-OfficePanelVisibility -ErrorAction SilentlyContinue) { - Update-OfficePanelVisibility -State $State - } - if (Get-Command -Name Update-CopyButtonState -ErrorAction SilentlyContinue) { - Update-CopyButtonState -State $State - } - - WriteLog "AutoLoad: Completed. Loaded Config=$true; Winget=$loadedWinget; BYO=$loadedBYO; Drivers=$loadedDrivers." } - catch { - WriteLog "AutoLoad: Unexpected failure: $($_.Exception.ToString())" + else { + WriteLog "SupplementalImport: DriversJsonPath not defined in config." } + + if ($loadedWinget -or $loadedBYO) { + $State.Controls.chkInstallApps.IsChecked = $true + } + if ($loadedWinget) { + $State.Controls.chkInstallWingetApps.IsChecked = $true + } + if ($loadedBYO) { + $State.Controls.chkBringYourOwnApps.IsChecked = $true + } + if ($loadedDrivers) { + $State.Controls.chkDownloadDrivers.IsChecked = $true + } + + if (Get-Command -Name Update-ApplicationPanelVisibility -ErrorAction SilentlyContinue) { + Update-ApplicationPanelVisibility -State $State -TriggeringControlName 'SupplementalImport' + } + if (Get-Command -Name Update-DriverDownloadPanelVisibility -ErrorAction SilentlyContinue) { + Update-DriverDownloadPanelVisibility -State $State + } + if (Get-Command -Name Update-DriverCheckboxStates -ErrorAction SilentlyContinue) { + Update-DriverCheckboxStates -State $State + } + if (Get-Command -Name Update-OfficePanelVisibility -ErrorAction SilentlyContinue) { + Update-OfficePanelVisibility -State $State + } + if (Get-Command -Name Update-CopyButtonState -ErrorAction SilentlyContinue) { + Update-CopyButtonState -State $State + } + + # Updated message to clarify successful load and that missing helper files are optional if not yet created. + if ($ShowWarnings -and $missing.Count -gt 0) { + $msg = "Configuration file loaded successfully.`n`n" + + "Optional helper file(s) referenced in the configuration were not found:`n" + + ($missing | ForEach-Object { "- $_" } | Out-String) + + "`nThese files are optional. They won't exist until you create Winget (AppList.json), User (UserAppList.json), or Driver (Drivers.json) manifests. You can create them later or ignore this message." + [System.Windows.MessageBox]::Show($msg.TrimEnd(), "Configuration Loaded - Optional Files Missing", "OK", "Information") | Out-Null + } + + WriteLog ("SupplementalImport: Complete. Winget={0} BYO={1} Drivers={2} Missing={3}" -f $loadedWinget, $loadedBYO, $loadedDrivers, $missing.Count) } Export-ModuleMember -Function * \ No newline at end of file