Can't get wait-job result - powershell

I'm trying to achieve this with a code:
☑ Run a command
☑ Wait X seconds
☐ If command completed and is successful i return 1
☐ If command completed and is not successful i return 0
☑ If command did not completed I return 0.
I don't need it to be asynchronous. This is my first code:
$timeoutSeconds = 10
$code = {
ping something
}
$job = Start-Job -ScriptBlock $code
if (Wait-Job $job -Timeout $timeoutSeconds) { Receive-Job $job }
Remove-Job -force $job
if ($job.State -eq 'Completed'){
$ExitCode = 0
}
else {
$ExitCode = 1
}
My Powershell
[System.Environment]::OSVersion.Version
Major Minor Build Revision
----- ----- ----- --------
6 2 9200 0
I would like this to be not only for my powershell version.
I tried to add a return inside my $code
$code = {
ping something
return $LASTEXITCODE
}
But I can't find anything inside my $job.
I tried to assign Receive-Job to a variable, but it doesn't get never populated.
if (Wait-Job $job -Timeout $timeoutSeconds) { $r = Receive-Job $job }
$r is alwasys empty (in both cases: Wait-Job reach or not the timeout).
I want to get the $LASTEXITCODE i'm returning in my $code block, I would like something like:
$timeoutSeconds = 10
$code = {
ping 2.3.1.2
return $LASTEXITCODE
}
$job = Start-Job -ScriptBlock $code
if (Wait-Job $job -Timeout $timeoutSeconds) { Receive-Job $job }
Remove-Job -force $job
if ($job.State -eq 'Completed'){
$ExitCode = $job.ReturnedValue #this doesn't exists
}
else {
$ExitCode = 1
}
Write-Output $ExitCode
My problem is how to discern if:
Job completed in less than timeout because it succeeded
Job completed in less than timeout because it failed faster than I thought.
The command I'm running is a software command I would like to leave unkown, it is a something like a ping but:
the command finish in less than 10 seconds when it works (return 0)
the command finish in more than 10 seconds (30 or more) when the resource is unreachable (return 1)
the command finish in less than 10 seconds when something went wrong (return 1)
If the case is 2 the job is not completed.
If the case is either 1 or 2 I need the returned value inside the $code block to understand if the command is good or not.
How do I get the returned value inside the $code block outside after the job is completed? (if job is not completed I'm aware I can't have it, but I should be able to use the result of a job)
Thanks

If Wait-Job -Timeout <timeoutInSeconds> returns the job object then you know the job completed in time. If a terminating error occurred that caused the job to exit prematurely, the State property on the job object will reflect this, so simply do:
$timeoutSeconds = 10
$completedInTime = $false
$code = {
MyCommands
}
# start job, wait for timeout
$job = Start-Job -ScriptBlock $code
if($job |Wait-Job -Timeout $timeoutSeconds){
# If it completed successfully, State will have the value `Completed`
$completedInTime = $job.State -eq 'Completed'
$results = $job |Receive-Job
}
$job |Remove-Job -Force
# `$true` = 1
# `$false` = 0
$ExitCode = [int]$completedInTime

Thanks to #Mathias R. Jessen I achieved this:
$timeoutSeconds = 10
$completedInTime = $false
$code = {
ping 2.13.1.2 /n 1 > null # this way results won't have this output that I don't need
return $LASTEXITCODE
}
# start job, wait for timeout
$job = Start-Job -ScriptBlock $code
if($job |Wait-Job -Timeout $timeoutSeconds){
# If it completed successfully, State will have the value `Completed`
if($job.State -eq 'Completed'){
# Only in this case results will hold the LASTEXITCODE I returned in code block
$results = $job |Receive-Job
if ($results -eq 0){
$ExitCode = 0
}
else {
$ExitCode = 1
}
}
else{
# In this case I know the command is failing
$ExitCode = 1
}
}
$job |Remove-Job -Force
Write-Output $ExitCode

Related

Function returning value after being initiated from Start-Job -ScriptBlock

