Run a job for each server in a list. I only want 5 jobs running at a time. When a job completes, it should start a new job on the next server on the list. Here's what I have so far, but I can't get it to start a new job after the first 5 jobs have ran:
$MaxJobs = 5
$list = Get-Content ".\list.csv"
$Queue = New-Object System.Collections.Queue
$CurrentJobQueue = Get-Job -State Running
$JobQueueCount = $CurrentJobQueue.count
ForEach($Item in $list)
{
Write-Host "Adding $Item to queue"
$Queue.Enqueue($Item)
}
Function Global:Remote-Install
{
$Server = $queue.Dequeue()
$j = Start-Job -Name $Server -ScriptBlock{
If($JobQueueCount -gt 0)
{
Test-Connection $Server -Count 15
}##EndIf
}##EndScriptBlock
}
For($i = 0 ;$i -lt $MaxJobs; $i++)
{
Remote-Install
}
PowerShell will do this for you if you use Invoke-Command e.g.:
Invoke-Command -ComputerName $serverArray -ScriptBlock { .. script here ..} -ThrottleLimit 5 -AsJob
BTW I don't think your use of a .NET Queue is going to work because Start-Job fires up another PowerShell process to execute the job.
You may take a look at the cmdlet Split-Pipeline of the module SplitPipeline.
The code will look like:
Import-Module SplitPipeline
$MaxJobs = 5
$list = Get-Content ".\list.csv"
$list | Split-Pipeline -Count $MaxJobs -Load 1,1 {process{
# process an item from $list represented by $_
...
}}
-Count $MaxJobs limits the number of parallel jobs. -Load 1,1 tells to pipe
exactly 1 item to each job.
The advantage of this approach is that the code itself is invoked synchronously
and it outputs results from jobs as if all was invoked sequentially (even
output order can be preserved with the switch Order).
But this approach does not use remoting. The code works in the current PowerShell session in several runspaces.
Related
I have:
for($i = 1 ; $i -le 3; $i++)
{
Start-Process powershell.exe
}
but I don't know how I would make the new windows run a ping command. Could be done with an extra script but have no idea. Thanks
Start-Process has an -ArgumentList parameter that can be used to pass arguments to the new PowerShell process.
powershell.exe and pwsh.exe have a -Command parameter that can be used to pass them a command.
You can combine the two like this:
for ($i = 1; $i -le 3; $i++)
{
Start-Process powershell.exe -ArgumentList '-NoExit',"-Command ping 127.0.0.$i"
}
If you don't use the -NoExit parameter the window will close as soon as the ping command finishes.
As mentioned in the comments to the question it is also possible to ping multiple hosts using the Test-Connection command like this:
Test-Connection -TargetName 127.0.0.1,127.0.0.2
This has a downside though in that it seems to ping one after the other rather than doing it in parallel.
Another way to do much the same thing in parallel, and probably more PowerShell style is to use jobs:
$jobs = #()
for ($i = 1; $i -le 3; $i++)
{
$jobs += Start-ThreadJob -ArgumentList $i { PARAM ($i)
Test-Connection "127.0.0.$i"
}
}
Wait-Job $jobs
Receive-Job $jobs -Wait -AutoRemoveJob
Note: Start-ThreadJob is newish. If you're still stuck on version 5 of PowerShell that comes with Windows use Start-Job instead, it spawns new processes where as Start-ThreadJob doesn't.
Nitpickers' corner
For those in the comments saying that appending to an array is slow. Strictly a more PowerShell way of doing this is given below. For three items, however, you won't be able to measure the difference and the readability of the code is way lower. It's also rather diverging from the original question.
1..3 | % { Start-ThreadJob -ArgumentList $_ { PARAM($i) Test-Connection "127.0.0.$i" } } | Wait-Job | Receive-Job -Wait -AutoRemoveJob
Here's a pinger script I have to watch multiple computers.
# pinger.ps1
# example: pinger comp001
# pinger $list
param ($hostnames)
#$pingcmd = 'test-netconnection -port 515'
$pingcmd = 'test-connection'
$sleeptime = 1
$sawup = #{}
$sawdown = #{}
foreach ($hostname in $hostnames) {
$sawup[$hostname] = $false
$sawdown[$hostname] = $false
}
#$sawup = 0
#$sawdown = 0
while ($true) {
# if (invoke-expression "$pingcmd $($hostname)") {
foreach ($hostname in $hostnames) {
if (& $pingcmd -count 1 $hostname -ea 0) {
if (! $sawup[$hostname]) {
echo "$([console]::beep(500,300))$hostname is up $(get-date)"
# [pscustomobject]#{Hostname = $hostname; Status = 'up'; Date = get-date} # format-table waits for 2 objects
$sawup[$hostname] = $true
$sawdown[$hostname] = $false
}
} else {
if (! $sawdown[$hostname]) {
echo "$([console]::beep(500,300))$hostname is down $(get-date)"
# [pscustomobject]#{Hostname = $hostname; Status = 'down'; Date = get-date}
$sawdown[$hostname] = $true
$sawup[$hostname] = $false
}
}
}
sleep $sleeptime
}
Example usage (it beeps):
.\pinger comp001,comp002
comp001 is up 07/13/2022 12:07:59
comp002 is up 07/13/2022 12:08:00
I have developed a script which does a lot of processing for a front end tool, now I am attempting to have the script run with multiple threads. It interacts a lot with SQL databases, this should not be a problem for multithreading as the database transactions are very short lived, and the queries well optimised.
what is the issue ?
#.\tester.ps1 -servers (1,'Server1',3,1),(2,'Server2',3,1) -output_folder 'C:\temp'
param ([array[]]$servers),$output_folder
for ($i = 0; $i -lt $servers.Count; $i++)
{
$myserverid = $servers[$i][0]
$myservername = $servers[$i][1]
$mylocationid = $servers[$i][2]
$myappid = $servers[$i][3]
write-output " $myserverid and $myservername and $mylocationid and $myappid"
invoke-sqlcmd -ServerInstance "$myservername" -query "select top 10 name from sysobjects" -Database "master"
}
The script file above will gets passed an array of servers and currently it will loop through the array one by one. A way for me to make the process faster is to run the script in parallel /run the script with multiple threads.
Research
I have looked at a technet script on https://gallery.technet.microsoft.com/scriptcenter/Run-a-PowerShell-script-991c8a42
Its not quite the same as my array is not just a list of servers, there will be other parameters sent with it.
What am I after
A way or pointer to make the script be able to run in parallel or an example using the provided script above.
Thanks in advance.
Extending my comment. In PowerShell v5, use Jobs and Workflows for Parallel use cases.
# Example using parallel jobs
$start = Get-Date
# get all hotfixes
$task1 = { Get-Hotfix }
# get all scripts in your profile
$task2 = { Get-Service | Where-Object Status -eq Running }
# parse log file
$task3 = { Get-Content -Path $env:windir\windowsupdate.log | Where-Object { $_ -like '*successfully installed*' } }
# run 2 tasks in the background, and 1 in the foreground task
$job1 = Start-Job -ScriptBlock $task1
$job2 = Start-Job -ScriptBlock $task2
$result3 = Invoke-Command -ScriptBlock $task3
# wait for the remaining tasks to complete (if not done yet)
$null = Wait-Job -Job $job1, $job2
# now they are done, get the results
$result1 = Receive-Job -Job $job1
$result2 = Receive-Job -Job $job2
# discard the jobs
Remove-Job -Job $job1, $job2
$end = Get-Date
# Example, using WorkFlow
workflow Test-WFConnection
{
param
(
[string[]]$Computers
)
foreach -parallel ($computer in $computers)
{
Test-Connection -ComputerName $computer -Count 1 -ErrorAction SilentlyContinue
}
}
I made a powershell script, that reads remote PCs registry keys, and prints them out to an html page.
Sometimes remote PCs freeze/hang, etc. This increases the final html page by around 40 sec for each frozen PC.
How can I time just a part of my script, let's say 1-2 commands and if that time gets too large, i terminate that command and continue script with the next PC out of PC name-array?
Or maybe the solution is not in timing, is there other way? Thanks!
Smth like:
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', $remote[$i]) + timer runs in parallel + if condition for the timer count. And when the counter reaches threshold terminate OpenRemoteBaseKey & continue
Execute the statement as a job. Monitor the time outside the job. If the job runs longer than you prefer, kill it.
$job = Invoke-Command `
-Session $s
-ScriptBlock { $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', $remote[$i]) } `
-AsJob `
-JobName foo
$int = 0
while (($job.State -like "Running") -and ($int -lt 3)) {
Start-Sleep -Seconds 1
$int++
}
if ($Job.State -like "Running") { $job | Stop-Job }
I need to generate some load on about 100 Windows 2012 R2 servers. The idea I have is to use PowerShell remoting to kick off my script the generate the CPU load. I am also trying to control the load by checking the current load and if it is over X% don't spawn a new job. It's not the most elegant script, but the script works if I execute it locally. When I run it remotely things go wrong.
it seems as if the foreach loop does nothing. So I just have an idle process
it will start about 15 jobs with no issues then the rest all report as having failed.
I have tried various methods of calling the function. I have modified the function so that it contains a server variable but still not getting the desired result. I have replaced Start-Job with Invoke-Command. PSRemoting is working I can do basic tasks on the remote servers.
Any guidance would be appreciated
Below is my code
function Invoke-CPUStress
{
[CmdletBinding()]
param (
$duration
)
$start = (get-date).AddMinutes(1)
$end = $start.AddMinutes($duration)
while ((Get-Date) -lt $start)
{
sleep -Seconds 10
}
while ((get-date) -lt $end)
{
$Load = (Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
If ($Load -lt 60)
{
Start-Job -ScriptBlock {
$result = 1;
foreach ($number in 1..2147483647)
{
$result = $result * $number
sleep -Milliseconds 1
}
}
}
}
Get-Job | Stop-Job
Get-Job | Remove-Job
}
Invoke-CPUStress -duration 5
Overview
Looking to call a Powershell script that takes in an argument, runs each job in the background, and shows me the verbose output.
Problem I am running into
The script appears to run, but I want to verify this for sure by streaming the results of the background jobs as they are running.
Code
###StartServerUpdates.ps1 Script###
#get list of servers to update from text file and store in array
$servers=get-content c:\serverstoupdate.txt
#run all jobs, using multi-threading, in background
ForEach($server in $servers){
Start-Job -FilePath c:\cefcu_it\psscripts\PSPatch.ps1 -ArgumentList $server
}
#Wait for all jobs
Get-Job | Wait-Job
#Get all job results
Get-Job | Receive-Job
What I am currently seeing:
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
23 Job23 Running True localhost #patch server ...
25 Job25 Running True localhost #patch server ...
What I want to see:
Searching for approved updates ...
Update Found: Security Update for Windows Server 2003 (KB2807986)
Update Found: Windows Malicious Software Removal Tool - March 2013 (KB890830)
Download complete. Installing updates ...
The system must be rebooted to complete installation.
cscript exited on "myServer" with error code 3.
Reboot required...
Waiting for server to reboot (35)
Searching for approved updates ...
There are no updates to install.
cscript exited on "myServer" with error code 2.
Servername "myServer" is fully patched after 2 loops
I want to be able to see the output or store that somewhere so I can refer back to be sure the script ran and see which servers rebooted, etc.
Conclusion:
In the past, I ran the script and it went through updating the servers one at a time and gave me the output I wanted, but when I started doing more servers - this task took too long, which is why I am trying to use background jobs with "Start-Job".
Can anyone help me figure this out, please?
You may take a look at the module SplitPipeline.
It it specifically designed for such tasks. The working demo code is:
# import the module (not necessary in PS V3)
Import-Module SplitPipeline
# some servers (from 1 to 10 for the test)
$servers = 1..10
# process servers by parallel pipelines and output results immediately
$servers | Split-Pipeline {process{"processing server $_"; sleep 1}} -Load 1, 1
For your task replace "processing server $_"; sleep 1 (simulates a slow job) with a call to your script and use the variable $_ as input, the current server.
If each job is not processor intensive then increase the parameter Count (the default is processor count) in order to improve performance.
Not a new question but I feel it is missing an answer including Powershell using workflows and its parallel possibilities, from powershell version 3. Which is less code and maybe more understandable than starting and waiting for jobs, which of course works good as well.
I have two files: TheScript.ps1 which coordinates the servers and BackgroundJob.ps1 which does some kind of check. They need to be in the same directory.
The Write-Output in the background job file writes to the same stream you see when starting TheScript.ps1.
TheScript.ps1:
workflow parallelCheckServer {
param ($Servers)
foreach -parallel($Server in $Servers)
{
Invoke-Expression -Command ".\BackgroundJob.ps1 -Server $Server"
}
}
parallelCheckServer -Servers #("host1.com", "host2.com", "host3.com")
Write-Output "Done with all servers."
BackgroundJob.ps1 (for example):
param (
[Parameter(Mandatory=$true)] [string] $server
)
Write-Host "[$server]`t Processing server $server"
Start-Sleep -Seconds 5
So when starting the TheScript.ps1 it will write "Processing server" 3 times but it will not wait for 15 seconds but instead 5 because they are run in parallel.
[host3.com] Processing server host3.com
[host2.com] Processing server host2.com
[host1.com] Processing server host1.com
Done with all servers.
In your ForEach loop you'll want to grab the output generated by the Jobs already running.
Example Not Tested
$sb = {
"Starting Job on $($args[0])"
#Do something
"$($args[0]) => Do something completed successfully"
"$($args[0]) => Now for something completely different"
"Ending Job on $($args[0])"
}
Foreach($computer in $computers){
Start-Job -ScriptBlock $sb -Args $computer | Out-Null
Get-Job | Receive-Job
}
Now if you do this all your results will be mixed. You might want to put a stamp on your verbose output to tell which output came from.
Or
Foreach($computer in $computers){
Start-Job -ScriptBlock $sb -Args $computer | Out-Null
Get-Job | ? {$_.State -eq 'Complete' -and $_.HasMoreData} | % {Receive-Job $_}
}
while((Get-Job -State Running).count){
Get-Job | ? {$_.State -eq 'Complete' -and $_.HasMoreData} | % {Receive-Job $_}
start-sleep -seconds 1
}
It will show all the output as soon as a job is finished. Without being mixed up.
If you're wanting to multiple jobs in-progress, you'll probably want to massage the output to help keep what output goes with which job straight on the console.
$BGList = 'Black','Green','DarkBlue','DarkCyan','Red','DarkGreen'
$JobHash = #{};$ColorHash = #{};$i=0
ForEach($server in $servers)
{
Start-Job -FilePath c:\cefcu_it\psscripts\PSPatch.ps1 -ArgumentList $server |
foreach {
$ColorHash[$_.ID] = $BGList[$i++]
$JobHash[$_.ID] = $Server
}
}
While ((Get-Job).State -match 'Running')
{
foreach ($Job in Get-Job | where {$_.HasMoreData})
{
[System.Console]::BackgroundColor = $ColorHash[$Job.ID]
Write-Host $JobHash[$Job.ID] -ForegroundColor Black -BackgroundColor White
Receive-Job $Job
}
Start-Sleep -Seconds 5
}
[System.Console]::BackgroundColor = 'Black'
You can get the results by doing something like this after all the jobs have been received:
$array=#()
Get-Job -Name * | where{$array+=$_.ChildJobs.output}
.ChildJobs.output will have anything that was returned in each job.
function OutputJoblogs {
[CmdletBinding(DefaultParameterSetName='Name')]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[System.Management.Automation.Job] $job,
[Parameter(Mandatory=$true, Position=1)]
[string] $logFolder,
[Parameter(Mandatory=$true, Position=2)]
[string] $logTimeStamp
)
#Output All logs
while ($job.sate -eq "Running" -or $job.HasMoreData){
start-sleep -Seconds 1
foreach($remotejob in $job.ChildJobs){
if($remotejob.HasMoreData){
$output=(Receive-Job $remotejob)
if($output -gt 0){
$remotejob.location +": "+ (($output) | Tee-Object -Append -file ("$logFolder\$logTimeStamp."+$remotejob.Location+".txt"))
}
}
}
}
#Output Errors
foreach($remotejob in $job.ChildJobs){
if($remotejob.Error.Count -gt0){$remotejob.location +": "}
foreach($myerr in $remotejob.Error){
$myerr 2>&1 | Tee-Object -Append -file ("$logFolder\$logTimeStamp."+$remotejob.Location+".ERROR.txt")
}
if($remotejob.JobStateInfo.Reason.ErrorRecord.Count -gt 0){$remotejob.location +": "}
foreach($myerr in $remotejob.JobStateInfo.Reason.ErrorRecord){
$myerr 2>&1 | Tee-Object -Append -file ("$logFolder\$logTimeStamp."+$remotejob.Location+".ERROR.txt")
}
}
}
#example of usage
$logfileDate="$((Get-Date).ToString('yyyy-MM-dd-HH.mm.ss'))"
$job = Invoke-Command -ComputerName "servername1","servername2" -ScriptBlock {
for ($i=1; $i -le 5; $i++) {
$i+"`n";
if($i -gt 2){
write-error "Bad thing happened"};
if($i -eq 4){
throw "Super Bad thing happened"
};
start-sleep -Seconds 1
}
} -asjob
OutputJoblogs -Job $job -logFolder "$PSScriptRoot\logs" -logTimeStamp $logfileDate