Functions/Start-CommandMultiThreaded.ps1

function Start-CommandMultiThreaded {
<#
.SYNOPSIS
    Takes a single command and multithreads it.
.DESCRIPTION
    Will multithread any command/cmdlet/function you specify.
.PARAMETER Command
    Where you specify the command you want to multithread.
.PARAMETER Objects
    The arguments that are provided to the command. Generally used for specifying the name of one or more computers. However, it can be used for specifying other arguments such as a list of users.
.PARAMETER MaxThreads
    The maximum threads to run. Can cause resource issues.
.PARAMETER MaxTime
    The amount of seconds to run the script after last job (object) is started.
.PARAMETER SleepTimer
    The amount of milliseconds between each time the script checks the status of jobs. For high resource utilization on the system or if the script is going to take longer to run, this should be increased.
.PARAMETER AddParameter
    Allows specifying additional parameters beyond what is used in Objects. Need to format in a hash table. Ex:
    @{"ParameterName" = "Value"}
    or
    @{"ParameterName" = "Value";"AnotherParameter" = "AnotherValue"}
.PARAMETER AddSwitch
    Allows specifying additional switches to add to the command you run. Need to format in a single string or an array of strings. Ex:
    "TotalCount"
    or
    @("TotalCount","All")
.EXAMPLE
    C:\PS>Start-CommandMultiThreaded Clear-Space (gc c:\Scripts\comps.txt)
    Will run the Clear-Space command against nine of the computers in the comps.txt file at a time. This is because the -MaxThreads parameter isn't set so it runs at the default of 9 objects at a time.
.EXAMPLE
    C:\PS>gc c:\Scripts\comps.txt | Start-CommandMultiThreaded Clear-Space
    Will run the Clear-Space command against nine of the computers in the comps.txt file at a time. This is because the -MaxThreads parameter isn't set so it runs at the default of 9 objects at a time.
.EXAMPLE
    C:\PS>Start-CommandMultiThreaded -Command Get-Service -Objects (gc c:\Scripts\comps.txt) -AddParameter @{"Name" = "wuauserv"} -AddSwitch @('RequiredServices','DependentServices')
    Will get the service "wuauserv" and it's dependent/required services from the computers listed in comps.txt.
.EXAMPLE
    C:\PS>Start-CommandMultiThreaded -Command Set-AxwayConfig -Objects COMP1,COMP2 -AddParameter @{"ConfigFile" = "C:\PKI\MyOrgsAxwayConfig.txt"}
    Will set the Axway config file on both the computer COMP1 and COMP2 at the same time using C:\PKI\MyOrgsAxwayConfig.txt on those computers as the file to import.
.INPUTS
    System.Management.Automation.PSObject.System.String
.OUTPUTS
    System.Management.Automation.PSCustomObject
.COMPONENT
    WSTools
.FUNCTIONALITY
    Multithread, multitask
.NOTES
    Author: Skyler Hart
    Created: Sometime before 2017-08-07
    Last Edit: 2022-09-05 22:19:49
    Other:
.LINK
    https://wanderingstag.github.io
#>

    [CmdletBinding()]
    Param (
        [Parameter()]
        [string]$Command,

        [Parameter(
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string[]]$Objects,

        [Parameter()]
        [int32]$MaxThreads = 9,

        [Parameter()]
        [int32]$MaxTime = 300,

        [Parameter()]
        [int32]$SleepTimer = 500,

        [Parameter()]
        [HashTable]$AddParameter,

        [Parameter()]
        [Array]$AddSwitch
    )

    Begin {
        $ISS = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)
        $RunspacePool.Open()
        If ($(Get-Command | Select-Object Name) -match $Command) {
            $Code = $Null
        }
        Else {
            $Code = [ScriptBlock]::Create($(Get-Content $Command))
        }
        $Jobs = @()
    }
    Process {
        Write-Progress -Activity "Loading threads" -Status "Starting Job $($jobs.count)"
        ForEach ($Object in $Objects){
            If ([string]::IsNullOrWhiteSpace($Code)) {
                $PowershellThread = [PowerShell]::Create().AddCommand($Command)
            }
            Else {
                $PowershellThread = [PowerShell]::Create().AddScript($Code)
            }

            $PowershellThread.AddArgument($Object.ToString()) | Out-Null
            ForEach ($Key in $AddParameter.Keys) {
                $PowershellThread.AddParameter($Key, $AddParameter.$key) | Out-Null
            }
            ForEach ($Switch in $AddSwitch) {
                $Switch
                $PowershellThread.AddParameter($Switch) | Out-Null
            }
            $PowershellThread.RunspacePool = $RunspacePool
            $Handle = $PowershellThread.BeginInvoke()
            $Job = "" | Select-Object Handle, Thread, object
            $Job.Handle = $Handle
            $Job.Thread = $PowershellThread
            $Job.Object = $Object.ToString()
            $Jobs += $Job
        }
    }
    End {
        $ResultTimer = Get-Date
        While (@($Jobs | Where-Object {$null -ne $_.Handle}).count -gt 0)  {
            $Remaining = "$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)"
            If ($Remaining.Length -gt 60) {
                $Remaining = $Remaining.Substring(0,60) + "..."
            }
            Write-Progress -Activity "Waiting for Jobs To Finish - $($MaxThreads - $($RunspacePool.GetAvailableRunspaces())) of $MaxThreads threads running" `
                -PercentComplete (($Jobs.count - $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) / $Jobs.Count * 100) `
                -Status "$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining - $remaining"
            ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})) {
                $Job.Thread.EndInvoke($Job.Handle)
                $Job.Thread.Dispose()
                $Job.Thread = $Null
                $Job.Handle = $Null
                $ResultTimer = Get-Date
            }
            If (($(Get-Date) - $ResultTimer).totalseconds -gt $MaxTime) {
                Write-Error "Script appears to be frozen, try increasing MaxResultTime"
                Exit
            }
            Start-Sleep -Milliseconds $SleepTimer
        }
        $RunspacePool.Close() | Out-Null
        $RunspacePool.Dispose() | Out-Null
    }
}