Imagine you created function that returns Boolean value (e.g. Set-SomeConfiguration). Then, you call that function with
Start-Job -Scriptblock { Set-SomeConfiguration -ComputerName $computer }
Is there any way of retrieving the Boolean value generated by Set-SomeConfiguration?
Yes, using the Receive-Job cmdlet:
$SomeJob = Start-Job { Set-SomeConfiguration -ComputerName $computer }
$Result = $SomeJob | Receive-Job -Wait
The -Wait parameter ensures that Receive-Job waits for the job to finish and return its results.
(Note: most Set-* cmdlets won't - and shouldn't - actually return anything. To achieve what you describe, you could return the value of the automatic variable $?: {Set-SomeConfiguration;$?}, or inspect the State and Error properties of the job before receiving)
If you want more granular control over how long you want to wait, use Wait-Job.
In this example, we wait for 10 seconds (or until the job has finished):
# Job that takes a variable amount of time
$Job = Start-Job -ScriptBlock {
Start-Sleep -Seconds $(5..15 | Get-Random)
return "I woke up!"
}
# Wait for 10 seconds
if(Wait-Job $Job -Timeout 10){
# Job returned before timeout, let's grab results
$Results = $Job | Receive-Job
} else {
# Job did not return in time
# You can log, do error handling, defer to a default value etc. in here
}
# Clean up
Get-Job | Remove-Job

What's the best way to pass values to a running/background script block?

I have script which start several running script blocks by start-job.
What's the best approach to pass some variables/values to the running background script block?
There are some options like service broker/queue, files, etc. Is there a lighter way?
For example,
$sb = {
$Value = $args[0] # initial value
while ($true)
{
# Get more values from caller
$Value = .....
}
}
start-job -ScriptBlock $sb -ArgumentList $initValue
# There are more values to send to the script after the script block is started.
while (moreVaulesAvailable)
{
# $sb.Value = .... newly generated values ?
}
Start-Job started another PowerShell process. Is there any built-in mechanism to pass values between PS processes?
You can use MSMQ to do this. There is a MSMQ module that comes with PowerShell V3. Here's an example of how to pass messages to a background task using MSMQ:
$sb = {
param($queueName)
$q = Get-MsmqQueue $queueName
while (1) {
$messages = #(try {Receive-MsmqQueue -InputObject $q -RetrieveBody} catch {})
foreach ($message in $messages)
{
"Job received message: $($message.Body)"
if ($message.Body -eq '!quit')
{
return
}
}
Start-Sleep -Milliseconds 1000
"Sleeping..."
}
}
$queueName = 'JobMessages'
$q = Get-MsmqQueue $queueName
if ($q)
{
"Clearing the queue $($q.QueueName)"
$q | Clear-MsmqQueue > $null
}
else
{
$q = New-MsmqQueue $queueName
"Created queue $($q.QueueName)"
}
$job = Start-Job -ScriptBlock $sb -ArgumentList $queueName -Name MsgProcessingJob
"Job started"
$msg = New-MsmqMessage "Message1 for job sent at: $(Get-Date)"
Send-MsmqQueue -Name $q.Path -MessageObject $msg > $null
Receive-Job $job
$msg = New-MsmqMessage "Message2 for job sent at: $(Get-Date)"
Send-MsmqQueue -Name $q.Path -MessageObject $msg > $null
$msg = New-MsmqMessage "!quit"
Send-MsmqQueue -Name $q.Path -MessageObject $msg > $null
Wait-Job $job -Timeout 30
Receive-Job $job
Get-Job $job.Name
Remove-Job $job
When I run this script I get the following output:
C:\PS> .\MsmqQueue.ps1
Clearing the queue private$\jobmessages
Job started
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
4 MsgProcessin... BackgroundJob Completed True localhost ...
Job received message: Message1 for job sent at: 12/15/2012 17:53:39
Sleeping...
Job received message: Message2 for job sent at: 12/15/2012 17:53:39
Sleeping...
Job received message: !quit
4 MsgProcessin... BackgroundJob Completed False localhost ...

How to know when a command isn't responding in powershell

