# encoding: ascii # api: powershell # title: Start-ScriptThreading # description: Ever wanted to start some threads on a block of code but hated the headache of creating, watching, and collecting job data? This is my attempt to put a bandaid on that issue. # version: 0.1 # type: function # author: Kenneth W Hightower JR # license: CC0 # function: Start-ScriptThreading # x-poshcode-id: 5758 # x-derived-from-id: 5759 # x-archived: 2017-01-08T12:56:01 # x-published: 2017-02-25T18:35:00 # # There are some assumptions made in this function, such as assuming you are threading to talk to a bunch of computers. Also assuming you only have one parameter to pass into your custom scriptblock, as well as assuming that the computer is a parameter for your scriptblock. # I hope you enjoy this nugget as I return to my retirement on Gallifrey… # function Start-ScriptThreading{ <# .SYNOPSIS Runs a scriptblock against multiple computers simultaneously .DESCRIPTION The ThreadedFunction function takes an array (usually a list of computers) and a scriptblock. The scriptblock will be run against each item in the array expecting each item of the array to also be a parameter expected in the scriptblock. .PARAMETER arComputers List of computers to run scriptblock against. .PARAMETER ScriptBlock Block of script to run against list from parameter -Computers .PARAMETER maxThreads Maximum number of threads to run at a time .PARAMETER SleepTimer Time to wait between loops. This is a spacing timer to prevent the main loop from hogging cpu time. A smaller number may create and check for jobs faster. A large number will allow the separate threads more cpu time. Any longer than 1000 milliseconds may render the progress indicator incorectly. (default is 500 milliseconds) .PARAMETER MaxWaitAtEnd Sets a timeout for threads before killing jobs. This timer is started after the last thread is created. (default is 180 seconds) .NOTES Name: ThreadedScript Author: Kenneth W Hightower JR (The13thDoctor) DateCreated: 11Feb2015 DateModified: 25Feb2015 To load this function into the current shell for use, or to use this file seperate from your main script, dot-source as follows '. .\ThreadedFunction.ps1' .EXAMPLE Start-ScriptThreading -Computers 'server01', 'server02' -ScriptBlock {ping $Computer} The command 'ping' is threaded and ran against both server01 and server02. Declaring $Computer is required as is. .EXAMPLE 'server01', 'server02' | Start-ScriptThreading -ScriptBlock {Get-WMIObject win32_computersystem -ComputerName $Computer} Pipes the array as Computers and gets the Win32_ComputerSystem WMI object from server01 and server02 simultaneously. .INPUTS System.String. Function will accept an array of strings and assume each as a parameter for the provided scriptblock. .OUTPUTS System.Object. All returned data from each iteration that the scriptblock may create is returned as a single array. #> param( [parameter(Mandatory=$true,ValueFromPipeline=$true)] [string[]]$arComputers, [parameter(Mandatory=$true,ValueFromPipeline=$false)] [scriptblock]$ScriptBlock, [parameter(Mandatory=$false)] [int]$maxThreads = 30, [parameter(Mandatory=$false)] [int]$SleepTimer = 500, [parameter(Mandatory=$false)] [int]$MaxWaitAtEnd = 180 ) BEGIN{ Write-Verbose "Initializing..." $return = @() $StartTime= Get-Date #Create working scriptblock $Script = [ScriptBlock]::Create( @" param(`$Computer) &{ $ScriptBlock } @PSBoundParameters "@ ) $Computers = @() } PROCESS{ foreach($Computer in $arComputers){ $Computers += $Computer #Yes, this will flatten multi-dimentional arrays... } } #when you pipeline to this function the begin section is called once, then for each item in the pipeline the entire process block is called. #In order to collect all items in the pipeline so that everything passed in can be threaded simultaneously as intended, we handle the threading at the end. #Otherwise each item will get threaded and collected before the next item is threaded. END{ #Start Making Jobs Write-Verbose "creating threads..." Foreach($Computer in $Computers){ #Check for running threads while ($(Get-Job -State Running | measure-object).count -ge $maxThreads){ write-host "$((Get-Job -State Running | measure-object).count) threads running, please wait..." start-sleep -s 3 } # End while ($(Get-Job -State Running Write-Verbose "starting $($Computer)" Start-Job -name $Computer -scriptblock $Script -ArgumentList $Computer #Pretty progress indicator $ComputersStillRunning = "" ForEach ($System in $(Get-Job -state running)){ $ComputersStillRunning += ", $($System.name)" } $ComputersStillRunning = $ComputersStillRunning.TrimStart(", ") Write-Progress -Activity "Creating Jobs" ` -Status "$($(Get-Job -State Running).count) threads running..." ` -CurrentOperation "$ComputersStillRunning" ` -PercentComplete ($(Get-Job -State Completed).count / $(Get-Job).count * 100) #Check for completed Jobs. Depending on number of jobs, we might as well collect data as we go. ForEach($Job in $(Get-Job -State Completed)){ If($Job.HasMoreData -eq "True"){ Write-Verbose "Job $($Job.Id) finished early" $Data = Receive-Job $Job $Return+=$Data }#end if($job.hasmoredata... }#end foreach($Job... }#end Foreach($Computer in... #Done creating jobs. Write-Verbose "Waiting for final threads..." $Complete = Get-date #Wait for the last created jobs to finish #Following loop gets skipped if pipeline is used to pass $Computers While ($(Get-Job -State Running | measure-object).count -gt 0){ #random information for progress screen $ComputersStillRunning = "" $TimeDiff = New-TimeSpan $StartTime $(Get-Date) [string]$strTimingStatus = "{0} minutes, {1} seconds so far" -f $TimeDiff.Minutes, $TimeDiff.Seconds ForEach ($System in $(Get-Job -state running)){ $ComputersStillRunning += ", $($System.name)" } $ComputersStillRunning = $ComputersStillRunning.TrimStart(", ") Write-Progress -Activity "Waiting for completion..." ` -Status "$($(Get-Job -State Running).count) threads remaining: $strTimingStatus" ` -CurrentOperation "$ComputersStillRunning" ` -PercentComplete ($(Get-Job -State Completed).count / $(Get-Job).count * 100) #Collecting jobs as we wait ForEach($Job in $(Get-Job -State Completed)){ If($Job.HasMoreData -eq "True"){ Write-Verbose "Job $($Job.Id) just completed" $Data = Receive-Job $Job $return+=$Data }#end if($job.hasmoredata }#end foreach($Job #Done waiting. Threads most likely hung up on something, throw them away. If ($(New-TimeSpan $Complete $(Get-Date)).totalseconds -ge $MaxWaitAtEnd){ Write-Verbose "Killing all jobs still running, tired of waiting..." Get-Job -State Running | Remove-Job -Force } #Lets not bog the CPU with lightning speed looping Start-Sleep -Milliseconds $SleepTimer } #Final cleaning, just in case.... ForEach($Job in $(Get-Job -State Completed)){ If($Job.HasMoreData -eq "True"){ Write-Verbose "Job $($Job.Id) completed late" $Data = Receive-Job $Job $return+=$Data }#end if($job.hasmoredata }#end foreach($Job Write-Verbose "Cleaning up threads" Get-Job | Remove-Job -Force | Out-Null #All this data needs to go somewhere.... return $return } }#end function ThreadedScript