functions/runspace/Invoke-PSFRunspace.ps1
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: <undefined> .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 = '<undefined>', [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() } } |