Public/New-ColorScriptCache.ps1
|
function New-ColorScriptCache { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Returns structured pipeline records for each cache operation.')] [OutputType([pscustomobject])] [CmdletBinding(DefaultParameterSetName = 'Selection', SupportsShouldProcess = $true, ConfirmImpact = 'Medium', HelpUri = 'https://nick2bad4u.github.io/PS-Color-Scripts-Enhanced/docs/help-redirect.html?cmdlet=New-ColorScriptCache')] [Alias('Update-ColorScriptCache', 'Build-ColorScriptCache')] param( [Parameter(ParameterSetName = 'Help')] [Alias('help')] [switch]$h, [Parameter(ParameterSetName = 'Selection', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [SupportsWildcards()] [ValidateScript({ Test-ColorScriptNameValue $_ -AllowWildcard })] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $null = $commandName, $parameterName, $commandAst, $fakeBoundParameters try { $records = ColorScripts-Enhanced\Get-ColorScriptList -AsObject -Quiet -ErrorAction Stop -WarningAction SilentlyContinue } catch { return } $pattern = if ([string]::IsNullOrWhiteSpace($wordToComplete)) { '*' } else { $trimmed = $wordToComplete.Trim([char]0x27, [char]0x22) if ([string]::IsNullOrWhiteSpace($trimmed)) { '*' } elseif ($trimmed -match '[*?]') { $trimmed } else { $trimmed + '*' } } $records | Where-Object { $_.Name -and ($_.Name -like $pattern) } | Group-Object -Property Name | Sort-Object -Property Name | ForEach-Object { $first = $_.Group | Select-Object -First 1 $toolTip = if ($first.Description) { $first.Description } elseif ($first.Category) { "Category: $($first.Category)" } else { $first.Name } [System.Management.Automation.CompletionResult]::new( $first.Name, $first.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $toolTip ) } })] [string[]]$Name, [Parameter(ParameterSetName = 'All')] [switch]$All, [Parameter(ParameterSetName = 'Selection')] [Parameter(ParameterSetName = 'All')] [switch]$Force, [Parameter(ParameterSetName = 'Selection')] [Parameter(ParameterSetName = 'All')] [switch]$PassThru, [Parameter(ParameterSetName = 'Selection')] [Parameter(ParameterSetName = 'All')] [string[]]$Category, [Parameter(ParameterSetName = 'Selection')] [Parameter(ParameterSetName = 'All')] [string[]]$Tag, [Parameter(ParameterSetName = 'Selection')] [Parameter(ParameterSetName = 'All')] [switch]$Parallel, [Parameter(ParameterSetName = 'Selection')] [Parameter(ParameterSetName = 'All')] [Alias('Threads')] [ValidateRange(1, 256)] [int]$ThrottleLimit ) begin { $helpRequested = $false $summary = $null $results = $null $nameSet = $null $collectedNames = $null $addName = $null if ($h) { Show-ColorScriptHelp -CommandName 'New-ColorScriptCache' $helpRequested = $true } if ($helpRequested) { return } if ($PSBoundParameters.ContainsKey('All') -and -not $All) { Invoke-ColorScriptError -Message $script:Messages.SpecifyNameToSelectScripts -ErrorId 'ColorScriptsEnhanced.CacheSelectionMissing' -Category ([System.Management.Automation.ErrorCategory]::InvalidOperation) -Cmdlet $PSCmdlet } Initialize-CacheDirectory $summary = [pscustomobject]@{ Processed = 0 Updated = 0 Skipped = 0 Failed = 0 } $results = New-Object 'System.Collections.Generic.List[pscustomobject]' $nameSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $collectedNames = [System.Collections.Generic.List[string]]::new() $addName = { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return } if ($nameSet.Add($Value)) { [void]$collectedNames.Add($Value) } } if ($PSBoundParameters.ContainsKey('Name') -and $Name) { foreach ($value in $Name) { & $addName $value } } } process { if ($helpRequested) { return } if ($MyInvocation.ExpectingInput) { if ($PSBoundParameters.ContainsKey('Name') -and $Name) { foreach ($value in $Name) { & $addName $value } } elseif ($null -ne $_) { $candidate = $null if ($_ -is [string]) { $candidate = $_ } elseif ($_ -is [System.Management.Automation.PSObject]) { if ($_.PSObject.Properties['Name']) { $candidate = [string]$_.PSObject.Properties['Name'].Value } elseif ($_.PSObject.Properties['ScriptName']) { $candidate = [string]$_.PSObject.Properties['ScriptName'].Value } } if ($candidate) { & $addName $candidate } } } } end { if ($helpRequested) { return } $requestedNames = $collectedNames $allRecords = @() try { $allRecords = @(Get-ColorScriptEntry -Category $Category -Tag $Tag) } catch { Write-Verbose ("Get-ColorScriptEntry failed: {0}" -f $_.Exception.Message) $allRecords = @() } $normalized = New-Object 'System.Collections.Generic.List[object]' $normalize = $null $normalize = { param($Item) if ($null -eq $Item) { return } if ($Item -is [System.Collections.IEnumerable] -and -not ($Item -is [string]) -and -not ($Item -is [System.Management.Automation.PSObject]) -and -not ($Item -is [System.Collections.IDictionary])) { foreach ($nested in $Item) { & $normalize $nested } return } [void]$normalized.Add($Item) } foreach ($entry in $allRecords) { & $normalize $entry } $candidateRecords = $normalized.ToArray() if ($requestedNames.Count -gt 0) { $selection = Select-RecordsByName -Records $candidateRecords -Name $requestedNames.ToArray() $candidateRecords = if ($selection.Records) { $selection.Records } else { @() } foreach ($pattern in $selection.MissingPatterns) { if (-not [string]::IsNullOrWhiteSpace($pattern)) { Write-Warning ($script:Messages.ScriptNotFound -f $pattern) } } } if (-not $All -and $requestedNames.Count -eq 0 -and -not $Category -and -not $Tag -and -not $candidateRecords) { $candidateRecords = $normalized.ToArray() } $candidateRecords = @($candidateRecords | Where-Object { $_ }) if (-not $candidateRecords -or $candidateRecords.Count -eq 0) { Write-Warning $script:Messages.NoScriptsSelectedForCacheBuild if ($PassThru) { return @() } return } $total = $candidateRecords.Count $activity = 'Building colorscript cache' $preparationProgressId = 1 $executionProgressId = 2 $resultOrder = 0 $workQueue = New-Object 'System.Collections.Generic.List[pscustomobject]' $parallelRequested = $Parallel.IsPresent -or $PSBoundParameters.ContainsKey('ThrottleLimit') $effectiveThrottle = if ($PSBoundParameters.ContainsKey('ThrottleLimit')) { $ThrottleLimit } else { [System.Math]::Max(1, [Environment]::ProcessorCount) } if ($effectiveThrottle -gt 256) { $effectiveThrottle = 256 } $useParallel = $parallelRequested -and ($effectiveThrottle -gt 1) if ($useParallel -and ($PSVersionTable.PSVersion.Major -lt 7)) { $parallelNotSupportedMessage = if ($script:Messages -and $script:Messages.ContainsKey('ParallelCacheNotSupported')) { $script:Messages.ParallelCacheNotSupported } else { 'Parallel cache building requires PowerShell 7 or later. Falling back to sequential execution.' } Write-Warning $parallelNotSupportedMessage $useParallel = $false } if (-not $useParallel) { $index = 0 foreach ($record in $candidateRecords) { $index++ $statusPercent = [math]::Min(100, [math]::Max(0, ($index / $total) * 100)) $recordObject = if ($record -is [System.Management.Automation.PSObject]) { $record } else { [pscustomobject]$record } $scriptName = [string]$recordObject.Name $scriptPath = [string]$recordObject.Path if ([string]::IsNullOrWhiteSpace($scriptName) -or [string]::IsNullOrWhiteSpace($scriptPath)) { continue } Write-Progress -Id $executionProgressId -Activity $activity -Status ("Processing {0} of {1}: {2}" -f $index, $total, $scriptName) -PercentComplete $statusPercent $summary.Processed++ if (-not $Force) { $cacheEntry = Get-CachedOutput -ScriptPath $scriptPath if ($cacheEntry.Available) { $summary.Skipped++ $resultOrder++ $skipRecord = [pscustomobject]@{ Order = $resultOrder Record = [pscustomobject]@{ Name = $scriptName ScriptPath = $scriptPath CacheFile = $cacheEntry.CacheFile Status = 'SkippedUpToDate' Message = $script:Messages.StatusSkippedUpToDate CacheExists = $true ExitCode = 0 StdOut = $cacheEntry.Content StdErr = '' } } [void]$results.Add($skipRecord) continue } } if (-not (Invoke-ShouldProcess -Cmdlet $PSCmdlet -Target $scriptName -Action 'Build colorscript cache')) { $summary.Skipped++ $resultOrder++ $skippedRecord = [pscustomobject]@{ Order = $resultOrder Record = [pscustomobject]@{ Name = $scriptName ScriptPath = $scriptPath CacheFile = $null Status = 'SkippedByUser' Message = $script:Messages.StatusSkippedByUser CacheExists = $false ExitCode = $null StdOut = '' StdErr = '' } } [void]$results.Add($skippedRecord) continue } $operation = Invoke-ColorScriptCacheOperation -ScriptName $scriptName -ScriptPath $scriptPath if ($operation.Warning) { Write-Warning $operation.Warning } $summary.Updated += $operation.Updated $summary.Failed += $operation.Failed $resultOrder++ [void]$results.Add([pscustomobject]@{ Order = $resultOrder Record = $operation.Result }) } Write-Progress -Id $executionProgressId -Activity $activity -Completed -Status 'Completed' } else { $prepareIndex = 0 Write-Progress -Id $preparationProgressId -Activity $activity -Status ("Preparing 0 of {0}" -f $total) -PercentComplete 0 foreach ($record in $candidateRecords) { $prepareIndex++ $statusPercent = [math]::Min(100, [math]::Max(0, ($prepareIndex / $total) * 100)) $recordObject = if ($record -is [System.Management.Automation.PSObject]) { $record } else { [pscustomobject]$record } $scriptName = [string]$recordObject.Name $scriptPath = [string]$recordObject.Path if ([string]::IsNullOrWhiteSpace($scriptName) -or [string]::IsNullOrWhiteSpace($scriptPath)) { continue } Write-Progress -Id $preparationProgressId -Activity $activity -Status ("Preparing {0} of {1}: {2}" -f $prepareIndex, $total, $scriptName) -PercentComplete $statusPercent $summary.Processed++ if (-not $Force) { $cacheEntry = Get-CachedOutput -ScriptPath $scriptPath if ($cacheEntry.Available) { $summary.Skipped++ $resultOrder++ [void]$results.Add([pscustomobject]@{ Order = $resultOrder Record = [pscustomobject]@{ Name = $scriptName ScriptPath = $scriptPath CacheFile = $cacheEntry.CacheFile Status = 'SkippedUpToDate' Message = $script:Messages.StatusSkippedUpToDate CacheExists = $true ExitCode = 0 StdOut = $cacheEntry.Content StdErr = '' } }) continue } } if (-not (Invoke-ShouldProcess -Cmdlet $PSCmdlet -Target $scriptName -Action 'Build colorscript cache')) { $summary.Skipped++ $resultOrder++ [void]$results.Add([pscustomobject]@{ Order = $resultOrder Record = [pscustomobject]@{ Name = $scriptName ScriptPath = $scriptPath CacheFile = $null Status = 'SkippedByUser' Message = $script:Messages.StatusSkippedByUser CacheExists = $false ExitCode = $null StdOut = '' StdErr = '' } }) continue } $resultOrder++ [void]$workQueue.Add([pscustomobject]@{ Order = $resultOrder Name = $scriptName Path = $scriptPath }) } $pendingCount = $workQueue.Count Write-Progress -Id $preparationProgressId -Activity $activity -Completed -Status 'Preparation complete' if ($pendingCount -gt 0) { Write-Progress -Id $executionProgressId -Activity $activity -Status ("Building 0 of {0}" -f $pendingCount) -PercentComplete 0 $updateParallelProgress = { param( [int]$Completed, [int]$Total, [string]$ActivityName, [int]$ProgressId, [int]$ActiveCount, [string]$CurrentName ) $status = "Building {0} of {1}" -f $Completed, $Total if ($ActiveCount -gt 0) { $status += " (active {0})" -f $ActiveCount } if (-not [string]::IsNullOrWhiteSpace($CurrentName)) { $status += ": {0}" -f $CurrentName } $percent = if ($Total -le 0) { 0 } else { [math]::Min(100, [math]::Max(0, ($Completed / $Total) * 100)) } Write-Progress -Id $ProgressId -Activity $ActivityName -Status $status -PercentComplete $percent } $moduleManifest = Join-Path -Path $script:ModuleRoot -ChildPath 'ColorScripts-Enhanced.psd1' $initialState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() $null = $initialState.ImportPSModule(@($moduleManifest)) # Create a runspace pool robustly across PS versions/hosts. # Prefer using the current host when available; otherwise fall back to overloads without host # and explicitly set min/max runspaces if needed. $runspacePool = $null try { if ($Host -is [System.Management.Automation.Host.PSHost]) { $runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $effectiveThrottle, $initialState, $Host) } } catch { $runspacePool = $null } if (-not $runspacePool) { try { $runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $effectiveThrottle, $initialState) } catch { try { $runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool($initialState) $null = $runspacePool.SetMinRunspaces(1) $null = $runspacePool.SetMaxRunspaces($effectiveThrottle) } catch { $runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool() $null = $runspacePool.SetMinRunspaces(1) $null = $runspacePool.SetMaxRunspaces($effectiveThrottle) } } } $runspacePool.Open() $jobList = New-Object 'System.Collections.Generic.List[pscustomobject]' $workerScriptBlock = { param($scriptName, $scriptPath) $moduleInfo = Get-Module -Name 'ColorScripts-Enhanced' if (-not $moduleInfo) { Import-Module -Name $using:moduleManifest -Force -ErrorAction Stop $moduleInfo = Get-Module -Name 'ColorScripts-Enhanced' -ErrorAction Stop } $moduleInfo.Invoke({ param($name, $path) Invoke-ColorScriptCacheOperation -ScriptName $name -ScriptPath $path }, $scriptName, $scriptPath) } try { foreach ($item in $workQueue) { $psInstance = [System.Management.Automation.PowerShell]::Create() $psInstance.RunspacePool = $runspacePool $null = $psInstance.AddCommand('Microsoft.PowerShell.Core\Invoke-Command') $null = $psInstance.AddParameter('ScriptBlock', $workerScriptBlock) $null = $psInstance.AddParameter('ArgumentList', @($item.Name, $item.Path)) $asyncResult = $psInstance.BeginInvoke() [void]$jobList.Add([pscustomobject]@{ PowerShell = $psInstance Async = $asyncResult Item = $item }) } $completed = 0 while ($completed -lt $pendingCount) { $processedThisCycle = $false foreach ($job in $jobList.ToArray()) { if (-not $job.Async.IsCompleted) { continue } $processedThisCycle = $true try { $outputCollection = $job.PowerShell.EndInvoke($job.Async) $operation = if ($outputCollection -and $outputCollection.Count -gt 0) { $outputCollection[0] } else { $null } if ($operation) { if ($operation.Warning) { Write-Warning $operation.Warning } $summary.Updated += $operation.Updated $summary.Failed += $operation.Failed [void]$results.Add([pscustomobject]@{ Order = $job.Item.Order Record = $operation.Result }) } else { $summary.Failed++ $failureMessage = 'Cache build failed.' Write-Warning ("Failed to cache {0}: {1}" -f $job.Item.Name, $failureMessage) [void]$results.Add([pscustomobject]@{ Order = $job.Item.Order Record = [pscustomobject]@{ Name = $job.Item.Name ScriptPath = $job.Item.Path CacheFile = $null Status = 'Failed' Message = $failureMessage CacheExists = $false ExitCode = $null StdOut = '' StdErr = '' } }) } } catch { $summary.Failed++ $errorMessage = $_.Exception.Message Write-Warning ("Failed to cache {0}: {1}" -f $job.Item.Name, $errorMessage) [void]$results.Add([pscustomobject]@{ Order = $job.Item.Order Record = [pscustomobject]@{ Name = $job.Item.Name ScriptPath = $job.Item.Path CacheFile = $null Status = 'Failed' Message = $errorMessage CacheExists = $false ExitCode = $null StdOut = '' StdErr = $errorMessage } }) } finally { $completed++ $job.PowerShell.Dispose() [void]$jobList.Remove($job) $activeCount = $jobList.Count & $updateParallelProgress $completed $pendingCount $activity $executionProgressId $activeCount $job.Item.Name } } if (-not $processedThisCycle) { $activeCount = $jobList.Count & $updateParallelProgress $completed $pendingCount $activity $executionProgressId $activeCount $null Start-Sleep -Milliseconds 30 } } } finally { if ($runspacePool) { $runspacePool.Close() $runspacePool.Dispose() } } } Write-Progress -Id $executionProgressId -Activity $activity -Completed -Status 'Completed' } $finalRecords = @() if ($results.Count -gt 0) { $finalRecords = ($results | Sort-Object Order | ForEach-Object { $_.Record }) } $summary.Processed = $finalRecords.Count $summary.Updated = ($finalRecords | Where-Object { $_.Status -eq 'Updated' }).Count $summary.Failed = ($finalRecords | Where-Object { $_.Status -eq 'Failed' }).Count $summary.Skipped = ($finalRecords | Where-Object { $_.Status -like 'Skipped*' }).Count if (-not $PassThru -and $summary.Processed -gt 0) { $formatString = $null if ($script:Messages -and $script:Messages.ContainsKey('CacheBuildSummaryFormat')) { $formatString = $script:Messages.CacheBuildSummaryFormat } if ([string]::IsNullOrWhiteSpace($formatString)) { $formatString = 'Cache build summary: Processed {0}, Updated {1}, Skipped {2}, Failed {3}' } $summaryMessage = $formatString -f $summary.Processed, $summary.Updated, $summary.Skipped, $summary.Failed $summarySegment = New-ColorScriptAnsiText -Text $summaryMessage -Color 'Cyan' -NoAnsiOutput:$false Write-ColorScriptInformation -Message $summarySegment -Quiet:$false -PreferConsole -Color 'Cyan' } if ($PassThru) { return $finalRecords } } } |