diff --git a/.gitignore b/.gitignore index 9c528b6f..beab25e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # ignore the settings folder and files for VSCode and PSS -.vscode/* *.psproj *TempPoint* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7b58a8e7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationAfterEveryPipeline", + "powershell.codeFormatting.trimWhitespaceAroundPipe": true, + "powershell.codeFormatting.whitespaceBetweenParameters": true, + "powershell.codeFormatting.autoCorrectAliases": true, + "powershell.codeFormatting.useCorrectCasing": true, + + "files.encoding": "utf8bom", +} \ No newline at end of file diff --git a/PSFramework/PSFramework.psd1 b/PSFramework/PSFramework.psd1 index 4c82df50..c2fad0ec 100644 --- a/PSFramework/PSFramework.psd1 +++ b/PSFramework/PSFramework.psd1 @@ -4,7 +4,7 @@ RootModule = 'PSFramework.psm1' # Version number of this module. - ModuleVersion = '1.13.406' + ModuleVersion = '1.13.414' # ID used to uniquely identify this module GUID = '8028b914-132b-431f-baa9-94a6952f21ff' @@ -95,6 +95,7 @@ 'Install-PSFLoggingProvider' 'Invoke-PSFCommand' 'Invoke-PSFFilter' + 'Invoke-PSFRunspace' 'Join-PSFPath' 'New-PSFFilter' 'New-PSFFilterCondition' diff --git a/PSFramework/PSFramework.psproj.psbuild b/PSFramework/PSFramework.psproj.psbuild deleted file mode 100644 index 07a24531..00000000 Binary files a/PSFramework/PSFramework.psproj.psbuild and /dev/null differ diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll index b7090b20..a8fe0c64 100644 Binary files a/PSFramework/bin/PSFramework.dll and b/PSFramework/bin/PSFramework.dll differ diff --git a/PSFramework/bin/PSFramework.pdb b/PSFramework/bin/PSFramework.pdb index 274101c9..6bbb17b4 100644 Binary files a/PSFramework/bin/PSFramework.pdb and b/PSFramework/bin/PSFramework.pdb differ diff --git a/PSFramework/bin/PSFramework.xml b/PSFramework/bin/PSFramework.xml index f5048d37..d1d56fcf 100644 --- a/PSFramework/bin/PSFramework.xml +++ b/PSFramework/bin/PSFramework.xml @@ -8573,6 +8573,49 @@ Purge all RBVs of datasets from all expired runspaces + + + The result of a runspace task + + + + + The object that triggered the task + + + + + All output + + + + + All Information Messages + + + + + All verbose messages + + + + + All warning messages + + + + + All error records + + + + + Creates a result object, representing the completed result of the runspace task. + + The original argument for the task + The output result of the task + The streams the task sent + Contains the state a managed, unique runspace can be in. @@ -8593,6 +8636,245 @@ The runspace has followed its order to stop and is currently disabled + + + An individual task executed in the runspace pool of its hosting RunspaceWrapper + + + + + The item to process in this task + + + + + Whether the task has completed successfully + + + + + Create a new runspace task. If the host has already stared execution, it is immediately queued for execution. + + The hosting RunspaceWrapper + The item to process in this task + + + + If the task is complete, collect results and direct the streams. Do nothing if not complete yet. + Delists itself from the hosting RunspaceWrapper, if completed. + + The command runtime to whose streams to write the results + Do not write to additional streams + Whether it successfully collected the results + + + + Wait until the task completes, then get the full result with all stream information + + A result object, containing output and streams + Don't try to collect results before starting the task + + + + Wait until the task completes, then get the output + + All output results of the task + Don't try to collect results before starting the task + + + + Wait until the task completes, then collect results and direct the streams. + + The command runtime to whose streams to write the results + hether to NOT write to the different streams. + Don't try to collect results before starting the task + + + + Start this task, queueing the code as a runspace in the runspace pool for execution + + If the runspacepool of the hosting RunspaceWrapper has not been opened yet, we cannot start yet + + + + Cancel and destroy this task. + + + + + Clean up this object + + + + + Runspace managing class used by Invoke-PSFRunspace. + + + + + Nme of the workload + + + + + The code to run in parallel + + + + + How many runspace tasks to execute in parallel + + + + + Total number of tasks in this wrapper + + + + + Number of Tasks still pending + + + + + Number of Tasks completed + + + + + What each runspace task will have available + + + + + List of tasks to execute + + + + + Variables available to all tasks + + + + + Functions available to all tasks + + + + + Modules available to all tasks + + + + + Whether the RunspaceWrapper is currently open for tasks + + + + + Add a variable to the initial sessionstate + + name of the variable + Value of the variable + + + + Add multiple variables to the initial sessionstate + + Name/value map of variables to inclue + + + + Add a module by name or path + + Name or path to the module + + + + Add a module by its module info object + + The module info object + + + + Define a function available to all tasks + + + + + + + + Define a function available to all tasks + + Function info object to copy over + + + + + Start the entire wrapper, creating a runspace pool and preparing for execution + + + + + Close the runspace pool, terminate everything and clean up. + + + + + Make sure everything is cleaned out after the job is done + + + + + Add a task that should be executed + + The argument for which the task should be executed + + + + Add a list of tasks that all should be executed + + The arugments for each of which the task should be executed + + + + Wait for all task results and receive results directly into the streams of the calling command + + The command runtime whose streams to write to + Whether additional streams should be hidden and only output shown + + + + Collect all tasks that already completed, and directly write the results to the streams of the calling command + + The command runtime whose streams to write to + Whether additional streams should be hidden and only output shown + + + + Wait for all task results and return result report objects, including information on all streams of the runspace + + List of completion reports, including all output, warnings, errors, informational messages, etc. + + + + Retrieve result report objects for each task already completed, including information on all streams of the runspace + + List of completion reports, including all output, warnings, errors, informational messages, etc. + + + + Wait for all task results and return the output. + + The resulting output of all tasks + + + + Receive the output of all currently completed tasks + + The output of all currently completed tasks + The serialization output options available @@ -8927,6 +9209,16 @@ Whether to execute the scriptblock in the global scope + + + When enabled, do not filter based on user input. + + + + + Maximum number of results to show when tab-completing. + + If true: Match input against any part of the options, not just the beginning @@ -9030,6 +9322,11 @@ Whether PSFramework completion should use fuzzy-matching when matching completion values with the already typed text. + + + The maximum number of results shown to the user, before truncating data sets. + + Registers a new completion scriptblock diff --git a/PSFramework/changelog.md b/PSFramework/changelog.md index 2728856e..13c5a70f 100644 --- a/PSFramework/changelog.md +++ b/PSFramework/changelog.md @@ -1,5 +1,16 @@ # CHANGELOG +## 1.13.414 (2025-10-14) + +- Upd: Wait-PSFRunspaceWorkflow - adding ProgressBar with `-ShowProgress` (#698 | @fslef) +- Upd: Register-PSFArgumentCompleter - adding parameter `-DontFilter`, disabling automatic filtering by user input and enabling complex custom filtering inside of the completer (#696) +- Upd: Register-PSFArgumentCompleter - adding parameter `-MaxResults`, truncating large result-sets for a more userfriendly display (#694) +- Fix: Invoke-PSFProtectedCommand - $paramStopPSFFunction Leaks To Global Scope (#697) +- Fix: Register-PSFArgumentCompleter - ignores `-DontSort` +- Fix: Configuration import - simple persistence from Environment fails for arrays (#692) +- Fix: ConvertTo-PSFPsd1 - Error: The property 'Depth' cannot be found on this object (#695) +- Fix: Filter "Environment" - "Elevated" miss-detects MacOS (#693) + ## 1.13.406 (2025-08-29) - New: Assert-PSFInternalCommand - Verifies, that the command calling it in turn was only called from another command within the same module. (#685) diff --git a/PSFramework/en-us/stringsRunspaces.psd1 b/PSFramework/en-us/stringsRunspaces.psd1 index bb57809d..762e6371 100644 --- a/PSFramework/en-us/stringsRunspaces.psd1 +++ b/PSFramework/en-us/stringsRunspaces.psd1 @@ -2,6 +2,10 @@ 'Add-PSFRunspaceWorker.Error.UntrustedFunctionCode' = 'Failed to load function {0}: The provided function code is not trusted (in Constrained language Mode) and cannot be imported. Ensure the code building the scriptblock is trusted to create a non-constrained scriptblock.' # $pair.Key 'Add-PSFRunspaceWorker.Error.UntrustedTextFunction' = 'Failed to load function {0}: String-based code is not trusted in a secured console. Provide its code as a scriptblock, rather than a string to enable code trust verification.' # $pair.Key + 'Invoke-PSFRunspace.Error.ModuleImport' = 'Failed to include module: "{0}"' # $module + 'Invoke-PSFRunspace.Error.UntrustedTextFunction' = 'Failed to import function "{0}". Providing function-code as text is not supported in a hardened PowerShell process. Provide the function-code instead as a scriptblock.' # $pair.Key + 'Invoke-PSFRunspace.Error.UntrustedFunctionCode' = 'Failed to import function "{0}". Scriptblock is not in FullLanguage mode and thus not trusted!' # $pair.Key + 'New-PSFRunspaceWorkflow.Error.ExistsAlready' = 'Failed to create workflow {0}: It already exists! Use "-Force" to overwrite the existing Runspace Workflow, interrupting all currently ongoing processing.' # $Name 'Read-PSFRunspaceQueue.Error.Continual.TooManyWorkflows' = 'Error resolving queue to read from in continuous mode: {0}. Multiple workflows found, while continuous read only supports a single workflow. Workflows found: {1}' # $Name, ($resolvedWorkflows.Name -join ', ') diff --git a/PSFramework/functions/data/ConvertTo-PSFPsd1.ps1 b/PSFramework/functions/data/ConvertTo-PSFPsd1.ps1 index 6f43f5ce..7443cfe2 100644 --- a/PSFramework/functions/data/ConvertTo-PSFPsd1.ps1 +++ b/PSFramework/functions/data/ConvertTo-PSFPsd1.ps1 @@ -47,7 +47,7 @@ ) begin { $converter = [PSFramework.Data.Psd1Converter]::new() - $converter.Depth = $Depth + $converter.MaxDepth = $Depth $converter.EnableVerbose = $EnableVerbose $converter.Config = $Configuration $converter.Cmdlet = $PSCmdlet diff --git a/PSFramework/functions/runspace/Invoke-PSFRunspace.ps1 b/PSFramework/functions/runspace/Invoke-PSFRunspace.ps1 new file mode 100644 index 00000000..8fc5c843 --- /dev/null +++ b/PSFramework/functions/runspace/Invoke-PSFRunspace.ps1 @@ -0,0 +1,207 @@ +function Invoke-PSFRunspace { + <# + .SYNOPSIS + Execute a scriptblock in parallel. + + .DESCRIPTION + Execute a scriptblock in parallel. + + This command offers two separate "modes" of operation: + - Similar to "ForEach-Object -Parallel" within a PowerShell pipeline. + - Similar to "Start-ThreadJob", in that it returns a task object you can collect reults from later. + + In the former scenario, it offers redirecting all the streams (verbose, warning, errors, ...) for each task into the main runspace. + Supports importing variables into the background runspaces using the "$using:"-statement + + .PARAMETER ScriptBlock + The code to execute in parallel. + + .PARAMETER InputObject + The items for which to execute the scriptblock. + One instance per item, whether piped or provided explicitly. + + .PARAMETER AsTask + Rather than wait for the processing to complete, return an object representing the overall execution. + To collect the results, call one of the following methods on the object: + - Collect(): Wait until everything is completed and collect the output. + - CollectCurrent(): Collect the output of tasks that have completed so far + - CollectResult(): Wait until everything is completed and collect report objects for each item, including the different streams, input and output + - CollectCurrentResult(): Collect report objects for each task already completed, including the different streams, input and output + + .PARAMETER Name + Name of the runspace workload. + Documentary only. + Defaults to: + + .PARAMETER OutputStyle + How should output be processed. + - Output: Produce the output of each task as output of this command. Redirect all background-streams into this command's streams unless combined ith "-NoStreams" + - Result: Each task is completed with a results object, including the different streams, input and output + Has no effect when used with "-AsTask" + Defaults to: Output + + .PARAMETER ThrottleLimit + How man tasks are executed in parallel. + Defaults to: 5 + + .PARAMETER Variables + Any variables to provide to the background runspaces. + Maps name to value. + You can also import variables into the background runspaces by using the "$using:"-statement + + .PARAMETER Functions + Any functions to import into the background runspace. + Maps name to code. + Code can be either text or scriptblock: + - @{ 'Get-Example' = (Get-Command Get-Example).Definition } + - @{ 'Get-Example' = [scriptblock]::Create((Get-Command Get-Example).Definition) } + + Note: + In a secured environment, where PowerShell Constrained Language Mode has been deployed, only the scriptblock-variant will work! + + .PARAMETER Modules + Any modules to include in the background runspaces. + + .PARAMETER ImportPSFramework + Import the PSFramework into the background runspaces. + + .PARAMETER NoStreams + Do not redirect background streams into the streams of Invoke-PSFRunspace. + Has no effect when using either "-AsTask" or "-OutputStyle Result". + + .PARAMETER InitialSessionState + A full initial session state object you preconfigured to operate the background tasks. + Note: Variables, type references & method invocations must work for this command to succeed. + + .EXAMPLE + PS C:\> Get-ADUser -Filter * | Invoke-PSFRunspace { $_ | Get-ADPrincipalGroupMembership } + + Retrieve all users from Active Directory, then retrieve their group memberships + + .EXAMPLE + PS C:\> Get-ADUser -Filter * | Invoke-PSFRunspace { $_ | Get-ADPrincipalGroupMembership } -OutputStyle Result + + Retrieve all users from Active Directory, then retrieve their group memberships, returning a report object, mapping each user to their group memberships. + + .EXAMPLE + PS C:\> $task = Get-ADUser -Filter * | Invoke-PSFRunspace { $_ | Get-ADPrincipalGroupMembership } + PS C:\> $task.CollectResult() + + First start searching for all users' group memberships. + Then later collect all the results in convenient result datasets, mapping input to output and including all errors / warnings / etc. + #> + [OutputType([PSFramework.Runspace.RunspaceWrapper])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [ScriptBlock] + $ScriptBlock, + + [Parameter(ValueFromPipeline = $true)] + $InputObject, + + [switch] + $AsTask, + + [string] + $Name = '', + + [ValidateSet('Output', 'Result')] + [string] + $OutputStyle = 'Output', + + [int] + $ThrottleLimit = 5, + + [ValidateNotNull()] + [hashtable] + $Variables = @{}, + + [ValidateNotNull()] + [hashtable] + $Functions = @{}, + + [object[]] + $Modules, + + [switch] + $ImportPSFramework, + + [switch] + $NoStreams, + + [initialsessionstate] + $InitialSessionState + ) + begin { + $runspaceWrapper = [PSFramework.Runspace.RunspaceWrapper]::new() + $runspaceWrapper.ThrottleLimit = $ThrottleLimit + $runspaceWrapper.Name = $Name + + #region Provide Context + if ($InitialSessionState) { $runspaceWrapper.initialsessionstate = $InitialSessionState } + + $runspaceWrapper.AddVariable($Variables) + + # See usually invisible background streams + $runspaceWrapper.AddVariable("VerbosePreference", $VerbosePreference) + $runspaceWrapper.AddVariable("InformationPreference", $InformationPreference) + + if ($ImportPSFramework) { $runspaceWrapper.AddModule((Get-Module PSFramework)) } + foreach ($module in $Modules) { + try { $runspaceWrapper.AddModule($module) } + catch { Stop-PSFFunction -String 'Invoke-PSFRunspace.Error.ModuleImport' -StringValues $module -EnableException $true -Cmdlet $PSCmdlet } + } + + foreach ($pair in $Functions.GetEnumerator()) { + if ($consoleConstrained -and $pair.Value -isnot [ScriptBlock]) { + Stop-PSFFunction -String 'Invoke-PSFRunspace.Error.UntrustedTextFunction' -StringValues $pair.Key -EnableException $true -Cmdlet $PSCmdlet -Category SecurityError + } + if ($consoleConstrained -and ([PsfScriptBlock]$pair.Value).LanguageMode -ne 'FullLanguage') { + Stop-PSFFunction -String 'Invoke-PSFRunspace.Error.UntrustedFunctionCode' -StringValues $pair.Key -EnableException $true -Cmdlet $PSCmdlet -Category SecurityError + } + if ($pair.Value -is [ScriptBlock]) { + $runspaceWrapper.AddFunction($pair.Key, $pair.Value) + $functionsResolved[$pair.Key] = $pair.Value + continue + } + $runspaceWrapper.AddFunction($pair.Key, [scriptblock]::Create($pair.Value)) + } + #endregion Provide Context + + #region Handle Code + $actualCode = $ScriptBlock + + if ($actualCode.Ast.Extent.Text -match '\$using:') { + $convertedCodeData = ConvertFrom-PsfUsingStatement -ScriptBlock $actualCode + $actualCode = $convertedCodeData.Code + foreach ($variableName in $convertedCodeData.Variables) { + $runspaceWrapper.AddVariable($variableName, $PSCmdlet.SessionState.PSVariable.Get($variableName).Value) + } + } + + $runspaceWrapper.Code = $actualCode + #endregion Handle Code + $runspaceWrapper.Start() + } + process { + if ($PSBoundParameters.Keys -contains 'InputObject') { + $runspaceWrapper.AddTaskBulk(@($InputObject)) + } + if ($AsTask) { return } + + switch ($OutputStyle) { + 'Result' { $runspaceWrapper.CollectCurrentResult() } + default { $runspaceWrapper.CollectCurrent($PSCmdlet, $NoStreams.ToBool()) } + } + } + end { + if ($AsTask) { return $runspaceWrapper } + + switch ($OutputStyle) { + 'Result' { $runspaceWrapper.CollectResult() } + default { $runspaceWrapper.Collect($PSCmdlet, $NoStreams.ToBool()) } + } + $runspaceWrapper.Stop() + } +} \ No newline at end of file diff --git a/PSFramework/functions/runspace/Wait-PSFRunspaceWorkflow.ps1 b/PSFramework/functions/runspace/Wait-PSFRunspaceWorkflow.ps1 index 39376550..a883a513 100644 --- a/PSFramework/functions/runspace/Wait-PSFRunspaceWorkflow.ps1 +++ b/PSFramework/functions/runspace/Wait-PSFRunspaceWorkflow.ps1 @@ -2,10 +2,10 @@ <# .SYNOPSIS Wait for a Runspace Workflow to complete. - + .DESCRIPTION Wait for a Runspace Workflow to complete. - + .PARAMETER Queue The name of the queue to measure completion by. Usually the last output queue in the chain of steps. @@ -13,17 +13,17 @@ .PARAMETER WorkerName The name of the worker to measure completion by. Usually the last step in the chain of steps. - + .PARAMETER Closed The workflow is considered completed, when the queue or worker selected is closed. - + .PARAMETER Count The workflow is considered completed, when the queue selected has received the specified number of results. This looks at the total amount ever provided, not current number queued. - + .PARAMETER ReferenceQueue The workflow is considered completed, when the queue selected has received the same number of items as the reference queue. - + .PARAMETER ReferenceMultiplier When comparing the result queue with a reference queue, multiply the number of items in the reference queue by this value. Use when the number of output items, based from the original input, scales by a constant multiplier. @@ -31,7 +31,7 @@ .PARAMETER QueueTimeout Wait based on how long ago the last item was added to the specified queue. - + .PARAMETER PassThru Pass through the workflow object waiting for. Useful to stop it once waiting has completed. @@ -39,16 +39,19 @@ .PARAMETER Timeout Maximum wait time. Throws an error if exceeded. Defaults to 1 day. - + .PARAMETER Name Name of the workflow to wait for. - + .PARAMETER InputObject A runspace workflow object to wait for. - + + .PARAMETER ShowProgress + To show a progressbar while waiting for runspace completion + .EXAMPLE PS C:\> $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Count 1000 - + Wait until 1000 items have been queued to "Done" in total. .EXAMPLE @@ -60,10 +63,11 @@ PS C:\> $workflow | Wait-PSFRunspaceWorkflow -Queue Done -ReferenceQueue Input Wait until the "Done" queue has processed as many items as there were written to the "Input" queue. - + .LINK https://psframework.org/documentation/documents/psframework/runspace-workflows.html #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Closed')] param ( @@ -107,6 +111,9 @@ [switch] $PassThru, + [switch] + $ShowProgress, + [PSFramework.Parameter.TimeSpanParameter] $Timeout, @@ -119,18 +126,119 @@ [PSFramework.Runspace.RSWorkflow[]] $InputObject ) + begin { + #region Utility Functions + function Write-WorkflowProgress { + [CmdletBinding()] + param ( + [string] + $Mode, + + [DateTime] + $Start, + + $Workflow, + + [int] + $CurrentCount, + + [string] + $QueueName, + + $QueueObject, + + [string] + $WorkerName, + + [int] + $WorkflowProgressID, + + [hashtable] + $WorkerIDs, + + [int] + $TargetCount + ) + + $elapsed = ([int]((Get-Date) - $Start).TotalSeconds) + $overallCurrent = 0 + if ($QueueName) { $overallCurrent = $Workflow.Queues.$QueueName.TotalItemCount } + if ($WorkerName) { $overallCurrent = $Workflow.Workers.$WorkerName.CountInputCompleted } + if ($QueueObject) { $overallCurrent = $QueueObject.TotalItemCount } + if ($PSBoundParameters.Keys -contains 'CurrentCount') { $overallCurrent = $CurrentCount } + + $targetString = '' + if ($TargetCount) { $targetString = "/$TargetCount" } + + Write-Progress -Id $WorkflowProgressID -Activity "Workflow: $($Workflow.Name)" -Status "Mode:$Mode | Current:$overallCurrent$($targetString) | Elapsed:$($elapsed)s" -PercentComplete -1 + + foreach ($workerObjName in $Workflow.Workers.Keys) { + $workerObj = $Workflow.Workers.$workerObjName + + $inQueueName = if ($workerObj.PSObject.Properties.Name -contains 'InQueue') { $workerObj.InQueue } else { $null } + $outQueueName = $workerObj.OutQueue + + $inQeue = $null + if ($inQueueName) { $inQeue = $Workflow.Queues.($inQueueName) } + $outQueue = $null + if ($outQueueName) { $outQueue = $Workflow.Queues.($outQueueName) } + + $inTotal = if ($inQeue) { $inQeue.TotalItemCount } else { 0 } + $outTotal = if ($outQueue) { $outQueue.TotalItemCount } else { 0 } + $outClosed = if ($outQueue) { $outQueue.Closed } else { $false } + $current = $workerObj.CountInputCompleted + + Write-Progress -Id $WorkerIDs[$workerObjName].RunnerId -ParentId $WorkflowProgressID -Activity "Runner: $workerObjName" -Status "Progress: $current$($targetString) | Elapsed:$($elapsed)s" -PercentComplete -1 + + # Items status (only print non-empty fields) + $parts = @("Completed:$current") + if ($inQueueName) { $parts += "InQ:$inQueueName total:$inTotal" } + if ($outQueueName) { $parts += "OutQ:$outQueueName total:$outTotal closed:$outClosed" } + $itemStatus = ($parts -join ' | ') + Write-Progress -Id $WorkerIDs[$workerObjName].ItemsId -ParentId $WorkerIDs[$workerObjName].RunnerId -Activity "# of items" -Status $itemStatus -PercentComplete -1 + } + } + #endregion Utility Functions + } process { $resolvedWorkflows = Resolve-PsfRunspaceWorkflow -Name $Name -InputObject $InputObject -Cmdlet $PSCmdlet $limit = (Get-Date).AddDays(1) if ($Timeout) { $limit = (Get-Date).Add($Timeout) } + # base progress id counter (simple, monotonic) + $progressId = 1 + foreach ($resolvedWorkflow in $resolvedWorkflows) { + $start = Get-Date + + if ($ShowProgress) { + $workflowProgressID = $progressId + $progressId += 1 + Write-Progress -Id $workflowProgressID -Activity "Workflow: $($resolvedWorkflow.Name)" -Status "Elapsed: 0s" -PercentComplete -1 + + $workerIds = @{} + foreach ($workerObjName in $resolvedWorkflow.Workers.Keys) { + $runnerId = $progressId + $itemsId = $progressId + 1 + $progressId += 2 + $workerIds[$workerObjName] = @{ + RunnerId = $runnerId + ItemsId = $itemsId + } + Write-Progress -Id $runnerId -ParentId $workflowProgressID -Activity "Runner: $workerObjName" -Status "Initializing..." -PercentComplete -1 + Write-Progress -Id $itemsId -ParentId $runnerId -Activity "Nb of items" -Status "Initializing..." -PercentComplete -1 + } + } + switch ($PSCmdlet.ParameterSetName) { 'Closed' { while (-not $resolvedWorkflow.Queues.$Queue.Closed) { if ($limit -lt (Get-Date)) { Stop-PSFFunction -String 'Wait-PSFRunspaceWorkflow.Error.Timeout' -StringValues $limit, $resolvedWorkflow.Name -Target $resolvedWorkflow -EnableException $true -Cmdlet $PSCmdlet -Category OperationTimeout } + if ($ShowProgress) { + Write-WorkflowProgress -Mode $PSCmdlet.ParameterSetName -Start $start -Workflow $resolvedWorkflow -QueueName $Queue -WorkflowProgressID $workflowProgressID -WorkerIDs $workerIds + } Start-Sleep -Milliseconds 200 } } @@ -140,6 +248,9 @@ if ($limit -lt (Get-Date)) { Stop-PSFFunction -String 'Wait-PSFRunspaceWorkflow.Error.Timeout' -StringValues $limit, $resolvedWorkflow.Name -Target $resolvedWorkflow -EnableException $true -Cmdlet $PSCmdlet -Category OperationTimeout } + if ($ShowProgress) { + Write-WorkflowProgress -Mode $PSCmdlet.ParameterSetName -Start $start -Workflow $resolvedWorkflow -QueueObject $queueObject -WorkflowProgressID $workflowProgressID -WorkerIDs $workerIds + } Start-Sleep -Milliseconds 200 } } @@ -148,6 +259,9 @@ if ($limit -lt (Get-Date)) { Stop-PSFFunction -String 'Wait-PSFRunspaceWorkflow.Error.Timeout' -StringValues $limit, $resolvedWorkflow.Name -Target $resolvedWorkflow -EnableException $true -Cmdlet $PSCmdlet -Category OperationTimeout } + if ($ShowProgress) { + Write-WorkflowProgress -Mode $PSCmdlet.ParameterSetName -Start $start -Workflow $resolvedWorkflow -QueueName $Queue -WorkflowProgressID $workflowProgressID -WorkerIDs $workerIds -TargetCount $Count + } Start-Sleep -Milliseconds 200 } } @@ -156,6 +270,9 @@ if ($limit -lt (Get-Date)) { Stop-PSFFunction -String 'Wait-PSFRunspaceWorkflow.Error.Timeout' -StringValues $limit, $resolvedWorkflow.Name -Target $resolvedWorkflow -EnableException $true -Cmdlet $PSCmdlet -Category OperationTimeout } + if ($ShowProgress) { + Write-WorkflowProgress -Mode $PSCmdlet.ParameterSetName -Start $start -Workflow $resolvedWorkflow -WorkerName $WorkerName -WorkflowProgressID $workflowProgressID -WorkerIDs $workerIds -TargetCount $Count + } Start-Sleep -Milliseconds 200 } } @@ -164,6 +281,9 @@ if ($limit -lt (Get-Date)) { Stop-PSFFunction -String 'Wait-PSFRunspaceWorkflow.Error.Timeout' -StringValues $limit, $resolvedWorkflow.Name -Target $resolvedWorkflow -EnableException $true -Cmdlet $PSCmdlet -Category OperationTimeout } + if ($ShowProgress) { + Write-WorkflowProgress -Mode $PSCmdlet.ParameterSetName -Start $start -Workflow $resolvedWorkflow -QueueName $Queue -WorkflowProgressID $workflowProgressID -WorkerIDs $workerIds -TargetCount ($resolvedWorkflow.Queues.$ReferenceQueue.TotalItemCount * $ReferenceMultiplier) + } Start-Sleep -Milliseconds 200 } } @@ -172,6 +292,9 @@ if ($limit -lt (Get-Date)) { Stop-PSFFunction -String 'Wait-PSFRunspaceWorkflow.Error.Timeout' -StringValues $limit, $resolvedWorkflow.Name -Target $resolvedWorkflow -EnableException $true -Cmdlet $PSCmdlet -Category OperationTimeout } + if ($ShowProgress) { + Write-WorkflowProgress -Mode $PSCmdlet.ParameterSetName -Start $start -Workflow $resolvedWorkflow -WorkerName $WorkerName -WorkflowProgressID $workflowProgressID -WorkerIDs $workerIds -TargetCount ($resolvedWorkflow.Queues.$ReferenceQueue.TotalItemCount * $ReferenceMultiplier) + } Start-Sleep -Milliseconds 200 } } @@ -180,12 +303,24 @@ if ($limit -lt (Get-Date)) { Stop-PSFFunction -String 'Wait-PSFRunspaceWorkflow.Error.Timeout' -StringValues $limit, $resolvedWorkflow.Name -Target $resolvedWorkflow -EnableException $true -Cmdlet $PSCmdlet -Category OperationTimeout } + if ($ShowProgress) { + $sinceLast = (Get-Date) - $resolvedWorkflow.Queues.$Queue.LastUpdate + Write-WorkflowProgress -Mode $PSCmdlet.ParameterSetName -Start $start -Workflow $resolvedWorkflow -CurrentCount $sinceLast.TotalSeconds -WorkflowProgressID $workflowProgressID -WorkerIDs $workerIds -TargetCount $QueueTimeout.Value.TotalSeconds + } Start-Sleep -Milliseconds 200 } } } + if ($ShowProgress) { + foreach ($workerObjName in $workerIds.Keys) { + Write-Progress -Id $workerIds[$workerObjName].ItemsId -Activity "# of items" -Completed + Write-Progress -Id $workerIds[$workerObjName].RunnerId -Activity "Runner" -Completed + } + Write-Progress -Id $WorkflowProgressID -Activity "Workflow" -Completed + } + if ($PassThru) { $resolvedWorkflow } } } -} \ No newline at end of file +} diff --git a/PSFramework/functions/tabexpansion/Register-PSFTeppScriptblock.ps1 b/PSFramework/functions/tabexpansion/Register-PSFTeppScriptblock.ps1 index 11aacccf..56a01fe1 100644 --- a/PSFramework/functions/tabexpansion/Register-PSFTeppScriptblock.ps1 +++ b/PSFramework/functions/tabexpansion/Register-PSFTeppScriptblock.ps1 @@ -40,6 +40,10 @@ Whether the scriptblock should be executed in the global context. This parameter is needed to reliably execute in background runspaces, but means no direct access to module content. + .PARAMETER MaxResults + The maximum number of results shown to the user. + If more completions would be viable, only the first X are shown, as well as a message informaing about the truncation. + .PARAMETER MatchAnywhere Match input against any part of the completion text, not just the beginning. @@ -52,6 +56,10 @@ .PARAMETER DontSort Completion results are no longer sorted alphabetically. + .PARAMETER DontFilter + Do not automatically filter by the words the user typed. + This allows for the scriptblock to provide its own, more complex filtering. + .PARAMETER AutoTraining Automatically train the tab completion by caching user inputs. Requires using Update-PSFTeppCompletion inside of the respective command, to automatically pick up new values. @@ -89,6 +97,9 @@ [PSFramework.Parameter.TimeSpanParameter] $CacheDuration = 0, + + [int] + $MaxResults, [switch] $Global, @@ -105,6 +116,9 @@ [switch] $DontSort, + [switch] + $DontFilter, + [switch] $AutoTraining ) @@ -114,9 +128,11 @@ [PSFramework.TabExpansion.TabExpansionHost]::RegisterCompletion($Name, $ScriptBlock, $Mode, $CacheDuration, $Global) $scriptContainer = [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$Name] if ($PSBoundParameters.Keys -contains 'MatchAnywhere') { $scriptContainer.MatchAnywhere = $MatchAnywhere } + if ($PSBoundParameters.Keys -contains 'MaxResults') { $scriptContainer.MaxResults = $MaxResults } if ($PSBoundParameters.Keys -contains 'FuzzyMatch') { $scriptContainer.MatchAnywhere = $FuzzyMatch } if ($PSBoundParameters.Keys -contains 'AlwaysQuote') { $scriptContainer.MatchAnywhere = $AlwaysQuote } - if ($PSBoundParameters.Keys -contains 'DontSort') { $scriptContainer.MatchAnywhere = $DontSort } + if ($PSBoundParameters.Keys -contains 'DontSort') { $scriptContainer.DontSort = $DontSort } + if ($PSBoundParameters.Keys -contains 'DontFilter') { $scriptContainer.DoNotFilter = $DontFilter } if ($PSBoundParameters.Keys -contains 'AutoTraining') { $scriptContainer.AutoTraining = $AutoTraining } } } diff --git a/PSFramework/internal/configurations/tabexpansion.ps1 b/PSFramework/internal/configurations/tabexpansion.ps1 index c85fad9b..3ab12fff 100644 --- a/PSFramework/internal/configurations/tabexpansion.ps1 +++ b/PSFramework/internal/configurations/tabexpansion.ps1 @@ -1,4 +1,5 @@ # Settings around the Tab Expansion Experience Set-PSFConfig -Module PSFramework -Name 'TabExpansion.FuzzyMatch' -Value $false -Initialize -Handler { [PSFramework.TabExpansion.TabExpansionHost]::FuzzyMatch = $args[0] } -Validation 'bool' -Description 'Whether to match tab completions with Fuzzy-Matching by default.' Set-PSFConfig -Module PSFramework -Name 'TabExpansion.AlwaysQuote' -Value $false -Initialize -Handler { [PSFramework.TabExpansion.TabExpansionHost]::AlwaysQuote = $args[0] } -Validation 'bool' -Description 'Wrap all completion results into quotes, whitespace or not.' -Set-PSFConfig -Module PSFramework -Name 'TabExpansion.MatchAnywhere' -Value $false -Initialize -Handler { [PSFramework.TabExpansion.TabExpansionHost]::MatchAnywhere = $args[0] } -Validation 'bool' -Description 'Whether to match tab completions with Fuzzy-Matching by default.' \ No newline at end of file +Set-PSFConfig -Module PSFramework -Name 'TabExpansion.MatchAnywhere' -Value $false -Initialize -Handler { [PSFramework.TabExpansion.TabExpansionHost]::MatchAnywhere = $args[0] } -Validation 'bool' -Description 'Whether to match tab completions with Fuzzy-Matching by default.' +Set-PSFConfig -Module PSFramework -Name 'TabExpansion.MaxResults' -Value 0 -Initialize -Handler { [PSFramework.TabExpansion.TabExpansionHost]::MaxResults = $args[0] } -Validation integerpositive -Description 'The maximum number of results show for tab expansion. This is the global setting for ALL completions, specific setting defined via Register-PSFTeppScriptblock take precedence.' \ No newline at end of file diff --git a/PSFramework/internal/filter/environment.filter.ps1 b/PSFramework/internal/filter/environment.filter.ps1 index 2c405363..134709bb 100644 --- a/PSFramework/internal/filter/environment.filter.ps1 +++ b/PSFramework/internal/filter/environment.filter.ps1 @@ -52,7 +52,7 @@ $null = New-PSFFilterConditionSet -Module PSFramework -Name Environment -Version #region Elevation New-PSFFilterCondition @paramCon -Name Elevated -ScriptBlock { if ($PSVersionTable.PSVersion.Major -ge 6 -and $global:IsLinux) { return $true } - if ($PSVersionTable.PSVersion.Major -ge 6 -and $global:IsLinux) { return $true } + if ($PSVersionTable.PSVersion.Major -ge 6 -and $global:IsMacOS) { return $true } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal $identity diff --git a/PSFramework/internal/functions/configuration/Read-PsfConfigEnvironment.ps1 b/PSFramework/internal/functions/configuration/Read-PsfConfigEnvironment.ps1 index e46a21ed..1e9374e8 100644 --- a/PSFramework/internal/functions/configuration/Read-PsfConfigEnvironment.ps1 +++ b/PSFramework/internal/functions/configuration/Read-PsfConfigEnvironment.ps1 @@ -1,5 +1,5 @@ function Read-PsfConfigEnvironment { -<# + <# .SYNOPSIS Reads configuration settings from environment variables. @@ -33,6 +33,7 @@ begin { function ConvertFrom-EnvironmentSetting { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName = $true)] @@ -56,7 +57,7 @@ try { [pscustomobject]@{ FullName = $Name.SubString(($Prefix.Length + 1)) - Value = [PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($Value) + Value = [PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($Value) } } catch { @@ -82,14 +83,26 @@ if ([double]::TryParse($Value, 'Any', [System.Globalization.NumberFormatInfo]::InvariantInfo, [ref]$tempVal)) { return [PSCustomObject]@{ FullName = $fullName; Value = $tempVal } } - $tempVal = $null - if ([datetime]::TryParse($Value, [System.Globalization.DateTimeFormatInfo]::InvariantInfo, 'AssumeUniversal', [ref]$tempVal)) { + $tempVal = Get-Date + if ([datetime]::TryParse($Value, [System.Globalization.DateTimeFormatInfo]::InvariantInfo, [System.Globalization.DateTimeStyles]::AssumeUniversal, [ref]$tempVal)) { return [PSCustomObject]@{ FullName = $fullName; Value = $tempVal } } - if ($Value -match "^.|*") { - return [PSCustomObject]@{ FullName = $fullName; Value = $Value.SubString(2).Split($Value.Substring(0, 1)) } + if ($Value -match "^.\|.+") { + $values = $Value.SubString(2).Split($Value.Substring(0, 1)) + $valueObjects = foreach ($value in $values) { ConvertFrom-EnvironmentSetting -Name "PSF_Whatever" -Value $value -Simple $true -Prefix 'PSF' } + return [PSCustomObject]@{ FullName = $fullName; Value = $valueObjects.Value } + } + if ($Value -match '^\S+:') { + try { + [pscustomobject]@{ + FullName = $fullName + Value = [PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($Value) + } + return + } + catch { <# If it's a legal export, that's fine, but don't count on it #> } } - return [PSCustomObject]@{ FullName = $fullName; Value = $Value } + return [PSCustomObject]@{ FullName = $fullName; Value = $valueObjects.Value } } #endregion Simple Mode } diff --git a/PSFramework/internal/functions/runspace/ConvertFrom-PsfUsingStatement.ps1 b/PSFramework/internal/functions/runspace/ConvertFrom-PsfUsingStatement.ps1 new file mode 100644 index 00000000..ab2b8648 --- /dev/null +++ b/PSFramework/internal/functions/runspace/ConvertFrom-PsfUsingStatement.ps1 @@ -0,0 +1,45 @@ +function ConvertFrom-PsfUsingStatement { + <# + .SYNOPSIS + Removes all using statements from a scriptblock. + + .DESCRIPTION + Removes all using statements from a scriptblock. + Will return an object with two pieces of information: + + + Code: The original scriptblock, just with all $using:-statements replaced with simple variable names. + + Variables: A deduplicated list of all variables used by the scriptblock, that used to be covered under a $using:-Statement + + .PARAMETER ScriptBlock + The scriptblock to remove $using:-statements from. + + .EXAMPLE + PS C:\> ConvertFrom-PsfUsingStatement -ScriptBlock $code + + Removes all $using:-statements from $code + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ScriptBlock] + $ScriptBlock + ) + process { + $allUsings = $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.UsingExpressionAst] }, $true) + $varNames = $allUsings.SubExpression.VariablePath.UserPath | Microsoft.PowerShell.Utility\Sort-Object -Unique + + $scriptBlockText = $ScriptBlock.Ast.Extent.Text + $offset = $ScriptBlock.Ast.Extent.StartOffset + foreach ($usingInstance in $allUsings | Microsoft.PowerShell.Utility\Sort-Object { $_.Extent.StartOffset } -Descending) { + $scriptBlockText = $scriptBlockText.SubString(0, ($usingInstance.Extent.StartOffset - $offset)) + "`${$($usingInstance.SubExpression.VariablePath.UserPath)}" + $scriptBlockText.Substring(($usingInstance.Extent.EndOffset - $offset)) + } + + $code = [PsfScriptBlock]::new([scriptblock]::Create($scriptBlockText), $true) -as [scriptblock] + [PSFramework.Utility.UtilityHost]::SetPrivateProperty("LanguageMode", $code, ([PsfScriptBlock]$ScriptBlock).LanguageMode) + + [PSCustomObject]@{ + Variables = $varNames + Code = $code + } + } +} \ No newline at end of file diff --git a/PSFramework/internal/scripts/teppSimpleCompleter.ps1 b/PSFramework/internal/scripts/teppSimpleCompleter.ps1 index 0a23c385..0472cc2f 100644 --- a/PSFramework/internal/scripts/teppSimpleCompleter.ps1 +++ b/PSFramework/internal/scripts/teppSimpleCompleter.ps1 @@ -145,16 +145,22 @@ } $alwaysQuote = $scriptContainer.AlwaysQuote $sortParam = @{ Property = 'ListItemText' } - if ($scriptContainer.DontSort) { $sortParam = @{ Property = { 1 }}} + if ($scriptContainer.DontSort) { $sortParam = @{ Property = { 1 } } } if (-not $scriptContainer.ShouldExecute) { if ($scriptContainer.Trained.Count -gt 0) { $allItems = @($scriptContainer.LastCompletion) + ($scriptContainer.Trained | ConvertTo-TeppCompletionEntry) } else { $allItems = $scriptContainer.LastCompletion } - foreach ($item in ($scriptContainer.LastCompletion | Where-Object Text -match $scriptContainer.GetPattern($wordToComplete) | Sort-Object @sortParam)) { + $allResults = foreach ($item in ($scriptContainer.LastCompletion | Where-Object Text -Match $scriptContainer.GetPattern($wordToComplete) | Sort-Object @sortParam)) { New-PSFTeppCompletionResult -CompletionText $item.Text -ToolTip $item.ToolTip -ListItemText $item.ListItemText -AlwaysQuote:$alwaysQuote } + + if ($scriptContainer.MaxResults -gt 0 -and @($allResults).Count -gt $scriptContainer.MaxResults) { + @($allResults)[0..($scriptContainer.MaxResults - 1)] + New-Object System.Management.Automation.CompletionResult("", "... showing the first $($scriptContainer.MaxResults) / $(@($allResults).Count) results", "ParameterValue", 'Type more until fewer viable results are returned') + } + else { $allResults } return } @@ -172,9 +178,14 @@ else { $allItems = $items } - foreach ($item in ($allItems | Where-Object Text -match $scriptContainer.GetPattern($wordToComplete) | Sort-Object @sortParam)) { + $allResults = foreach ($item in ($allItems | Where-Object Text -Match $scriptContainer.GetPattern($wordToComplete) | Sort-Object @sortParam)) { New-PSFTeppCompletionResult -CompletionText $item.Text -ToolTip $item.ToolTip -ListItemText $item.ListItemText -AlwaysQuote:$alwaysQuote } + if ($scriptContainer.MaxResults -gt 0 -and @($allResults).Count -gt $scriptContainer.MaxResults) { + @($allResults)[0..($scriptContainer.MaxResults - 1)] + New-Object System.Management.Automation.CompletionResult(" ", "... showing the first $($scriptContainer.MaxResults) / $(@($allResults).Count) results", "ParameterValue", 'Type more until fewer viable results are returned') + } + else { $allResults } $scriptContainer.LastDuration = (Get-Date) - $start if ($items) { diff --git a/PSFramework/tests/functions/tabexpansion/input.Tests.ps1 b/PSFramework/tests/functions/tabexpansion/input.Tests.ps1 index 1e1eb214..fbe28688 100644 --- a/PSFramework/tests/functions/tabexpansion/input.Tests.ps1 +++ b/PSFramework/tests/functions/tabexpansion/input.Tests.ps1 @@ -19,7 +19,8 @@ } It 'can complete input from Get-ChildItem' { (Complete -Expression 'Get-ChildItem | Select-PSFObject ').CompletionText | Should -Match '^Attributes$|^BaseName$|^CreationTime$|^CreationTimeUtc$|^Directory$|^DirectoryName$|^Exists$|^Extension$|^FullName$|^IsReadOnly$|^LastAccessTime$|^LastAccessTimeUtc$|^LastWriteTime$|^LastWriteTimeUtc$|^Length$|^Name$|^Parent$|^PSChildName$|^PSDrive$|^PSIsContainer$|^PSParentPath$|^PSPath$|^PSProvider$|^Root$|^VersionInfo$|^LinkTarget$|^UnixFileMode$' - (Complete -Expression 'Get-ChildItem | Select-PSFObject ').Count | Should -Be 25 + if ($PSVersionTable.PSVersion.Major -le 5) { (Complete -Expression 'Get-ChildItem | Select-PSFObject ').Count | Should -Be 25 } + else { (Complete -Expression 'Get-ChildItem | Select-PSFObject ').Count | Should -Be 27 } } <# diff --git a/PSFramework/xml/PSFramework.Format.ps1xml b/PSFramework/xml/PSFramework.Format.ps1xml index 7bfe5518..106ccc9e 100644 --- a/PSFramework/xml/PSFramework.Format.ps1xml +++ b/PSFramework/xml/PSFramework.Format.ps1xml @@ -714,6 +714,80 @@ + + + PSFramework.Runspace.RunspaceResult + + PSFramework.Runspace.RunspaceResult + + + + + + + + + + + + + InputObject + + + Output + + + Errors + + + + + + + + + + PSFramework.Runspace.RunspaceWrapper + + PSFramework.Runspace.RunspaceWrapper + + + + + + + + + + + + + + + + Name + + + IsRunning + + + ThrottleLimit + + + CountTotal + + + CountPending + + + CountCompleted + + + + + + + PSFramework.TabExpansion.ScriptContainer diff --git a/library/PSFramework/Commands/InvokePSFProtectedCommand.cs b/library/PSFramework/Commands/InvokePSFProtectedCommand.cs index 24e47a75..434df3dd 100644 --- a/library/PSFramework/Commands/InvokePSFProtectedCommand.cs +++ b/library/PSFramework/Commands/InvokePSFProtectedCommand.cs @@ -166,44 +166,9 @@ private string _ErrorMessage /// private string _ErrorScript = @" param ( - $__PSFramework__Message, - - $__PSFramework__Exception, - - $__PSFramework__Target, - - $__PSFramework__Continue, - - $__PSFramework__ContinueLabel, - - $__PSFramework__FunctionName, - - $__PSFramework__ModuleName, - - $__PSFramework__File, - - $__PSFramework__Line, - - $__PSFramework__Cmdlet, - - $__PSFramework__EnableException + $__PSFramework__Parameters ) - -$paramStopPSFFunction = @{ - Message = $__PSFramework__Message - Exception = $__PSFramework__Exception - Target = $__PSFramework__Target - Continue = $__PSFramework__Continue - FunctionName = $__PSFramework__FunctionName - ModuleName = $__PSFramework__ModuleName - File = $__PSFramework__File - Line = $__PSFramework__Line - Cmdlet = $__PSFramework__Cmdlet - EnableException = $__PSFramework__EnableException - StepsUpward = 1 -} -if ($__PSFramework__ContinueLabel) { $paramStopPSFFunction['ContinueLabel'] = $__PSFramework__ContinueLabel } -Stop-PSFFunction @paramStopPSFFunction +Stop-PSFFunction @__PSFramework__Parameters return "; #endregion Private Fields @@ -289,7 +254,7 @@ protected override void ProcessRecord() Thread.Sleep(nextWait); nextWait = (int)((double)nextWait * RetryWaitEscalation); } - + } #endregion Cmdlet Implementation @@ -313,14 +278,29 @@ private void Terminate(Exception error) ) ); } - + if (Continue) DoContinue(ContinueLabel); return; } + Hashtable parameters = new Hashtable(StringComparer.OrdinalIgnoreCase); + parameters["Message"] = _ErrorMessage; + parameters["Exception"] = error; + parameters["Target"] = Target; + parameters["Continue"] = Continue; + if (!String.IsNullOrEmpty(ContinueLabel)) + parameters["ContinueLabel"] = ContinueLabel; + parameters["FunctionName"] = _Caller.CallerFunction; + parameters["ModuleName"] = _Caller.CallerModule; + parameters["File"] = _Caller.CallerFile; + parameters["Line"] = _Caller.CallerLine; + parameters["Cmdlet"] = PSCmdlet; + parameters["EnableException"] = EnableException; + parameters["StepsUpward"] = 1; + ScriptBlock errorBlock = ScriptBlock.Create(_ErrorScript); - object[] arguments = new object[] { _ErrorMessage, error, Target, Continue, ContinueLabel, _Caller.CallerFunction, _Caller.CallerModule, _Caller.CallerFile, _Caller.CallerLine, PSCmdlet, EnableException }; + object[] arguments = new object[] { parameters }; PSCmdlet.InvokeCommand.InvokeScript(false, errorBlock, null, arguments); } @@ -332,7 +312,8 @@ private void ErrorEventAction(Exception error) object errorRecord = error; if (error is RuntimeException) errorRecord = ((RuntimeException)error).ErrorRecord; - try { + try + { WriteMessage(Localization.LocalizationHost.Read("PSFramework.FlowControl.Invoke-PSFProtectedCommand.ErrorEvent", new object[] { _Message, Target }), Level, _Caller.CallerFunction, _Caller.CallerModule, _Caller.CallerFile, _Caller.CallerLine, Tag, Target); table["Result"] = PSCmdlet.InvokeCommand.InvokeScript(false, ErrorEvent, null, errorRecord); WriteMessage(Localization.LocalizationHost.Read("PSFramework.FlowControl.Invoke-PSFProtectedCommand.ErrorEvent.Success", new object[] { _Message, Target }), Level, _Caller.CallerFunction, _Caller.CallerModule, _Caller.CallerFile, _Caller.CallerLine, Tag, Target, table); @@ -344,4 +325,4 @@ private void ErrorEventAction(Exception error) } } } -} +} \ No newline at end of file diff --git a/library/PSFramework/Data/Converters/FileSystemInfoConverter.cs b/library/PSFramework/Data/Converters/FileSystemInfoConverter.cs index 67fb674d..3a74aac3 100644 --- a/library/PSFramework/Data/Converters/FileSystemInfoConverter.cs +++ b/library/PSFramework/Data/Converters/FileSystemInfoConverter.cs @@ -54,7 +54,7 @@ public string Convert(object Value, object[] Parents, int Depth, Psd1Converter C catch { } - if (property.Name == "Root" || property.Name == "Parent" || property.Name == "Directory") + if (property.Name == "Root" || property.Name == "Parent" || property.Name == "Directory" || property.Name == "BaseName") propValue = $"{propValue}"; sb.AppendLine($"{newIndent}{CodeGeneration.EscapeSingleQuotedStringContent(LanguagePrimitives.ConvertTo(property.Name))} = {DataHost.Convert(propValue, newParents, Depth + 1, Converter)}"); diff --git a/library/PSFramework/PSFramework.csproj b/library/PSFramework/PSFramework.csproj index 7deebd98..b356959b 100644 --- a/library/PSFramework/PSFramework.csproj +++ b/library/PSFramework/PSFramework.csproj @@ -218,7 +218,10 @@ + + + diff --git a/library/PSFramework/Runspace/RunspaceResult.cs b/library/PSFramework/Runspace/RunspaceResult.cs new file mode 100644 index 00000000..db8a3fcb --- /dev/null +++ b/library/PSFramework/Runspace/RunspaceResult.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using System.Threading.Tasks; + +namespace PSFramework.Runspace +{ + /// + /// The result of a runspace task + /// + public class RunspaceResult + { + /// + /// The object that triggered the task + /// + public object InputObject; + /// + /// All output + /// + public List Output = new List(); + /// + /// All Information Messages + /// + public List Information = new List(); + /// + /// All verbose messages + /// + public List Verbose = new List(); + /// + /// All warning messages + /// + public List Warnings = new List(); + /// + /// All error records + /// + public List Errors = new List(); + + /// + /// Creates a result object, representing the completed result of the runspace task. + /// + /// The original argument for the task + /// The output result of the task + /// The streams the task sent + public RunspaceResult(object InputObject, PSDataCollection Output, PSDataStreams Streams) + { + this.InputObject = InputObject; + if (Output.Count > 0) + this.Output.AddRange(Output); + if (Streams.Verbose.Count > 0) + Verbose.AddRange(Streams.Verbose); + if (Streams.Warning.Count > 0) + Warnings.AddRange(Streams.Warning); + if (Streams.Error.Count > 0) + Errors.AddRange(Streams.Error); +#if PS4 +#else + if (Streams.Information.Count > 0) + Information.AddRange(Streams.Information); +#endif + } + } +} diff --git a/library/PSFramework/Runspace/RunspaceTask.cs b/library/PSFramework/Runspace/RunspaceTask.cs new file mode 100644 index 00000000..db29911a --- /dev/null +++ b/library/PSFramework/Runspace/RunspaceTask.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace PSFramework.Runspace +{ + /// + /// An individual task executed in the runspace pool of its hosting RunspaceWrapper + /// + public class RunspaceTask : IDisposable + { + /// + /// The item to process in this task + /// + public object InputObject; + + /// + /// Whether the task has completed successfully + /// + public bool IsCompleted + { + get + { + if (null == Status) + return false; + return Status.IsCompleted; + } + } + + internal RunspaceWrapper Host; + + private bool started; + private PowerShell Runtime; + private IAsyncResult Status; + + private string _CodeWrapper = @" +param ($____PSF_Code, $____PSF_Item) +([PSFramework.Utility.PsfScriptBlock]$____PSF_Code).InvokeEx($false, $____PSF_Item, $____PSF_Item, $null, $true, $true, $____PSF_Item) +"; + + /// + /// Create a new runspace task. If the host has already stared execution, it is immediately queued for execution. + /// + /// The hosting RunspaceWrapper + /// The item to process in this task + public RunspaceTask(RunspaceWrapper Host, object InputObject) + { + this.Host = Host; + this.InputObject = InputObject; + Host.CountTotal++; + + if (Host.IsRunning) + Start(); + } + + /// + /// If the task is complete, collect results and direct the streams. Do nothing if not complete yet. + /// Delists itself from the hosting RunspaceWrapper, if completed. + /// + /// The command runtime to whose streams to write the results + /// Do not write to additional streams + /// Whether it successfully collected the results + public bool TryCollect(Cmdlet Command, bool NoStreams = false) + { + if (!IsCompleted) + return false; + + PSDataCollection result = Runtime.EndInvoke(Status); + if (NoStreams) + { + Kill(); + Command.WriteObject(result, true); + return true; + } + +#if PS4 +#else + foreach (InformationRecord info in Runtime.Streams.Information) + Command.WriteInformation(info); +#endif + foreach (VerboseRecord info in Runtime.Streams.Verbose) + Command.WriteVerbose(info.Message); + foreach (WarningRecord warning in Runtime.Streams.Warning) + Command.WriteWarning(warning.Message); + try + { + foreach (ErrorRecord error in Runtime.Streams.Error) + Command.WriteError(error); + } + finally + { + Kill(); + } + + Command.WriteObject(result, true); + return true; + } + + /// + /// Wait until the task completes, then get the full result with all stream information + /// + /// A result object, containing output and streams + /// Don't try to collect results before starting the task + public RunspaceResult CollectResult() + { + if (null == Runtime && null == Status) + throw new InvalidOperationException("Task has not been started yet!"); + + PSDataCollection result = Runtime.EndInvoke(Status); + RunspaceResult resultObj = new RunspaceResult(InputObject, result, Runtime.Streams); + + Kill(); + + return resultObj; + } + + /// + /// Wait until the task completes, then get the output + /// + /// All output results of the task + /// Don't try to collect results before starting the task + public PSDataCollection Collect() + { + if (null == Runtime && null == Status) + throw new InvalidOperationException("Task has not been started yet!"); + + PSDataCollection result = Runtime.EndInvoke(Status); + + Kill(); + + return result; + } + + /// + /// Wait until the task completes, then collect results and direct the streams. + /// + /// The command runtime to whose streams to write the results + /// hether to NOT write to the different streams. + /// Don't try to collect results before starting the task + public void Collect(Cmdlet Command, bool NoStreams = false) + { + if (null == Runtime && null == Status) + throw new InvalidOperationException("Task has not been started yet!"); + + PSDataCollection result = Runtime.EndInvoke(Status); + if (NoStreams) + { + Kill(); + Command.WriteObject(result, true); + return; + } + + +#if PS4 +#else + foreach (InformationRecord info in Runtime.Streams.Information) + Command.WriteInformation(info); +#endif + foreach (VerboseRecord info in Runtime.Streams.Verbose) + Command.WriteVerbose(info.Message); + foreach (WarningRecord warning in Runtime.Streams.Warning) + Command.WriteWarning(warning.Message); + try + { + foreach (ErrorRecord error in Runtime.Streams.Error) + Command.WriteError(error); + } + finally + { + Kill(); + } + + Command.WriteObject(result, true); + } + + /// + /// Start this task, queueing the code as a runspace in the runspace pool for execution + /// + /// If the runspacepool of the hosting RunspaceWrapper has not been opened yet, we cannot start yet + public void Start() + { + if (started) + return; + if (!Host.IsRunning) + throw new InvalidOperationException("Runspace Pool not opened yet!"); + + Runtime = PowerShell.Create(); + Runtime.RunspacePool = Host.Pool; + + Runtime.AddScript(_CodeWrapper) + .AddParameter("____PSF_Code", Host.Code) + .AddParameter("____PSF_Item", InputObject); + Status = Runtime.BeginInvoke(); + + started = true; + } + + /// + /// Cancel and destroy this task. + /// + public void Kill() + { + if (Runtime != null) + { + if (Runtime.Runspace != null) + Runtime.Runspace.Dispose(); + Runtime.Dispose(); + } + + Host.Tasks.Remove(this); + } + + /// + /// Clean up this object + /// + public void Dispose() + { + Kill(); + } + } +} diff --git a/library/PSFramework/Runspace/RunspaceWrapper.cs b/library/PSFramework/Runspace/RunspaceWrapper.cs new file mode 100644 index 00000000..e9e9216a --- /dev/null +++ b/library/PSFramework/Runspace/RunspaceWrapper.cs @@ -0,0 +1,298 @@ +using Microsoft.PowerShell.Commands; +using PSFramework.PSFCore; +using PSFramework.Utility; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace PSFramework.Runspace +{ + /// + /// Runspace managing class used by Invoke-PSFRunspace. + /// + public class RunspaceWrapper : IDisposable + { + /// + /// Nme of the workload + /// + public string Name = ""; + + /// + /// The code to run in parallel + /// + public ScriptBlock Code; + + /// + /// How many runspace tasks to execute in parallel + /// + public int ThrottleLimit = 5; + + /// + /// Total number of tasks in this wrapper + /// + public int CountTotal { get; internal set; } + + /// + /// Number of Tasks still pending + /// + public int CountPending => Tasks.Where(t => !t.IsCompleted).Count(); + + /// + /// Number of Tasks completed + /// + public int CountCompleted => CountTotal - CountPending; + + /// + /// What each runspace task will have available + /// + public InitialSessionState InitialSessionState = InitialSessionState.CreateDefault(); + + /// + /// List of tasks to execute + /// + public List Tasks = new List(); + + /// + /// Variables available to all tasks + /// + public Dictionary Variables = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Functions available to all tasks + /// + public Dictionary Functions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Modules available to all tasks + /// + public List Modules = new List(); + + /// + /// Whether the RunspaceWrapper is currently open for tasks + /// + public bool IsRunning { get; internal set; } + + internal RunspacePool Pool; + + #region Content + /// + /// Add a variable to the initial sessionstate + /// + /// name of the variable + /// Value of the variable + public void AddVariable(string Name, object Value) + { + Variables[Name] = new SessionStateVariableEntry(Name, Value, ""); + } + /// + /// Add multiple variables to the initial sessionstate + /// + /// Name/value map of variables to inclue + public void AddVariable(Hashtable VariableHash) + { + foreach (object key in VariableHash) + Variables[key.ToString()] = new SessionStateVariableEntry(key.ToString(), VariableHash[key], ""); + } + + /// + /// Add a module by name or path + /// + /// Name or path to the module + public void AddModule(string Module) + { + Modules.Add(new ModuleSpecification(Module)); + } + /// + /// Add a module by its module info object + /// + /// The module info object + public void AddModule(PSModuleInfo Module) + { + Modules.Add(new ModuleSpecification(Module.ModuleBase)); + } + + /// + /// Define a function available to all tasks + /// + /// + /// + /// + public void AddFunction(string Name, ScriptBlock Definition) + { + if (PSFCoreHost.ConstrainedConsole && (new PsfScriptBlock(Definition)).LanguageMode != PSLanguageMode.FullLanguage) + throw new PSSecurityException("Console is running in a secure context, function cannot be untrusted!"); + Functions[Name] = new SessionStateFunctionEntry(Name, Definition.ToString()); + } + /// + /// Define a function available to all tasks + /// + /// Function info object to copy over + /// + public void AddFunction(FunctionInfo Function) + { + if (PSFCoreHost.ConstrainedConsole) + throw new PSSecurityException("Console is running in a secure context, defining a function via FunctionInfo object is not supported!"); + Functions[Function.Name] = new SessionStateFunctionEntry(Function.Name, Function.Definition); + } + #endregion Content + + #region Execution + /// + /// Start the entire wrapper, creating a runspace pool and preparing for execution + /// + public void Start() + { + // Prepare Sessionstate + foreach (SessionStateVariableEntry value in Variables.Values) + InitialSessionState.Variables.Add(value); + if (Modules.Count > 0) + InitialSessionState.ImportPSModule(Modules); + foreach (SessionStateFunctionEntry value in Functions.Values) + InitialSessionState.Commands.Add(value); + + Pool = RunspaceFactory.CreateRunspacePool(InitialSessionState); + Pool.SetMinRunspaces(1); + Pool.SetMaxRunspaces(ThrottleLimit); + Pool.ApartmentState = System.Threading.ApartmentState.MTA; + Pool.Open(); + + IsRunning = true; + + if (Tasks.Count > 0) + foreach (RunspaceTask task in Tasks) + task.Start(); + } + + /// + /// Close the runspace pool, terminate everything and clean up. + /// + public void Stop() + { + IsRunning = false; + Pool.Close(); + Pool.Dispose(); + } + + /// + /// Make sure everything is cleaned out after the job is done + /// + public void Dispose() + { + Stop(); + } + #endregion Execution + + #region Tasks + /// + /// Add a task that should be executed + /// + /// The argument for which the task should be executed + public void AddTask(object InputObject) + { + Tasks.Add(new RunspaceTask(this, InputObject)); + } + + /// + /// Add a list of tasks that all should be executed + /// + /// The arugments for each of which the task should be executed + public void AddTaskBulk(IEnumerable InputObjects) + { + if (null == InputObjects) + return; + foreach (object item in InputObjects) + Tasks.Add(new RunspaceTask(this, item)); + } + + /// + /// Wait for all task results and receive results directly into the streams of the calling command + /// + /// The command runtime whose streams to write to + /// Whether additional streams should be hidden and only output shown + public void Collect(Cmdlet Command, bool NoStreams = false) + { + while (Tasks.Count > 0) + { + // Since we stream data to the calling command's streams - which may be in a pipeline - we don't want to block on a longer running task. + // Hence we cycle through the tasks, collect what is ready and sleep on our own. + foreach (RunspaceTask task in Tasks.ToArray().Where(t => t.IsCompleted)) + task.Collect(Command, NoStreams); + Thread.Sleep(50); + } + } + + /// + /// Collect all tasks that already completed, and directly write the results to the streams of the calling command + /// + /// The command runtime whose streams to write to + /// Whether additional streams should be hidden and only output shown + public void CollectCurrent(Cmdlet Command, bool NoStreams = false) + { + foreach (RunspaceTask task in Tasks.ToArray()) + task.TryCollect(Command, NoStreams); + } + + /// + /// Wait for all task results and return result report objects, including information on all streams of the runspace + /// + /// List of completion reports, including all output, warnings, errors, informational messages, etc. + public List CollectResult() + { + List results = new List(); + + foreach (RunspaceTask task in Tasks.ToArray()) + results.Add(task.CollectResult()); + + return results; + } + + /// + /// Retrieve result report objects for each task already completed, including information on all streams of the runspace + /// + /// List of completion reports, including all output, warnings, errors, informational messages, etc. + public List CollectCurrentResult() + { + List results = new List(); + + foreach (RunspaceTask task in Tasks.ToArray().Where(t => t.IsCompleted)) + results.Add(task.CollectResult()); + + return results; + } + + /// + /// Wait for all task results and return the output. + /// + /// The resulting output of all tasks + public List Collect() + { + List results = new List(); + foreach (RunspaceTask task in Tasks.ToArray()) + foreach (PSObject item in task.Collect()) + results.Add(item); + + return results; + } + + /// + /// Receive the output of all currently completed tasks + /// + /// The output of all currently completed tasks + public List CollectCurrent() + { + List results = new List(); + foreach (RunspaceTask task in Tasks.ToArray().Where(t => t.IsCompleted)) + foreach (PSObject item in task.Collect()) + results.Add(item); + + return results; + } + #endregion Tasks + } +} diff --git a/library/PSFramework/TabExpansion/ScriptContainer.cs b/library/PSFramework/TabExpansion/ScriptContainer.cs index 1e131cf5..0ffa5684 100644 --- a/library/PSFramework/TabExpansion/ScriptContainer.cs +++ b/library/PSFramework/TabExpansion/ScriptContainer.cs @@ -66,6 +66,26 @@ public class ScriptContainer /// public bool Global; + /// + /// When enabled, do not filter based on user input. + /// + public bool DoNotFilter; + + private int _MaxResults; + /// + /// Maximum number of results to show when tab-completing. + /// + public int MaxResults + { + get + { + if (_MaxResults > 0) + return _MaxResults; + return TabExpansionHost.MaxResults; + } + set { _MaxResults = value; } + } + /// /// If true: Match input against any part of the options, not just the beginning /// @@ -197,6 +217,9 @@ public string[] Invoke() /// The resolved pattern to match with public string GetPattern(string WordToComplete) { + if (DoNotFilter) + return ".*"; + StringBuilder stringBuilder = new StringBuilder(); if (!MatchAnywhere && !FuzzyMatch) stringBuilder.Append("^['\"]{0,1}"); diff --git a/library/PSFramework/TabExpansion/TabExpansionHost.cs b/library/PSFramework/TabExpansion/TabExpansionHost.cs index efaa3a46..4cc9bc57 100644 --- a/library/PSFramework/TabExpansion/TabExpansionHost.cs +++ b/library/PSFramework/TabExpansion/TabExpansionHost.cs @@ -55,6 +55,11 @@ public static Hashtable Cache /// Whether PSFramework completion should use fuzzy-matching when matching completion values with the already typed text. /// public static bool FuzzyMatch; + + /// + /// The maximum number of results shown to the user, before truncating data sets. + /// + public static int MaxResults; #endregion Settings #region Public logic access diff --git a/library/PSFramework/Utility/UtilityHost.cs b/library/PSFramework/Utility/UtilityHost.cs index ed0ebd6d..02456680 100644 --- a/library/PSFramework/Utility/UtilityHost.cs +++ b/library/PSFramework/Utility/UtilityHost.cs @@ -474,7 +474,7 @@ public static object GetPrivateField(string Name, object Instance) FieldInfo property = Instance.GetType().GetField(Name, BindingFlags.Instance | BindingFlags.NonPublic); if (property == null) - throw new ArgumentException(LocalizationHost.Read(String.Format("PSFramework.Assembly.UtilityHost.PrivateFieldNotFound", Name)), "Name"); + throw new ArgumentException(LocalizationHost.Read("PSFramework.Assembly.UtilityHost.PrivateFieldNotFound", new string[] { Name }), "Name"); return property.GetValue(Instance); }