# 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
