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 $_" }
Related
I have a function which requires the user to enter Y or N to delete a file and I'm trying to run this whole function as a start-job function. My code is:
$JobFunction2 = {
function init {
try {
$output = terraform init
$path = Get-Item -Path ".\"
$in = $output | Select-String "Terraform has been successfully initialized!"
if ($in) {
Write-Host -ForegroundColor GREEN 'Initialization successfull'
}
else {
Write-Host -ForegroundColor YELLOW 'Intiialization failed Please enter Y to delete the .terraform folder'
$fail = del $path/.terraform -Force
$output = terraform init
return $fail, $output
}
}
catch {
Write-Warning "Error Occurred while Initializing the folder and the path is: $path"
}
}
}
Start-Job -InitializationScript $JobFunction2 -ScriptBlock { init } | Wait-Job | Receive-Job
When I'm running the start-job it shows "Wait-Job cmdlet cannot finish working, because one or more jobs are blocked waiting for user interaction". But if I'm calling only the function name 'init' without starting a job it works perfectly. Is there any way I could prompt the user to input Y or No so that the function could work in start-job?
Not sure what is setting your job's State to Blocked, but if the question is: Is there any way I could prompt the user to input Y or N so that the function could work in Start-Job?
The answer is yes, below you can see an example of how it can be implemented, note that I'm using a while loop instead of Wait-Job.
According to the documentation, the cmdlet has a -Sate parameter that takes a <JobState> argument but I couldn't get it to work. Even tried:
Wait-Job -Any -Force -State Blocked
And got the same error as you, so here is the brute force solution you can use:
function init {
'Initializing Function {0}' -f $MyInvocation.MyCommand.Name
do
{
$choice = Read-Host 'Waiting for user input. Continue? [Y/N]'
if($choice -notmatch '^y|n$')
{
'Invalid input.'
}
}
until ($choice -match '^y|n$')
if($choice -eq 'y')
{
'Continue script...'
}
else
{
'Abort script...'
}
}
$funcDef = [scriptblock]::Create("function init { $function:init }")
$job = Start-Job -InitializationScript $funcDef -ScriptBlock { init }
$desiredState = 'Stopped', 'Completed', 'Failed'
while($job.State -notin $desiredState)
{
if($job.State -eq 'Blocked')
{
Receive-Job $job
}
Start-Sleep -Milliseconds 500
}
$job | Receive-Job -Wait -AutoRemoveJob
$fail = Remove-Item $path/.terraform -Force -Recurse -Confirm:$false
this helped me to solve my issue
Full disclosure: My problem may be based on a incomplete understanding of the Citrix PowerShell module for Xen Desktop.
I have the following script block. It's called in a loop, once for each VM in a list. I'm using PowerShell Jobs because I want to keep the UI thread free to update the UI while the jobs run.
Code "A"
$j = Start-Job -Name $jobName -ScriptBlock {
param($url, $uuid, $cred, $snapshotName)
$ErrorActionPreference = "Stop"
try
{
$error.clear()
$xSS = $cred | Connect-XenServer -url $url -NoWarnCertificates -SetDefaultSession -PassThru;
$vm = (Get-XenVM -SessionOpaqueRef $xss.opaque_ref -uuid $uuid)
#Create snapshot
Invoke-XenVM -Async -SessionOpaqueRef $xss.opaque_ref -VM $vm -XenAction Snapshot -NewName $snapshotName
return "OK"
}
catch
{
return ("ERROR: "+$error)
}
} -ArgumentList $global:configFileVmMetaData.poolUrl, $xenVm.key, $global:cred, $snapshotName
Code "A" works ok, but it takes longer then necessary because I'm executing the Connect-XenServer cmdlet each time I invoke the script block.
So, I tried calling Connect-XenServer once outside the loop and passing in the session variable as shown below in Code "B". The result was the error Could not find open sessions to any XenServers being thrown inside the script block. I'm assuming that the $xss session variable is getting whanged somehow when its passed into the script block.
Any ideas why $xss session variable is getting whanged?
Code "B"
$xSS = $cred | Connect-XenServer -url $global:configFileVmMetaData.poolUrl -NoWarnCertificates -SetDefaultSession -PassThru;
loop
{
$j = Start-Job -Name $jobName -ScriptBlock {
param($xss, $uuid, $snapshotName)
$ErrorActionPreference = "Stop"
try
{
$error.clear()
$vm = (Get-XenVM -SessionOpaqueRef $xss.opaque_ref -uuid $uuid)
#Create snapshot
Invoke-XenVM -Async -SessionOpaqueRef $xss.opaque_ref -VM $vm -XenAction Snapshot -NewName $snapshotName
return "OK"
}
catch
{
return ("ERROR: "+$error)
}
} -ArgumentList $xss, $xenVm.key, $snapshotName
}
Additional examples inspired by the Robert Cotterman answer
In all cases, I still get the Could not find open sessions to any
XenServers error
FYI - Using PowerShell 5.1
Example using $using. Variable contents are passed in and back out as expected
cls
$aLocal = "AAA"
$bLocal = "BBB"
$j = Start-Job -Name "TestJob" -ScriptBlock {
return ($using:aLocal + " *** " + $using:bLocal)
}
while ($true)
{
$g = get-job -name "TestJob"
write-host ("get-job " + $g.Name + " is " + $g.State)
if ($g.State -ne "Running")
{
break
}
start-sleep -Seconds 1
}
write-host ("receive-Job='" + (receive-Job -Name "TestJob") +"'")
$g = get-Job -Name "TestJob"
Write-Host ("get-Job "+$g.name + " " + $g.state + " " + $g.HasMoreData + " " + $g.id)
if($g)
{
Remove-Job -Name "TestJob"
}
Output
get-job TestJob is Running
get-job TestJob is Completed
receive-Job='AAA *** BBB'
get-Job TestJob Completed False 45
Remove-Job
Example using args. Variable contents are passed in and back out as expected
cls
$aLocal = "AAA"
$bLocal = "BBB"
$j = Start-Job -Name "TestJob" -ScriptBlock {
return ($args[0] + " *** " + $args[1])
} -ArgumentList ($aLocal, $bLocal)
while ($true)
{
$g = get-job -name "TestJob"
write-host ("get-job " + $g.Name + " is " + $g.State)
if ($g.State -ne "Running")
{
break
}
start-sleep -Seconds 1
}
write-host ("receive-Job='" + (receive-Job -Name "TestJob") +"'")
$g = get-Job -Name "TestJob"
Write-Host ("get-Job "+$g.name + " " + $g.state + " " + $g.HasMoreData + " " + $g.id)
if($g)
{
Remove-Job -Name "TestJob"
}
Output
get-job TestJob is Running
get-job TestJob is Completed
receive-Job='AAA *** BBB'
get-Job TestJob Completed False 49
Example using named args. Variable contents are passed in and back out as expected
cls
$aLocal = "AAA"
$bLocal = "BBB"
$j = Start-Job -Name "TestJob" -ScriptBlock {
param($a, $b)
return ($a + " *** " + $b)
} -ArgumentList ($aLocal, $bLocal)
while ($true)
{
$g = get-job -name "TestJob"
write-host ("get-job " + $g.Name + " is " + $g.State)
if ($g.State -ne "Running")
{
break
}
start-sleep -Seconds 1
}
write-host ("receive-Job='" + (receive-Job -Name "TestJob") +"'")
$g = get-Job -Name "TestJob"
Write-Host ("get-Job "+$g.name + " " + $g.state + " " + $g.HasMoreData + " " + $g.id)
if($g)
{
Remove-Job -Name "TestJob"
}
Output
get-job TestJob is Running
get-job TestJob is Completed
receive-Job='AAA *** BBB'
get-Job TestJob Completed False 55
I just don't think it works that way. The connection to the Xen server is created for your PowerShell session, with info about that connection collected in the $xss variable. Each job runs its own PowerShell session. So just passing $xss to the job isn't the same, it's just a description of the connection details.
Jobs and invoke-command require you to specify that you're using your variable. Simply change the variable from
$variable
To
$using:variable
Internal variables do not need this. But calling upon variables from the parent script do.
Alternatively, Since you're passing $xss as an argument, you wouldn't call it with $xss but rather
$args[0]
Since it's your first argument. And $args[1] for the second etc...
The reason is because the entire xss variable is being printed as an argument and is not named inside the job. It's given the name $args and has a place in the first slot (0).
I prefer $using:variable as it reduces confusion
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 ...
I'm trying to get a simple working example of using functions inside of jobs. I've managed to pass my function into the scriptblock used for my job, but I can't seem to get parameters to the function.
# concurrency
$Logx =
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
# Execution starts here
cls
$colors = #("red","blue","green")
$colors | %{
$scriptBlock =
{
Invoke-Expression -Command $args[1]
Start-Sleep 3
}
Write-Host "Processing: " $_
Start-Job -scriptblock $scriptBlock -args $_, $Logx
}
Get-Job
while(Get-Job -State "Running")
{
write-host "Running..."
Start-Sleep 2
}
# Output
Get-Job | Receive-Job
# Cleanup jobs
Remove-Job *
Here's the output:
Processing: red
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
175 Job175 Running True localhost ...
Processing: blue
177 Job177 Running True localhost ...
Processing: green
179 Job179 Running True localhost ...
179 Job179 Running True localhost ...
177 Job177 Running True localhost ...
175 Job175 Running True localhost ...
Running...
Running...
OUT:
OUT:
OUT:
So as evidenced by the OUT: x3 in the output my function is getting called, but I haven't found any syntax that allows me to get the parameter to the function. Thoughts?
EDIT:
Note in Shawn's observation below and my response I tried using functions as variables because using a traditional function does not seem to work. If there is a way to get that working I'd be more than happy to not have to pass my functions around as variables.
The answer is to use the initializationscript parameter of Start-Job. If you define all your functions in a block and pass the block they become available.
Solution was found in this post:
How do I Start a job of a function i just defined?
Here is my example from before, now working:
# concurrency
$func = {
function Logx
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
}
# Execution starts here
cls
$colors = #("red","blue","green")
$colors | %{
$scriptBlock =
{
Logx $args[0]
Start-Sleep 9
}
Write-Host "Processing: " $_
Start-Job -InitializationScript $func -scriptblock $scriptBlock -args $_
}
Get-Job
while(Get-Job -State "Running")
{
write-host "Running..."
Start-Sleep 2
}
# Output
Get-Job | Receive-Job
# Cleanup jobs
Remove-Job *
If you do not prefix your function name with keyword function, PowerShell does not know to treat it as such. As you have written your script it is basically a variable with some special text in it. Which as your output shows it is only executing the commands it recognizes within that variable's content: Write-Host "OUT:".
Using the correct syntax will tell PowerShell it is a function and that you have variables to pass into it that you need executed:
function Logx
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
Then when you call it within your script you will just use Logx
Got this far. Have to run out, will try back later.
PS: What is getting passed at args[1], I am getting a lot of red,
CategoryInfo : InvalidData: (:) [Invoke-Expression], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.InvokeExpressionCommand
here is what I've managed so far.
# concurrency
$Logx =
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
# Execution starts here
cls
$colors = #("red","blue","green")
$colors | %{
& $scriptBlock =
{ Invoke-Expression -Command $args[1]
Start-Sleep 3
}
Write-Host "Processing: " $_
Start-Job -scriptblock $scriptBlock -ArgumentList #($_, $Logx)
}
# Get-Job
while(Get-Job -State "Running")
{
write-host "Running..."
Start-Sleep 2
}
# Output
Get-Job | Receive-Job
# Cleanup jobs
Remove-Job *
I have the following code.
function createZip
{
Param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
try {
Start-Job -ScriptBlock { createZip "abd" "acd" }
}
catch {
$_ | fl * -force
}
Get-Job | Wait-Job
Get-Job | receive-job
Get-Job | Remove-Job
However, the script returns the following error.
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
309 Job309 Running True localhost createZip "a...
309 Job309 Failed False localhost createZip "a...
Receive-Job : The term 'createZip' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:17 char:22
+ Get-Job | receive-job <<<<
+ CategoryInfo : ObjectNotFound: (function:createZip:String) [Receive-Job], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
It seems the function name cannot be recognized inside the script block of start-job. I tried function:createZip too.
Start-Job actually spins up another instance of PowerShell.exe which doesn't have your createZip function. You need to include it all in a script block:
$createZip = {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
An example returning an error message from the background job:
$createZip = {
param ([String] $source, [String] $zipfile)
$output = & zip.exe $source $zipfile 2>&1
if ($LASTEXITCODE -ne 0) {
throw $output
}
}
$job = Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
$job | Wait-Job | Receive-Job
Also note that by using a throw the job object State will be "Failed" so you can get only the jobs which failed: Get-Job -State Failed.
If you are still new to using start-job and receive-job, and want to debug your function more easily, try this form:
$createZip = {
function createzipFunc {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
#other funcs and constants here if wanted...
}
# some secret sauce, this defines the function(s) for us as locals
invoke-expression $createzip
#now test it out without any job plumbing to confuse you
createzipFunc "abd" "acd"
# once debugged, unfortunately this makes calling the function from the job
# slightly harder, but here goes...
Start-Job -initializationScript $createZip -scriptblock {param($a,$b) `
createzipFunc $a $b } -ArgumentList "abc","def"
All not made simpler by the fact I did not define my function as a simple filter as you have, but which I did because I wanted to pass a number of functions into my Job in the end.
Sorry for digging this thread out, but it solved my problem too and so elegantly at that. And so I just had to add this little bit of sauce which I had written while debugging my powershell job.