I have a powershell script which launch a command line utility DacIESvcCli.exe
DacIESvcCli.exe sends me a response and when I receive it I take the status, which can be "Running" or "Completed"
My problem is that sometimes the call hangs and I never get a response. The following script can run 3 days without interruption.
How can I prevent this from happening?
$myCounter = 0
while($myCounter -lt 5){
Write "start of the while counter : " $myCounter
$exportResponse = C:\DAC\DacIESvcCli.exe -s "myserver.database.windows.net" -u "mylogin#myserver" -p "mypassword" -requestid 'e1e34eee-1aaa-4cc9-8c48-3a2239fe1bff' -status
$exportStatus = $exportResponse[10].split(" ")[1].toString()
Write $exportStatus
$myCounter++
}
Here is the output
start of the while counter :
0
Completed
start of the while counter :
1
Completed
start of the while counter :
2
Completed
start of the while counter :
3
_
... and it never ends.
Here is part of my script for Travis Heseman
param($username, $password, $serverName, $requestId)
$consecutiveFailedAttempt = 0
while( $exportStatus -ne "Completed" -and $exportStatus -ne "Failed" -and $exportStatus -ne "TooLong"){
if($exportStatus -ne "FirstRun"){
Start-Sleep -m 60000 # Wait 1 min
}
$currentNow = Get-Date
$job = Start-Job { param($s, $u, $p, $r) C:\DAC\DacIESvcCli.exe -s $s -User $u -p $p -requestid $r -status } -ArgumentList #($serverName, $username, $password, $requestId)
Wait-Job $job -Timeout 60 # if the command takes longer than 60 sec we timeout and retry
Stop-Job $job
$exportResponse = Receive-Job $job
Remove-Job $job
if($exportResponse[10]){
$exportStatus = $exportResponse[10].split(" ")[1].toString()
$consecutiveFailedAttempt = 0;
}else{
$currentNow = Get-Date
$whileMessage = "Time out we retry " + $currentNow
$whileMessage | Out-File $File -append
$exportStatus = "Unknown"
$consecutiveFailedAttempt++;
}
if($consecutiveFailedAttempt -gt 10){
$exportStatus = "TooLong"
}
}
# do wantever you want with the export status

Managing the running time of background jobs. Timing out if not completed after x seconds,

I would like to time my background jobs (started with start-job) and time them out after x seconds. I find it hard however to keep track of the running time on each separate job (I am running aprox 400 jobs).
I wish there was a way to time out the job and set it to failed if not completed in X seconds, but I find no timeout-parameter.
What would be a good way to track the individual run-time of the jobs?
I guess I could create a hashtable with start-time of each job and the job-id and check against the running state and do a manual timeout, but that sounds kinda "inventing the wheel".
Any ideas?
Edit
Thank you everyone for a fruitful discussion and great inspiration on this topic!
You can use a hash table of timers:
$jobtimer = #{}
foreach ($job in $jobs){
start-job -name $job -ScriptBlock {scriptblock commands}
$jobtimer[$job] = [System.Diagnostics.Stopwatch]::startnew()
}
The running time of each job will be in $jobtimer[$job].elapsed
Just walk through the list of running jobs and stop any that have run past your timeout spec e.g.:
$timeout = [timespan]::FromMinutes(1)
$now = Get-Date
Get-Job | Where {$_.State -eq 'Running' -and
(($now - $_.PSBeginTime) -gt $timeout)} | Stop-Job
BTW there are more properties to a job object than the default formatting shows e.g.:
3 > $job | fl *
State : Running
HasMoreData : True
StatusMessage :
Location : localhost
Command : Start-sleep -sec 30
JobStateInfo : Running
Finished : System.Threading.ManualResetEvent
InstanceId : de370ea8-763b-4f3b-ba0e-d45f402c8bc4
Id : 3
Name : Job3
ChildJobs : {Job4}
PSBeginTime : 3/18/2012 11:07:20 AM
PSEndTime :
PSJobType : BackgroundJob
Output : {}
Error : {}
Progress : {}
Verbose : {}
Debug : {}
Warning : {}
You can specify the timeout option of Wait-Job:
-Timeout
Determines the maximum wait time for each background job, in seconds.
The default, -1, waits until the job completes, no matter how long it
runs. The timing starts when you submit the Wait-Job command, not the
Start-Job command.
If this time is exceeded, the wait ends and the command prompt
returns, even if the job is still running. No error message is
displayed.
Here's some example code:
This part just makes some test jobs:
Remove-Job -Name *
$jobs = #()
1..10 | % {
$jobs += Start-Job -ScriptBlock {
Start-Sleep -Seconds (Get-Random -Minimum 5 -Maximum 20)
}
}
The variable $timedOutJobs contains jobs that timed out. You can then restart them or what have you.
$jobs | Wait-Job -Timeout 10
$timedOutJobs = Get-Job | ? {$_.State -eq 'Running'} | Stop-Job -PassThru
For completeness, this answer combines the maximum seconds per job and the maximum concurrent jobs running. As this is what most people are after.
The example below retrieves the printer configuration for each print server. There can be over 3000 printers, so we added throttling.
$i = 0
$maxConcurrentJobs = 40
$maxSecondsPerJob = 60
$jobTimer = #{ }
$StopLongRunningJobs = {
$jobTimer.GetEnumerator().where( {
($_.Value.IsRunning) -and
($_.Value.Elapsed.TotalSeconds -ge $maxSecondsPerJob)
}).Foreach( {
$_.Value.Stop()
Write-Verbose "Stop job '$($_.Name.Name)' that ran for '$($_.Value.Elapsed.TotalSeconds)' seconds"
Stop-Job $_.Name
})
}
Foreach ($Computer in #($GetPrinterJobResults.Where( { $_.Data }) )) {
foreach ($Printer in $Computer.Data) {
do {
& $StopLongRunningJobs
$running = #(Get-Job -State Running)
$Wait = $running.Count -ge $maxConcurrentJobs
if ($Wait) {
Write-Verbose 'Waiting for jobs to fininsh'
$null = $running | Wait-Job -Any -Timeout 5
}
} while ($Wait)
$i++
Write-Verbose "$I $($Computer.ComputerName) Get print config '$($Printer.Name)'"
$Job = $Printer | Get-PrintConfiguration -AsJob -EA Ignore
$jobtimer[$Job] = [System.Diagnostics.Stopwatch]::StartNew()
}
}
$JobResult = Get-Job | Wait-Job -Timeout $maxSecondsPerJob -EA Ignore
$JobResult = Get-Job | Stop-Job -EA Ignore # Add this line if you want to stop the Jobs that reached the max time to wait (TimeOut)
$JobResult = Get-Job | Receive-Job -EA Ignore
$JobResult.count

