Advertisement
Old-Lost

Invoke-Parallel

Apr 19th, 2017
182
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. function Invoke-Parallel {
  2.     <#
  3.         .SYNOPSIS
  4.         Run a scriptblock against input objects in parallel.
  5.         .DESCRIPTION
  6.         Execute a scriptblock against all input objects. Uses PSJobs to run multiple jobs in parallel. Note that variables, functions, and modules will not be available in the scriptblock. For variables and functions this can be overridden on an individual basis using the -Variable and -Function parameters.
  7.         .PARAMETER  Scriptblock
  8.         The script block to execute. Refer to the object using the $_ (or $PSItem) automatic variable.
  9.         .PARAMETER  InputObject
  10.         The set of objects to run against. This can be specified on the command line or through the pipeline.
  11.         .PARAMETER  Function
  12.         Specifies a list of FunctionInfo objects that will be available within the scriptblock. Use the "Get-Item function:<FunctionName>" cmdlet to get the object(s).
  13.         .PARAMETER  Variable
  14.         Specifies a list of variables names whose names and values will be available within the scriptblock. Any changes to the value of these variables in the scriptblock will not be reflected in the calling scope.
  15.         .PARAMETER  Throttle
  16.         The number of jobs allowed to run in parallel. The default value is 10.
  17.         .PARAMETER  Timeout
  18.         The number of seconds any single job is allowed to run before being forcibly terminated. The default is 0, meaning no time limit.
  19.         .PARAMETER  SleepMilliseconds
  20.         The number of milliseconds to pause between checking the job queue for finished jobs. The default is 200, meaning the queue is checked 5 times a second.
  21.         .PARAMETER  Quiet
  22.         Suppress the progress bar. By default the progress bar is displayed.
  23.         .PARAMETER  Activity
  24.         Specifies the first line of text in the heading above the status bar. This text describes the activity whose progress is being reported.
  25.         .PARAMETER  Format
  26.         A scriptblock used to format the input object into text. Use $PSItem (or $_) for the input object. The result of the scriptblock is used in the progress bar.
  27.         .PARAMETER  Property
  28.         A property of the input object to use in the progress bar text. The property's value is converted to a string for display purposes.
  29.         .EXAMPLE
  30.         Get-Content c:\servers.txt | Invoke-Parallel -Scriptblock { Test-Connection $_ }
  31.         .EXAMPLE
  32.         Get-Something 'One value' 32
  33.         .INPUTS
  34.         Whatever is expected by the scriptblock specified using the -Scriptblock parameter
  35.         .OUTPUTS
  36.         The type of object output by the -Scriptblock parameter, if any
  37.         .NOTES
  38.         Any job terminated through timeout will of course not produce any additional output.
  39.         Uses ProgressManager class from https://pastebin.com/QrVLYZST
  40.         Uses Remove-Member function from https://pastebin.com/gjwmFA4v
  41.     #>
  42.     [CmdletBinding(DefaultParameterSetName = 'Format')]
  43.     Param (
  44.         [Parameter(Mandatory, Position = 0)][scriptblock]$ScriptBlock,
  45.         [Parameter(Mandatory, ValueFromPipeline)]$InputObject,
  46.         [System.Management.Automation.FunctionInfo[]]$Function,
  47.         [string[]]$Variable,
  48.         [int]$Throttle = 10,
  49.         [int]$Timeout = 0,
  50.         [Alias('SleepTimer')][int]$SleepMilliseconds = 200,
  51.         [Parameter(ParameterSetName = 'Quiet')][Alias('HideProgress')][switch]$Quiet,
  52.         [Parameter(ParameterSetName = 'Format')][Parameter(ParameterSetName = 'Property')][string]$Activity = 'Monitoring Progress',
  53.         [Parameter(ParameterSetName = 'Format')][scriptblock]$Format = { $_.ToString() },
  54.         [Parameter(ParameterSetName = 'Property')][ValidateNotNullOrEmpty()][string]$Property
  55.     )
  56.     BEGIN {
  57.         if ((-not $PSBoundParameters.ContainsKey('Quiet')) -and (-not [Environment]::UserInteractive)) {
  58.             $Quiet = $true
  59.         }
  60.         $NewScriptBlock = [scriptblock]::Create('Set-Variable x -Value @($input) -Option Private; $private:x | ForEach-Object { ' + $ScriptBlock.ToString() + ' }')
  61.         [hashtable]$StartJobParams = @{ ScriptBlock = $NewScriptBlock }
  62.         #region Generate InitializationScript for all jobs
  63.         [System.Collections.Generic.List`1[string]]$InitScript = @()
  64.         [void]$InitScript.Add("Set-Location '$PWD'")
  65.         if ($PSBoundParameters['Function']) {
  66.             foreach ($f in $Function) {
  67.                 $Name = $f.Name
  68.                 try {
  69.                     [string]$Definition = $f.Definition
  70.                     [void]$InitScript.Add("function $Name { $Definition }")
  71.                 } catch {
  72.                     Write-Warning "${Name}: $($_.Exception.Message)"
  73.                 }
  74.             }
  75.         }
  76.         if ($PSBoundParameters['Variable']) {
  77.             [string]$TempfileName = (New-TemporaryFile).FullName
  78.             $Vars = $Variable | % {
  79.                 [PSCustomObject]@{
  80.                     Name  = $_
  81.                     Value = $PSCmdlet.GetVariableValue($_)
  82.                 }
  83.             }
  84.             $Vars | Export-Clixml $TempfileName -Encoding UTF8 -Depth 4
  85.             [void]$InitScript.Add("Import-Clixml '$TempfileName' | ForEach-Object { try { Set-Variable -Name `$_.Name -Value `$_.Value -WhatIf:`$false -Scope Script } catch {} }")
  86.         }
  87.         $StartJobParams.InitializationScript = [scriptblock]::Create($InitScript -join [System.Environment]::NewLine)
  88.         # Write-Verbose "InitializationScript: $($StartJobParams.InitializationScript.ToString())"
  89.         #endregion
  90.         [boolean]$IgnoreProcess = $false
  91.         [System.Collections.Generic.List`1[System.Object]]$List = @()
  92.         [ProgressManager]$pm = [ProgressManager]::new(0)
  93.         $pm.Activity = $Activity
  94.         If ($PSBoundParameters.ContainsKey('InputObject')) {
  95.             [void]$List.AddRange(@($InputObject))
  96.             $IgnoreProcess = $true
  97.             $pm.Total = $List.Count
  98.         }
  99.         $pm.Format = switch ($PSCmdlet.ParameterSetName) {
  100.             'Format' { $Format; break }
  101.             'Property' { { $_ | Select-Object -Property $Property | % { $_.$Property } | Out-String }; break }
  102.             default { { $_.ToString() } }
  103.         }
  104.         [System.Management.Automation.Job]$Job = $null
  105.         [System.Collections.Generic.List`1[System.Management.Automation.Job]]$Jobs = @()
  106.         [System.Collections.Generic.List`1[System.Management.Automation.Job]]$CompletedJobs = @()
  107.         [System.Collections.Generic.List`1[System.Management.Automation.Job]]$TimedOutJobs = @()
  108.         $RemoveMemberParams = @{
  109.             Name     = 'PSComputerName', 'PSShowComputerName', 'RunspaceId', 'PSSourceJobInstanceId'
  110.             PassThru = $true
  111.         }
  112.         [scriptblock]$Cleanup = {
  113.             # test-ending
  114.             trap { continue }
  115.             $ErrorActionPreference = 'SilentlyContinue'
  116.             # Write-Verbose "END finally block starting"
  117.             if ((Get-Variable TempfileName -ErrorAction Ignore) -and (Test-Path $TempfileName)) {
  118.                 Remove-Item -Path $TempfileName -Verbose:$false -Confirm:$false -ErrorAction Ignore
  119.             }
  120.             $pm.Complete()
  121.             $Jobs | Stop-Job -PassThru | Wait-Job | Remove-Job -Force
  122.             [void]$Jobs.Clear()
  123.         }
  124.         [scriptblock]$ProcessJobs = {
  125.             param ([string]$Phase)
  126.             # Write-Verbose "Start ProcessJobs ($Phase)"
  127.             if ($Phase -ne 'PROCESS') {
  128.                 # Write-Verbose "Sleeping for $SleepMilliseconds milliseconds"
  129.                 Start-Sleep -Milliseconds $SleepMilliseconds
  130.             }
  131.             # Output any data from jobs
  132.             $Jobs.where{ $_.HasMoreData }.foreach{ Receive-Job -Job $_ | Remove-Member @RemoveMemberParams }
  133.             #region Process completed jobs
  134.             [void]$CompletedJobs.Clear()
  135.             $Jobs | ? { $_.State -in @('Completed', 'Failed') } | % { [void]$CompletedJobs.Add($_) }
  136.             $CompletedJobs.where{ $_ }.foreach{
  137.                 $Job = $_
  138.                 if ($Job.HasMoreData) { Receive-Job -Job $Job | Remove-Member @RemoveMemberParams }
  139.                 # Write-Verbose "Removing job $($_.Name) from queue"
  140.                 Remove-Job -Job $Job -ErrorAction Ignore
  141.                 [void]$Jobs.Remove($Job)
  142.                 $pm.Update()
  143.             }
  144.             #endregion
  145.             #region Kill any jobs over time limit
  146.             if ($Timeout -and $Jobs.Count) {
  147.                 [void]$TimedOutJobs.Clear()
  148.                 $Jobs | ? { (New-TimeSpan $_.PSBeginTime).TotalSeconds -gt $Timeout } | % { [void]$TimedOutJobs.Add($_) }
  149.                 $TimedOutJobs.where{ $_ }.foreach{
  150.                     $Job = $_
  151.                     Write-Verbose "Removing job $($Job.Name) because of timeout"
  152.                     if ($Job.HasMoreData) { Receive-Job -Job $Job | Remove-Member @RemoveMemberParams }
  153.                     Stop-Job -Job $Job -ErrorAction Ignore
  154.                     Remove-Job -Job $Job -ErrorAction Ignore
  155.                     [void]$Jobs.Remove($Job)
  156.                     $pm.Update()
  157.                 }
  158.             }
  159.             #endregion
  160.             #region Start new jobs if under limit
  161.             while ($Jobs.Count -lt $Throttle -and $List.Count) {
  162.                 $Object = $List[0]
  163.                 if (-not $Object) {
  164.                     # Write-Verbose "Null object...skipping"
  165.                     [void]$List.RemoveAt(0)
  166.                     continue
  167.                 }
  168.                 $Job = $Object | Start-Job @StartJobParams
  169.                 $Job | Add-Member -NotePropertyName InputObject -NotePropertyValue $Object -Force
  170.                 # Write-Verbose "Adding job $($Job.Name) to queue"
  171.                 [void]$Jobs.Add($Job)
  172.                 [void]$List.RemoveAt(0)
  173.             }
  174.             #endregion
  175.             #region Update progress bar
  176.             if (-not $Quiet -and $Jobs.Count -and ($Phase -ne 'BEGIN')) {
  177.                 $pm.WriteProgress($Jobs[-1].InputObject)
  178.             }
  179.             # Write-Verbose "End ProcessJobs ($Phase)"
  180.             #endregion
  181.         } # $ProcessJobs
  182.         . $ProcessJobs 'BEGIN'
  183.     }
  184.     PROCESS {
  185.         if ($IgnoreProcess) { return }
  186.         # Write-Verbose "Adding $($InputObject | Out-String -Stream) to list"
  187.         [void]$List.Add($InputObject)
  188.         $pm.Total += 1
  189.         # Start-Sleep -Milliseconds 10
  190.         [bool]$Breaking = $true
  191.         try {
  192.             . $ProcessJobs 'PROCESS'
  193.             $Breaking = $false
  194.         } finally {
  195.             if ($Breaking) { . $Cleanup }
  196.         }
  197.     }
  198.     END {
  199.         try {
  200.             while ($Jobs.Count -gt 0) { . $ProcessJobs 'END' }
  201.         } finally {
  202.             . $Cleanup
  203.             # Write-Verbose "END finally block ending"
  204.         }
  205.     }
  206. } # Invoke-Parallel
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement