Getting Started - Jobs
Welcome to my Getting Started with Windows PowerShell series!
In case you missed the earlier posts, you can check them out here:
- Customizing your environment
- Command discovery
- Using the ISE and basic function creation
- A deeper dive into functions
- Loops
- Modules
- Help
- Accepting pipeline input
- Strings
We will be exploring:
PowerShell Background Jobs
Jobs in PowerShell allow you to run commands in the background. You use a command to start the job, and then various other commands to check in and receive information from it. Jobs can be started as 64-bit or 32-bit, regardless of what you launch your session as. You can also start jobs that run as a different user.
Why Use PowerShell Jobs?
PowerShell jobs are handy for a lot of different uses. One could be running a script that contains multiple jobs that will perform different Active Directory tasks. You can run those in parallel, and use a While loop to check in on the jobs as they execute.
In this post you'll see how to start jobs, see running jobs, and get information as jobs complete.
Creating Jobs
The command to start a job in PowerShell is Start-Job.
Let's use this command to start a job that will wait for one minute, and then execute the command Get-Process.
Start-Job -Name SleepProcess -ScriptBlock {Start-Sleep -Seconds 60; Get-Process}
The output of the job started is displayed after we execute the Start-Job command.
If you wanted to wait for this job to complete (which would make the console unusable until the job completes), you can use the Wait-Job command. With Wait-Job you'll need to specify the Job Name, Id, or simply pipe the Start-Job command to Wait-Job.
Start-Job -Name Wait -ScriptBlock {Start-Sleep -Seconds 60} | Wait-Job
And now we wait for the console to free up after 60 seconds have passed.
Monitoring Jobs
To keep an eye on the running jobs in PowerShell, we'll use the command Get-Job.
Get-Job
Get-Job will show us all running jobs by default. We can also narrow it down to just our job via the Job Name or Job Id. Let's try getting this Job via the Job Id.
Get-Job -Id 1
If there were more Jobs running, we'd still only see this Job as we specified that we only want to see the Job with the Id value of 1.
Collecting Job Information
There are few different ways to collect information from running jobs in PowerShell. The official command to do it is Receive-Job. You'll need to specify the Job Name or Id when you use this command. Let's receive the information from this job, and specify the -Keep parameter so we can receive the information again later.
Receive-Job -Id 1 -Keep
You can see it displayed the output of Get-Process. Let's store the job in an object, and look at another way to receive the information.
This is a bit interesting. PowerShell stores most of the job information in a child job for each job you run. To access this information, you must access the ChildJobs property for the first child job, or 0. Let's take a look.
$ourOutput = Start-Job -Name GetOutput -ScriptBlock {Get-Process}
Here we started a job, and stored the object in the $ourOutput variable.
Now we can use that variable to take a look at the results.
$ourOutput
Let's see the available methods and properties for $ourOutput.
$ourOutput | Get-Member
Hey, there's the ChildJobs property! Let's take a look at it's properties and methods as well.
$ourOutput.ChildJobs[0] | Get-Member
Both $ourOutput and $ourOutput.ChildJobs[0] have the Output property. However, since our job is actually spawned as child job, this property will be empty on the initial object, and have a value only on the child object.
$ourOutput.Output $ourOutput.ChildJobs[0].Output
Error Handling
Error handling in jobs can be tricky, as sometimes a job state will show as completed, but the output of the job is actually a failed command. There are other times where a job will state that is actually has failed. In each case, there are two main spots to check for error information when a job fails.
Let's take a look at the two examples. I can replicate different states with the New-Item command and toggling the -ErrorAction parameter to Stop.
Job State Failed
$failJob = Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory -ErrorAction Stop}
Let's look at the $failJob object to make sure it has indeed failed...
$failJob
Alright... now here is the weird part. Normally you'd look at the ChildJob's Error property to get the error message. That... or Receive-Job would show you an error. I have tried all of those approaches. It looks like Receive-Job will display the error, but you're unable to store it in a variable or parse the message in any way. Not helpful!
$jobError = Receive-Job $failJob $failJob.ChildJobs[0].Error $jobError
So how do we see what happened? We need to look at the property $failJob.ChildJobs[0].JobStateInfo.Reason.Message to get the message of what happened (as a string).
$failJob.ChildJobs[0].JobStateInfo.Reason.Message $jobError = $failJob.ChildJobs[0].JobStateInfo.Reason.Message $jobError
There we go, got the error!
Job State Completed
There are times when the job will complete, and the error message will indeed be in the ChildJob's Error property. Let's take the -ErrorAction Stop off the New-Item command and start the job again.
$failJob = Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory}
Now let's verify it shows as completed.
$failJob
This job has essentially the same error message as the other error example above. The difference is the error is stored in the ChildJob property of Error. This is one of the most confusing aspects of PowerShell jobs. Receive-Job will display the error, but also like above not let you store it. Not good for automation!
Here's how we get the error information this time.
$failJob.ChildJobs[0].Error $jobError = $failJob.ChildJobs[0].Error $jobError
Job Cleanup
Now that we've gone over how to create jobs, receive their information, and perform some error handling... let's move on to cleanup.
The Remove-Job command will remove jobs from PowerShell. You can see this from start to finish with this example.
$ourJob = Start-Job -Name CleanUp -ScriptBlock {Get-Process} $ourJob Get-Job
You can see the CleanUp job name amidst all of the other jobs I still have from this post.
To remove this job only, you can use Remove-Job with the Job Name or Id. We can also pipe the variable we stored our job object in ($ourJob) to Remove-Job.
$ourJob | Remove-Job Get-Job
And it's gone! What if we wanted to remove all of these jobs? You can actually pipe Get-Job to Remove-Job.
Get-Job | Remove-Job Get-Job
As you can see, all of the jobs are now cleaned up.
Multiple Job Example
The glory of PowerShell Jobs is being able to run multiple commands at the same time. This example code will start up multiple jobs, and then use a While loop to monitor them. As they complete, the output and/or error messages will be output to a text file with the job name and date stamp as the file name. The output folder is C:\PowerShell\part10\output.
Copy and paste the code in the ISE, and ensure you can write to C:\PowerShell\Part10. If you can't, you may see some nasty error messages, as I have not added any error handling to this example. Save the script as C:\PowerShell\Part10\part10.ps1.
Code
#Set the jobs variable to $true so the while loop processes at least once $jobs = $true #Set the output folder path $outputFolder = 'C:\PowerShell\part10\output' #Create some jobs to monitor. One for each error handling example, and then one success that takes a minute to complete. Start-Job -Name SleepProcess -ScriptBlock {Start-Sleep -Seconds 60; Get-Process} Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory -ErrorAction Stop} Start-Job -Name FailCompletedJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory} #If the folder doesn't exist, create it If (!(Test-Path $outputFolder)) { New-Item -Path $outputFolder -ItemType Directory } #While $jobs = $true... While ($jobs) { #Begin $jobs While Loop #Store the jobs in $ourJobs $ourJobs = Get-Job Write-Host "Checking for jobs..." #Use a ForEach loop to iterate through the jobs foreach ($jobObject in $ourJobs) { #Begin $ourJobs ForEach loop #Null out variables used in this loop cycle $jobResults = $null $errorMessage = $null $jobFile = $null $jobCommand = $null #Store the command used in the job to display later $jobCommand = $jobObject.Command #Use the Switch statement to take different actions based on the job's state value Switch ($jobObject.State) { #Begin Job State Switch #If the job state is running, display the job info {$_ -eq 'Running'} { Write-Host "Job: [$($jobObject.Name)] is still running..."`n Write-Host "Command: $jobCommand"`n } #If the job is completed, create the job file, say it's been completed, and then perform an error check #Then display different information if an error is found, versus successful completion #Use a here-string to create the file contents, then add the contents to the file #Finally use Remove-Job to remove the job {$_ -eq 'Completed'} { #Create file $jobFile = New-Item -Path $outputFolder -Name ("$($jobObject.Name)_{0:MMddyy_HHmm}.txt" -f (Get-Date)) -ItemType File Write-Host "Job [$($jobObject.Name)] has completed!" #Begin completed but with error checking... if ($jobObject.ChildJobs[0].Error) { #Store error message in $errorMessage $errorMessage = $jobObject.ChildJobs[0].Error | Out-String Write-Host "Job completed with an error!"`n Write-Host "$errorMessage"`n -ForegroundColor Red -BackgroundColor DarkBlue #Here-string that contains file contents $fileContents = @" Job Name: $($jobObject.Name) Job State: $($jobObject.State) Command: $jobCommand Error: $errorMessage "@ #Add the content to the file Add-Content -Path $jobFile -Value $fileContents } else { #Get job result and store in $jobResults $jobResults = Receive-Job $jobObject.Name Write-Host "Job completed without errors!"`n Write-Host ($jobResults | Out-String)`n #Here-string that contains file contents $fileContents = @" Job Name: $($jobObject.Name) Job State: $($jobObject.State) Command: $jobCommand Output: $($jobResults | Out-String) "@ #Add content to file Add-Content -Path $jobFile -Value $fileContents } #Remove the job Remove-Job $jobObject.Name } #If the job state is failed, state that it is failed and then create the file #Add the error message to the file contents via a here-string #Then use Remove-Job to remove the job {$_ -eq 'Failed'} { #Create the file $jobFile = New-Item -Path $outputFolder -Name ("$($jobObject.Name)_{0:MMddyy_HHmm}.txt" -f (Get-Date)) -ItemType File #Store the failure reason in $failReason $failReason = $jobObject.ChildJobs[0].JobStateInfo.Reason.Message Write-Host "Job: [$($jobObject.Name)] has failed!"`n Write-Host "$failReason"`n -ForegroundColor Red -BackgroundColor DarkBlue #Here-string that contains file contents $fileContents = @" Job Name: $($jobObject.Name) Job State: $($jobObject.State) Command: $jobCommand Error: $failReason "@ #Add content to file Add-Content -Path $jobFile -Value $fileContents #Remove the job Remove-Job $jobObject.Name } } #End Job State Switch } #End $ourJobs ForEach loop #Clear the $ourJobs variable $ourJobs = $null #Get the new list of jobs as it may have changed since we did some cleanup for failed/completed jobs $ourJobs = Get-Job #If jobs exists, keep the loop running by setting $jobs to $true, else set it to $false if ($ourJobs) {$jobs = $true} else {$jobs = $false} #Wait 10 seconds to check for jobs again Start-Sleep -Seconds 10 } #End $jobs While Loop
Here's the code in action...
So there's the output to the console. Let's check out the C:\PowerShell\Part10\Output folder.
That looks good. Let's take a peek at each file's contents.
And there you have it! You can do whatever you'd like with the job information in the Switch statement.
Homework
- Jobs are capable of being run in different ways, including remotely. There are also a handful of WMI command that can be started as jobs. To see more ways jobs can be used, check out these resources.
- Add some error handling to the script example in this post.
- Post ideas in the comments!
I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!
-Ginger Ninja