- Install Ansible on Windows - Thu, Jul 20 2023
- Use Azure Bastion as a jump host for RDP and SSH - Tue, Apr 18 2023
- Azure Virtual Desktop: Getting started - Fri, Apr 14 2023
Let’s say you sat down at your administrative workstation with a cup of coffee, and you planned to run a series of administration tasks by using PowerShell. The first few tasks complete quickly—no big deal. However, when you run a particularly long-running task (perhaps a remoting job that queries a dozen servers in parallel), your PowerShell session hangs and you find yourself waiting... and waiting... and waiting.
Sure, you can spawn another PowerShell console session, but then you lose all the variables and other goodies that you loaded into your original PowerShell runspace. What to do?
The short answer is to take advantage of the PowerShell background jobs architecture. From now on, we can send long-running tasks to the background and continue our current PowerShell session uninterrupted. Let’s see how to do this.
Starting a new background job
Fire up an elevated PowerShell console session and run the following command to retrieve a list of job-related cmdlets:
PS C:\> Get-Command -Noun Job | Select-Object -Property Name Name ---- Debug-Job Get-Job Receive-Job Remove-Job Resume-Job Start-Job Stop-Job Suspend-Job Wait-Job
To create a new background job, we use Start-Job (frankly, I originally looked for a New-Job command, but the PowerShell team must have thought that Start-Job was more action-oriented).
As usual, we’ll first run Update-Help to make sure we have the latest and greatest PowerShell documentation, and then we’ll examine the help for Start-Job, paying particular attention to the examples.
Get-Help -Name Start-Job -ShowWindow
Let’s say that we needed to scour our computer’s D: drive for PowerShell .ps1 script files. Depending on how clogged your file system is, a recursive search such as the following could take a while:
Get-ChildItem -Path D:\ -Filter *.ps1 -Recurse
Let’s instead define a new job named “ScriptSearch” that puts the work into the background of our console session:
PS C:\> Start-Job -Name "ScriptSearch" -ScriptBlock { Get-ChildItem -Path D:\ -Filter *.ps1 -Recurse } Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 2 ScriptSearch BackgroundJob Running True localhost
Let me break down the job details table for you:
- Id: Jobs receive a unique identification number. The PowerShell job engine often breaks a “parent” job into one or more “child” jobs. You see this a lot when you use remoting.
- Name: This is the optional “friendly” name that you provide with the -Name parameter.
- PSJobTypeName: Besides BackgroundJob, PowerShell also has RemoteJob, PSWorkflowJob, and PSScheduledJob job types.
- State: This tells you whether the job is running, finished, or in an error state.
- HasMoreData: If this is True, then the job results are still in memory.
- Location: This tells you on which computer(s) the job task is executing or has executed.
Retrieving job results
We use Receive-Job to check on job status. The big “gotcha” here is that, if you forget to include the -Keep parameter, PowerShell dumps the job results from memory! Watch this:
PS C:\> Receive-Job -Id 2 Directory: D:\ Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 3/16/2015 10:23 AM 687 InstallGoogleChrome.ps1
Now, I’ll try to receive the job results a second time:
PS C:\> Receive-Job -Id 2 PS C:\>
Ouch! Let’s run Get-Job to see if the job object still persists in memory:
PS C:\> Get-Job Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 2 ScriptSearch BackgroundJob Completed False localhost
Yeah, the ScriptSearch job shows as completed, and the HasMoreData value of False says that the job results data flew out the proverbial window. We can either close our PowerShell console session to dump the job object from memory, or we can use the PowerShell pipeline and the Remove-Job cmdlet:
Get-Job -Name "ScriptSearch" | Remove-Job
Adding scripts to the mix
The Start-Job cmdlet has a -FilePath parameter that accepts .ps1 script files. Let’s imagine we wrote a script called EventScrape.ps1 that pulled warning and error messages from the System event log from two computers. Here’s the code:
Invoke-Command -ComputerName dc1,adfs1,vpnclient1 -ScriptBlock {Get-EventLog -LogName System -EntryType Warning }
To run the script as a background job, we simply use the appropriate parameter:
PS C:\> Start-Job -Name "EventScrape" -FilePath "C:\EventScrape.ps1" Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 2 EventScrape BackgroundJob Running True localhost
My job ID 2 shows as “localhost” for its location because the script (job) itself is managed on the local computer, which, in my case, is a Windows Server 2012 R2 domain controller named dc1. Within the script code, of course, we’re touching three boxes simultaneously, thanks to the magic of PowerShell remoting.
When fetching results, we’ll be sure to include -Keep:
PS C:\> Receive-Job -Name "EventScrape" -Keep | Select-Object -First 1 | Format-List Index : 1458 EntryType : Warning InstanceId : 40961 Message : The Security System could not establish a secured connection with the server ldap/dc1/company.pri@COMPANY.PRI. No authentication protocol was available. Category : (0) CategoryNumber : 0 ReplacementStrings : {ldap/dc1/company.pri@COMPANY.PRI} Source : LsaSrv TimeGenerated : 3/25/2015 10:43:55 AM TimeWritten : 3/25/2015 10:43:55 AM UserName : NT AUTHORITY\SYSTEM PSComputerName : localhost
The PSComputerName property is particularly handy when you use PowerShell remoting because you’ll learn on which remote computer the message arose. Let’s verify that the job’s data still exists in our runspace:
PS C:\> Get-Job Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 2 EventScrape BackgroundJob Completed True localhost
Remember, the True value for the HasMoreData property means that we are free to receive the job results again, provided we include the -Keep parameter.
Understanding parent and child jobs
To demonstrate how PowerShell implements parent and child jobs, let’s start a new remoting job that again targets my three lab computers:
PS C:\> Start-Job -Name "wmi-os" -ScriptBlock { Get-WmiObject -Class Win32_operatingsystem -ComputerName dc1,adfs1,vpnclient1 } Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 4 wmi-os BackgroundJob Running True localhost
If you’ve been watching my code output closely, you’ll observe that my wmi-os job has job ID 4, while the previous job, EventScrape, has job ID 2. What’s the deal with job ID 3? Look here:
PS C:\> Get-Job -Name "EventScrape" -IncludeChildJob Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 2 EventScrape BackgroundJob Completed True localhost 3 Job3 Completed True localhost
PowerShell always defines a parent, or top-level, job, and then does the actual work in one or more child jobs. As you can see, the -IncludeChildJob parameter allows you to see the full family relationship, as it were, between parent and child.
We can also selectively retrieve job results data from a child job directly. Practically speaking, I always query the parent job when I’m fetching results; examining the child jobs is useful when you’re troubleshooting errors.
PS C:\> Receive-Job -id 4 -Keep SystemDirectory : C:\Windows\system32 Organization : BuildNumber : 9600 RegisteredUser : Windows User SerialNumber : 00252-80027-07122-AA034 Version : 6.3.9600 # additional output omitted
One bummer about the above example is that using -ComputerName with Get-WMIObject doesn’t return a PSComputerName value like “true” PowerShell remoting does. A better approach would be to run the Get-WMIObject call from the context of an Invoke-Command statement. Live and learn!
Using the -AsJob parameter
Some Windows PowerShell commands can be shoehorned into a PowerShell job by using the -AsJob parameter. Let’s investigate which commands can do this in PowerShell v5 preview:
PS C:\> Get-Command -ParameterName AsJob | Select-Object -Property Name Name ---- Get-WmiObject Invoke-Command Invoke-WmiMethod Remove-WmiObject Restart-Computer Set-WmiInstance Stop-Computer Test-Connection
Cool—Invoke-Command is on the list. Let’s capture a list of stopped services on our three computers as a job:
PS C:\> Invoke-Command -ComputerName dc1,adfs1,vpnclient1 -ScriptBlock { Get-Service | Where-Object { $_.Status -eq "Stopped" } } -AsJob Id Name PSJobTypeName State HasMoreData Location -- ---- ------------- ----- ----------- -------- 4 Job4 RemoteJob Running True dc1,adf...
The -AsJob parameter is great for when you decide in the middle of your PowerShell statement that, “Hey, this task would make for a great background job!”
For further study
I hope you found this article useful. Please don’t forget about PowerShell’s built-in conceptual help library. Run the following command to get the filenames; I’ve hyperlinked each one to its online version for your studying convenience:
Subscribe to 4sysops newsletter!
PS C:\> Get-Help -Name about_* | Select-Object -Property Name, Synopsis | Where-Object {$_.Name -Like "*job*" } | Format-Table -AutoSize
- about_Jobs
- about_Job_Details
- about_Remote_Jobs
- about_Scheduled_Jobs
- about_Scheduled_Jobs_Advanced
- about_Scheduled_Jobs_Basics
- about_Scheduled_Jobs_Troubleshooting