How to capture the exception raised in the scriptblock of start-job?

I have the following script,
$createZip = {
Param ([String]$source, [String]$zipfile)
Process {
echo "zip: $source`n --> $zipfile"
throw "test"
}
}
try {
Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
echo "**Don't reach here if error**"
LogThezippedFile
}
catch {
echo "Captured: "
$_ | fl * -force
}
Get-Job | Wait-Job
Get-Job | receive-job
Get-Job | Remove-Job
However, the exception raised in another powershell instance cannot be captured. What's the best way to capture the exception?
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
343 Job343 Running True localhost ...
**Don't reach here if error**
343 Job343 Failed True localhost ...
zip: abd
--> acd
Receive-Job : test
At line:18 char:22
+ Get-Job | receive-job <<<<
+ CategoryInfo : OperationStopped: (test:String) [Receive-Job], RuntimeException
+ FullyQualifiedErrorId : test
Using throw will change the job object's State property to "Failed". The key is to use the job object returned from Start-Job or Get-Job and check the State property. You can then access the exception message from the job object itself.
Per your request I updated the example to also include concurrency.
$createZip = {
Param ( [String] $source, [String] $zipfile )
if ($source -eq "b") {
throw "Failed to create $zipfile"
} else {
return "Successfully created $zipfile"
}
}
$jobs = #()
$sources = "a", "b", "c"
foreach ($source in $sources) {
$jobs += Start-Job -ScriptBlock $createZip -ArgumentList $source, "${source}.zip"
}
Wait-Job -Job $jobs | Out-Null
foreach ($job in $jobs) {
if ($job.State -eq 'Failed') {
Write-Host ($job.ChildJobs[0].JobStateInfo.Reason.Message) -ForegroundColor Red
} else {
Write-Host (Receive-Job $job) -ForegroundColor Green
}
}
This should be a comment really, but I don't have the reputation to leave comments.
My answer is that you should use Andy Arismendi's answer, but also output $job.ChildJobs[0].Error
As $job.ChildJobs[0].JobStateInfo.Reason.Message isn't always useful.
I was able to "rethrow" the exception in the main thread by using:
Receive-Job $job -ErrorAction Stop
I'll my use case as an example. It can easily be applied to the OP.
$code = {
$Searcher = New-Object -ComObject Microsoft.Update.Searcher
#Errors from Search are not terminating, but will be present in the output none the less.
$Results = $Searcher.Search('IsInstalled=0 and IsHidden=0')
$Results.Updates
};
$job = Start-Job -ScriptBlock $code;
$consume = Wait-Job $job -Timeout 600;
if ($job.state -eq 'Running') {
Stop-Job $job
throw 'Windows update searcher took more than 10 minutes. Aborting'
};
#Captures and throws any exception in the job output
Receive-Job $job -ErrorAction Stop;
Write-Host "Finished with no errors"; #this will not print if there was an error
Works in v2.0.
Note that if the error within the job is non-terminating, the subsequent lines will continue to execute. But, this will not be obvious in the output returned from Receive-Job, as Receive-Job "terminates half way thorugh" - it throws out of it's self when the error object is encountered.
One way to avoid that is to wrap the whole block in a try {} catch{throw;}
Also, Job state will not be 'Failed' if the exception is non-terminating
TLDR:
# Works with both terminating and non terminating errors
$j = start-job {1/0} | wait-job; try { receive-job $j -ErrorAction Stop } catch { "err $_" }