# encoding: ascii
# api: powershell
# title: Start-ComputerJobs
# description: The Start-ComputerJobs cmdlet runs a specified scriptblock within a maximum number of simultaneously spawned Powershell jobs. 
# version: 1.2
# type: function
# author: Tome Tanasovski
# license: CC0
# function: Start-ComputerJobs
# x-poshcode-id: 1941
# x-archived: 2010-07-02T00:05:49
#
# You must pass a list of ComputerNames to it.  The names passed to the cmdlet will replace $computername within the scriptblock prior to starting each thread.
# This can be thought of as a multithreaded approach to a foreach loop.
# The name is probably a bad one, but it suited my needs.  I may rework it to be more generic.
#
#requires -version 2.0
function Start-ComputerJobs{
<#
    .NOTES
     Name: Start-ComputerJobs
     Author: Tome Tanasovski
     Created: 6/25/2010
     Modified: 6/25/2010
     Version: 1.2
     Website: http://powertoe.wordpress.com
        
    .SYNOPSIS
     Multithreads a scriptblock with jobs
 
    .DESCRIPTION
     The Start-ComputerJobs cmdlet runs a specified scriptblock within a maximum number of simultaneously spawned Powershell jobs.  
     
     You must pass a list of ComputerNames to it.  The names passed to the cmdlet will replace $computername within the scriptblock prior to starting each thread.
     
     This can be thought of as a multithreaded approach to a foreach loop.
 
    .PARAMETER ComputerName
     Specifies a list of computers that you wish to inject into your scriptblock.  The list of computer names will replace $computername in the script block.  This does not actually need to be a list of computer names.  It can be anything you wish to inject into your scriptblock.
     
    .PARAMETER ScriptBlock
     Specifies the commands to run in the background jobs.  Enclose the commands in braces ({}) or use $ExecutionContext.InvokeCommand.NewScriptBlock to create a scriptblock.
     
     The scriptblock must contain a $computername variable.  $computername will be replaced by each entry into the pipeline or from the parameter ComputerName.
     
    .PARAMETER Threads
     Specifies the maximum number of jobs that can be simultaneously running at any given time.
     
     This defaults to 10 if nothing is specified.
     
    .PARAMETER Sleep
     Specifies the amount of time in seconds to sleep before querying the active jobs.  This defaults to 5 seconds.  If your jobs are long running you will want to increase this number.  If they are very quick to run you may want to decrease this number.
 
    .EXAMPLE
     $computers = "computer1","computer2","computer3","computer4","computer5" 
     Start-ComputerJobs -computername $computers -ScriptBlock {dir \\$computername\c$} 
     
     Queries the share for each computer in the computer list.  This will spawn the default 5 jobs at once with a 5 second pause between querying the jobs.
        
    .EXAMPLE
     Get-Content computerlist.txt |Start-ComputerJobs -ScriptBlock {Get-Service -ComputerName $computername} -Threads 3 -Sleep 10 -Verbose
     
     Reads a list of computers from a file named computerlist.txt.  For each computer listed it will return the results from get-service.  It will do this with three simultaneous jobs and will sleep 10 seconds between querying the jobs.  Verbose will tell you when each job has been launced.
        
    .INPUTS
     System.String        
 
    .OUTPUTS
     PSObject
        
     Returns the results of the commands in the job.  It is the data returned from receive-job.  If you're scriptblock contains write-host it will write that to the console as it is running.  To avoid this you should just return objects within your scriptblock. i.e. Instead of write-host "text" just use "text"
#>

    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string[]]$ComputerName,
        [Parameter(Mandatory=$true)]
        [ScriptBlock]$ScriptBlock,
        [Parameter(Mandatory=$false)]
        [ValidateScript({$_ -gt 0})]
        [int32]$Threads = 10,
        [Parameter(Mandatory=$false)]
        [ValidateScript({$_ -gt 0})]
        [int32]$Sleep = 5
        
    )    
    BEGIN {        
        $firstrun = $true
        $computers = @()
        $curthreads = 0
        $jobs = @()
        $valid = $false
    }
    PROCESS {
        if ($firstrun) {
            $firstrun = $false
                        
            # Validate scriptblock - Making sure it has a $computername variable to use       
            if ($ScriptBlock -match '\$computername') {
                $valid = $true                
            } else {
                Write-Error 'You must specify a scriptblock that contains a variable called $computername'
                $valid = $false
            }
        }
        if ($valid) {
            foreach ($computer in $computername) {
                while ($jobs.count -eq $Threads) {
                    $newjobs = @()
                    $jobs |foreach {
                        Receive-Job $_.id
                        if ($_.state -eq "Running" -or $_.HasMoreData) {
                            $newjobs += $_
                        }
                        else {
                            $_ |Remove-Job
                        }
                    }
                    $jobs = $newjobs
                    if ($jobs.count -eq $Threads) {
                        sleep $sleep
                    }
                }                        
                $runscript = $ExecutionContext.InvokeCommand.NewScriptBlock(($ScriptBlock -replace '\$computername', "$computer"))
                Write-Verbose "Starting Job for computer: $computer"
                $jobs += Start-Job -ScriptBlock $runscript
                $curthreads++
            }
        }
    }
    END {
        while ($jobs.count -ne 0) {
            $newjobs = @()
            $jobs |foreach {
                Receive-Job $_.id
                if ($_.state -eq "Running" -or $_.HasMoreData) {
                    $newjobs += $_
                }
                else {
                    $_ |Remove-Job
                }
            }
            $jobs = $newjobs
            if ($jobs.count -gt 0) {
                sleep $sleep
            }            
        }  
    }
}
