PowerShell: Do/Until loop until multiple jobs are completed - powershell

Morning folks,
I'm working on a Do/Until loop that tracks an invoke-command job against multiple servers. What I don't know how to capture in the Until block is to continue doing the loop until all servers report a Completed status.
below is my test code
Invoke-Command -ComputerName Server01,Server02,Server03 -ScriptBlock{sleep -s 120} -AsJob -JobName "Test Job"
Do{
Write-Host "Job is still running"
sleep -s 10
}Until((Get-Job "Test Job").state -eq "Completed"
I'm thinking I need to put the following in the Until block but I'm not sure
Until(ForEach($job in (Get-Job "Test Job")){
$Job.State -eq "Completed"
})

I don't have the means to test this at the moment, but if you're after a progress output, I don't see why this wouldn't work.
$jobs = Invoke-Command -ComputerName Server01,Server02,Server03 -ScriptBlock{sleep -s 120} -AsJob
do {
foreach ($job in ($jobs.ChildJobs | ? 'State' -EQ 'Running')
{
Write-Host -Object (
"Job {0} is still running on computer {1}." -f $job.Name, $job.Location
)
}
Start-Sleep -Seconds 1
} until ($jobs.ChildJobs.State -notcontains 'Running')
As mentioned above, I don't have the means to test this, nor do I recall if the State property you'd get from the parent job takes into account for the child jobs running. Technically speaking, what you tried would have worked as well given it had the closing parenthesis.
Do{
Write-Host "Job is still running"
sleep -s 10
}Until((Get-Job "Test Job").state -eq "Completed")
I didn't account for every issue you may come across but one thing you may be wary of is that the State property may not always be in a "Completed", or "Running" state. This could lead to an indefinite loop testing for a "Completed" status.

Related

Cannot break do-while loop in Powershell

I have been working on this PowerShell script. I'm still pretty new at this. The way it works is that I give it a list of servers which it goes though and restarts the App Pools 1 by 1. I have been having a problem with the snippet below. I do not use Restart-WebAppPool because it sometimes gives the App Pool a start before it's ready and leaves it stopped. I cannot show the whole script because it's big and has proprietary info. The problem that I'm having is I can't seem to break the do-while loop. In it I'm checking App Pool status to make sure that it's stopped.
What I get appears for $PL_Break appears to be a valid string showing "Stopped". However, even when it shows "Stopped" it doesn't break the loop.
$PL_Timeout = New-TimeSpan -Seconds 95
foreach ($PL_Server in $PL_ServerName)
{
$PL_Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
Write-host "`n`n`nRestarting App Pool : $PL_AppPool"
write-host "Stopping: " $PL_Server -f Green
Invoke-Command -ComputerName $PL_Server -ArgumentList $PL_AppPool -ScriptBlock {param($PL_App) Stop-WebAppPool -Name $PL_App}
do {
sleep 5
$PL_Br = Invoke-Command -ComputerName $PL_Server -ArgumentList $PL_AppPool -ScriptBlock {param($PL_App) Get-IISAppPool $PL_App | Select-Object State}
$PL_Break = [string]$PL_Br.State.value
} while (($PL_Stopwatch.elapsed -lt $PL_Timeout) -or ($PL_Break -ne "Stopped"))
} # Foreach - Server

PowerShell - provide console output to user while waiting for jobs (start-job) to finish

I use this script to run some jobs:
#========================================================================
#Get User stats with ADInfo < --- need to see if this can be converted to native PowerShell
$XMLConfig = Get-ChildItem c:\ADInfo\XML_Config_Files
$Jobs = #()
#loop through each config file and run ADAudit - there is one file per domain
foreach ($config in $XMLConfig) {
write-host "Starting a background job for $($config.name)"
$Jobs += start-job -ScriptBlock {c:\ADInfoCmd.exe /config $args[0] } -ArgumentList $config.fullname.tostring()
}
write-host "`nJobs are running"
#=======================================================================
#End of script
Some jobs take much longer than others and I would like to be able to send a user friendly update to the console when any one of the started jobs are still running to show the script hasn't stalled.
I tried something like this
do{
write-host "working..."
}
while (wait-job $jobs)
but it writes once and then waits for the jobs to finish
I tried this
$joblist = get-job $jobs | where state -eq running
while ($joblist){
write-host "working..."
}
but I get an error for all the jobs get-job : The command cannot find the job because the job name System.Management.Automation.PSRemotingJob was not found and $joblist is never assigned a value.
Is there a way to do this?
I had passed the entire PS Object to get-job. It worked when I passed only the job ID
This is what I ended up using and provides enough feedback to the user to demonstrate the script is still working.
write-host "`nJobs are running" -ForegroundColor Yellow -NoNewline
$RunningJobs = Get-Job $jobs.id | where state -eq running
while($runningjobs){
write-host "." -NoNewline
$RunningJobs = Get-Job $jobs.id | where state -eq running
Start-Sleep -Seconds 3
}
Write-host "Background Jobs Complete"
Write-Host "Script Ends" -ForegroundColor Yellow

Breaking out of a PS command after a certain amount of time?

I'm looking for an example that can break out of the current command if it runs for longer than say, 1 minutes, which would allow me to re-run it.
Here is my script :
$ServiceName = 'service'
$arrService = Get-Service -Name $ServiceName
while ($arrService.Status -ne 'Running')
{
Start-Service $ServiceName
write-host $arrService.status
write-host 'Service starting'
Start-Sleep -seconds 3
$arrService.Refresh()
if ($arrService.Status -eq 'Running')
{
Write-Host 'Service is now Running'
}
}
Use Start-Job Start-Job cmdlet to run the process. Then define Wait-job
Wait-Job cmdlet with a -timeout. The -timeout defines the time for the wait to end and the command prompt will return.
A point to remember: -timeout don't display any error messages. Keep that in mind while doing error handling.
Could use some polish, but something like below may work:
$ServiceName = 'W32Time'
$arrService = Get-Service -Name $ServiceName
$Interval = 3
$TimeOut = 20
$Count = 0
Start-Service $ServiceName
:SrvCheck While ( $arrService.Status -ne 'Running' )
{
$arrService.Refresh()
if ( $arrService.Status -eq 'Running' )
{
Write-Host 'Service is now Running'
Break SrvCheck
}
if ( $Count -ge $timeout )
{
Write-Host "Timed out wating for service : $ServiceName to start, waited $($Count*$Interval) Seconds..."
Break SrvCheck
}
Write-Host "Waiting for service start, Current Status $($arrService.Status) ... Checks: $Count | Time Elapsed: $($Count*$Interval) ."
Start-Sleep -Seconds $Interval
++$Count
}
Moved the service start up out of the loop, unless it was your intent to reattempt the start after every failed check. Even so you may want to put some error handling around it. I know for example UAC can interfere in a service start request so you may want something there.
So the $TimeOut variable is 20 so each interval being 3 seconds the attempt will time out at 60 seconds.
If you want the check to occur at least once I'd switch this into a Do Until. With While, if the service starts quickly enough you won't see the message 'Service is now running'
Let me know how it goes.

Get child job's child job output

If you start a job (A) and this job, in turn, starts another job (B), is it possible to fetch the output of (B) by using something as such?
(Get-Job -Name A).ChildJobs[0].ChildJobs[0]...
I was hoping I could recursively drill down into the object but curiously (Get-Job -Name A).ChildJobs[0] always has an empty ChildJobs collection. This may be just a misunderstanding on my part of how jobs are created.
One workaround to this is to wait until job B finishes, fetch its output, store it in a variable, and perform a Write-Output on the variable so the parent script can handle it. It works but that means I have to wait until job B finishes which might take 10-40 minutes.
I could perhaps (in job B) write output immediately as it occurs to a flat file or perhaps a SQLite database but I was hoping I could fetch it from the top-most scope of the script.
Here is an example
Get-Job | Remove-Job
$ErrorActionPreference = "stop"
$Level1Job = {
$Level2Job = {
$Level3Job = {
Write-Output "Level 3, start"
Start-Sleep -s 5
Write-Output "Level 3, end"
}
Write-Output "Level 2, start"
# start the third job...
Start-Job -ScriptBlock $Level3Job -Name "level 3"
# wait for the job to complete
while(get-job | where-object { ($_.State -ne "completed") -and ($_.State -ne "failed") }){ Start-Sleep -s 2 }
Start-Sleep -s 5
Write-Output "Level 2, end"
}
Write-Output "Level 1, start"
# start the second job on the remote computer...
Start-Job -ScriptBlock $Level2Job -Name "level 2"
# wait for the job to complete
while(get-job | where-object { ($_.State -ne "completed") -and ($_.State -ne "failed") }){ Start-Sleep -s 2 }
Start-Sleep -s 5
Write-Output "Level 1, end"
}
# start the first job...
Start-Job -ScriptBlock $Level1Job -Name "level 1"
# wait for the job to complete
while(get-job | where-object { ($_.State -ne "completed") -and ($_.State -ne "failed") }){ Start-Sleep -s 2 }
Start-Sleep –s 5
(get-job)[0].ChildJobs[0] | fl *
ChildJobs are used for remote-jobs. If you use for example Invoke-Command -ScriptBlock { ... } -AsJob -ComputerName YourRemoteComputer you get a remote-job (the ChildJob) and a local job, that handles the remote-job.
You have the Job-Object from start-Job. if your first job returns this object, you can receive it and read the output

Powershell: Run multiple jobs in parralel and view streaming results from background jobs

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