Feat: Add configurable thread limit for parallel operations

Adds a "Threads" setting to the UI, allowing users to control the throttle limit for parallel tasks like driver and application processing.

This introduces a new textbox in the build options and updates the parallel processing function to use this configurable value instead of a hardcoded one.

Input validation is also added to ensure the threads value is a valid integer and is at least 1. The new setting is integrated into the configuration save/load functionality.
This commit is contained in:
rbalsleyMSFT
2025-07-18 13:45:58 -07:00
parent e639cee4ee
commit 7cc7919da4
11 changed files with 119 additions and 34 deletions
@@ -300,7 +300,8 @@ function Invoke-CopyBYOApps {
-CompletedStatusText "Copied" `
-ErrorStatusPrefix "Error: " `
-WindowObject $State.Window `
-MainThreadLogPath $State.LogFilePath
-MainThreadLogPath $State.LogFilePath `
-ThrottleLimit $State.Controls.txtThreads.Text
# Final status update (handled by Invoke-ParallelProcessing)
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
@@ -89,6 +89,7 @@ function Get-UIConfig {
UserAppListPath = "$($State.Controls.txtApplicationPath.Text)\UserAppList.json"
USBDriveList = @{}
Username = $State.Controls.txtUsername.Text
Threads = [int]$State.Controls.txtThreads.Text
Verbose = $State.Controls.chkVerbose.IsChecked
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
VMLocation = $State.Controls.txtVMLocation.Text
@@ -272,6 +273,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtFFUCaptureLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUCaptureLocation' -State $State
Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State
Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
Set-UIValue -ControlName 'chkBuildUSBDriveEnable' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BuildUSBDrive' -State $State
Set-UIValue -ControlName 'chkCompactOS' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompactOS' -State $State
Set-UIValue -ControlName 'chkUpdateADK' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'UpdateADK' -State $State
@@ -633,7 +633,8 @@ function Invoke-DownloadSelectedDrivers {
-CompletedStatusText 'Completed' `
-ErrorStatusPrefix 'Error: ' `
-WindowObject $State.Window `
-MainThreadLogPath $State.LogFilePath
-MainThreadLogPath $State.LogFilePath `
-ThrottleLimit $State.Controls.txtThreads.Text
$overallSuccess = $true
$successfullyDownloaded = [System.Collections.Generic.List[PSCustomObject]]::new()
@@ -670,10 +671,10 @@ function Invoke-DownloadSelectedDrivers {
$make = $makeLookup[$modelName]
if ($make) {
$successfullyDownloaded.Add([PSCustomObject]@{
Make = $make
Model = $modelName
DriverPath = $driverPath
})
Make = $make
Model = $modelName
DriverPath = $driverPath
})
}
else {
WriteLog "Warning: Could not find 'Make' for successful download of model '$modelName'. Skipping from DriverMapping.json."
@@ -722,7 +723,7 @@ function Invoke-DownloadSelectedDrivers {
}
'HP' {
$modelObject = @{
Name = $driverItem.Model
Name = $driverItem.Model
}
}
'Lenovo' {
@@ -2,6 +2,69 @@ function Register-EventHandlers {
param([PSCustomObject]$State)
WriteLog "Registering UI event handlers..."
# --------------------------------------------------------------------------
# SECTION: Shared Input Validation Handlers
# --------------------------------------------------------------------------
# Define a shared event handler for TextBoxes that should only accept integer input
$integerPreviewTextInputHandler = {
param($eventSource, $textCompositionEventArgs)
# Use a regex to check if the input text is NOT a digit. \D matches any non-digit character.
if ($textCompositionEventArgs.Text -match '\D') {
# If the input is not a digit, mark the event as handled to prevent the character from being entered.
$textCompositionEventArgs.Handled = $true
}
}
# Define a handler to validate pasted text, ensuring it's only integers
$integerPastingHandler = {
param($sender, $pastingEventArgs)
if ($pastingEventArgs.DataObject.GetDataPresent([string])) {
$pastedText = $pastingEventArgs.DataObject.GetData([string])
# Check if the pasted text consists ONLY of one or more digits.
if ($pastedText -notmatch '^\d+$') {
# If not, cancel the paste operation.
$pastingEventArgs.CancelCommand()
}
}
else {
# If the pasted data is not in a string format, cancel it.
$pastingEventArgs.CancelCommand()
}
}
# List of TextBox controls that require integer-only input
$integerOnlyTextBoxes = @(
$State.Controls.txtDiskSize,
$State.Controls.txtMemory,
$State.Controls.txtProcessors,
$State.Controls.txtThreads
)
# Attach the handlers to each relevant textbox
foreach ($textBox in $integerOnlyTextBoxes) {
if ($null -ne $textBox) {
$textBox.Add_PreviewTextInput($integerPreviewTextInputHandler)
[System.Windows.DataObject]::AddPastingHandler($textBox, $integerPastingHandler)
}
}
# Add specific validation for the Threads textbox to ensure it's not empty and is at least 1
if ($null -ne $State.Controls.txtThreads) {
$State.Controls.txtThreads.Add_LostFocus({
param($eventSource, $routedEventArgs)
$textBox = $eventSource
$currentValue = 0
# Try to parse the current text as an integer
$isValidInteger = [int]::TryParse($textBox.Text, [ref]$currentValue)
# If the text is not a valid integer OR the value is less than 1, reset it to the default value '1'
if (-not $isValidInteger -or $currentValue -lt 1) {
$textBox.Text = '1'
WriteLog "Threads value was invalid or less than 1. Reset to 1."
}
})
}
# Build Tab Event Handlers
$State.Controls.btnBrowseFFUDevPath.Add_Click({
param($eventSource, $routedEventArgs)
@@ -95,6 +95,7 @@ function Initialize-UIControls {
$State.Controls.txtFFUCaptureLocation = $window.FindName('txtFFUCaptureLocation')
$State.Controls.txtShareName = $window.FindName('txtShareName')
$State.Controls.txtUsername = $window.FindName('txtUsername')
$State.Controls.txtThreads = $window.FindName('txtThreads')
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
@@ -206,6 +207,7 @@ function Initialize-UIDefaults {
$State.Controls.txtFFUCaptureLocation.Text = $State.Defaults.generalDefaults.FFUCaptureLocation
$State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName
$State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
$State.Controls.chkBuildUSBDriveEnable.IsChecked = $State.Defaults.generalDefaults.BuildUSBDriveEnable
$State.Controls.chkCompactOS.IsChecked = $State.Defaults.generalDefaults.CompactOS
$State.Controls.chkUpdateADK.IsChecked = $State.Defaults.generalDefaults.UpdateADK
@@ -155,12 +155,12 @@ function Search-WingetPackagesPublic {
# for large datasets as it avoids holding complex objects in memory and bypasses the
# expensive formatting system for the raw results.
Find-WinGetPackage -Query $Query -ErrorAction Stop |
Select-Object -Property @{Name = 'IsSelected'; Expression = { $false } },
Name,
Id,
Version,
Source,
@{Name = 'DownloadStatus'; Expression = { '' } }
Select-Object -Property @{Name = 'IsSelected'; Expression = { $false } },
Name,
Id,
Version,
Source,
@{Name = 'DownloadStatus'; Expression = { '' } }
}
catch {
WriteLog "Error during Winget search: $($_.Exception.Message)"
@@ -731,7 +731,8 @@ function Invoke-WingetDownload {
-CompletedStatusText "Completed" `
-ErrorStatusPrefix "Error: " `
-WindowObject $State.Window `
-MainThreadLogPath $State.LogFilePath
-MainThreadLogPath $State.LogFilePath `
-ThrottleLimit $State.Controls.txtThreads.Text
# Final status update is handled by Invoke-ParallelProcessing, but we need to re-enable the button
$State.Controls.pbOverallProgress.Visibility = 'Collapsed'
@@ -754,10 +755,10 @@ function Update-WingetVersionFields {
[string]$moduleText
)
$State.Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Normal, [Action] {
$State.Controls.txtWingetVersion.Text = $wingetText
$State.Controls.txtWingetModuleVersion.Text = $moduleText
[System.Windows.Forms.Application]::DoEvents()
})
$State.Controls.txtWingetVersion.Text = $wingetText
$State.Controls.txtWingetModuleVersion.Text = $moduleText
[System.Windows.Forms.Application]::DoEvents()
})
}
Export-ModuleMember -Function *
@@ -112,6 +112,7 @@ function Get-GeneralDefaults {
FFUCaptureLocation = $ffuCapturePath
ShareName = "FFUCaptureShare"
Username = "ffu_user"
Threads = 5
BuildUSBDriveEnable = $false
CompactOS = $true
Optimize = $true