Home > Sample chapters > Programming > Visual Studio and .NET

Using PowerShell Remoting and Jobs

Using Windows PowerShell jobs

Windows PowerShell jobs permit you to run one or more commands in the background. Once you start the Windows PowerShell job, the Windows PowerShell console returns immediately for further use. This permits you to accomplish multiple tasks at the same time. You can begin a new Windows PowerShell job by using the Start-Job cmdlet. The command to run as a job is placed in a script block, and the jobs are sequentially named Job1, Job2, and so on. This is shown here:

PS C:\> Start-Job -ScriptBlock {get-process}

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
10     Job10           BackgroundJob   Running       True            localhost


PS C:\>

The jobs receive job IDs that are also sequentially numbered. The first job created in a Windows PowerShell console always has a job ID of 1. You can use either the job ID or the job name to obtain information about the job. This is shown here:

PS C:\> Get-Job -Name job10

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
10     Job10           BackgroundJob   Completed     True            localhost

PS C:\> Get-Job -Id 10

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
10     Job10           BackgroundJob   Completed     True            localhost


PS C:\>

Once you see that the job has completed, you can receive the job. The Receive-Job cmdlet returns the same information that returns if a job is not used. The Job1 output is shown here (truncated to save space):

PS C:\> Receive-Job -Name job10

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     62       9     1672       6032    80     0.00   1408 apdproxy
    132       9     2316       5632    62            1364 atieclxx
    122       7     1716       4232    32             948 atiesrxx
    114       9    14664      15372    48            1492 audiodg
    556      62    53928       5368   616     3.17   3408 CCC
     58       8     2960       7068    70     0.19    928 conhost
     32       5     1468       3468    52     0.00   5068 conhost
    784      14     3284       5092    56             416 csrss
    529      27     2928      17260   145             496 csrss
    182      13     8184      11152    96     0.50   2956 DCPSysMgr
    135      11     2880       7552    56            2056 DCPSysMgrSvc
 ... (truncated output)

Once a job has been received, that is it—the data is gone, unless you saved it to a variable or you call the Receive-Job cmdlet with the -keep switched parameter. The following code attempts to retrieve the information stored from job10, but as appears here, no data returns:

PS C:\> Receive-Job -Name job10
PS C:\>

What can be confusing about this is that the job still exists, and the Get-Job cmdlet continues to retrieve information about the job. This is shown here:

PS C:\> Get-Job -Id 10

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
10     Job10           BackgroundJob   Completed     False           localhost

As a best practice, use the Remove-Job cmdlet to delete remnants of completed jobs when you are finished using the job object. This will avoid confusion regarding active jobs, completed jobs, and jobs waiting to be processed. Once a job has been removed, the Get-Job cmdlet returns an error if you attempt to retrieve information about the job—because it no longer exists. This is illustrated here:

PS C:\> Remove-Job -Name job10
PS C:\> Get-Job -Id 10
Get-Job : The command cannot find a job with the job ID 10. Verify the value of the
Id parameter and then try the command again.
At line:1 char:1
+ Get-Job -Id 10
+ ~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (10:Int32) [Get-Job], PSArgumentException
    + FullyQualifiedErrorId : JobWithSpecifiedSessionNotFound,Microsoft.PowerShell.
   Commands.GetJobCommand

When working with the job cmdlets, I like to give the jobs their own name. A job that returns process objects via the Get-Process cmdlet might be called getProc. A contextual naming scheme works better than trying to keep track of names such as Job1 and Job2. Do not worry about making your job names too long, because you can use wildcard characters to simplify the typing requirement. When you receive a job, make sure you store the returned objects in a variable. This is shown here:

PS C:\> Start-Job -Name getProc -ScriptBlock {get-process}

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
12     getProc         BackgroundJob   Running       True            localhost


PS C:\> Get-Job -Name get*

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
12     getProc         BackgroundJob   Completed     True            localhost


PS C:\> $procObj = Receive-Job -Name get*
PS C:\>

Once you have the returned objects in a variable, you can use the objects with other Windows PowerShell cmdlets. One thing to keep in mind is that the object is deserialized. This is shown here, where I use gm as an alias for the Get-Member cmdlet:

PS C:\> $procObj | gm


   TypeName: Deserialized.System.Diagnostics.Process

This means that not all the standard members from the System.Diagnostics.Process .NET Framework object are available. The default methods are shown here (gps is an alias for the Get-Process cmdlet, gm is an alias for Get-Member, and -m is enough of the -membertype parameter to distinguish it on the Windows PowerShell console line):

PS C:\> gps | gm -m method


   TypeName: System.Diagnostics.Process

Name                      MemberType Definition
----                      ---------- ----------
BeginErrorReadLine        Method     System.Void BeginErrorReadLine()
BeginOutputReadLine       Method     System.Void BeginOutputReadLine()
CancelErrorRead           Method     System.Void CancelErrorRead()
CancelOutputRead          Method     System.Void CancelOutputRead()
Close                     Method     System.Void Close()
CloseMainWindow           Method     bool CloseMainWindow()
CreateObjRef              Method     System.Runtime.Remoting.ObjRef CreateObjRef(type
                                     requestedType)
Dispose                   Method     System.Void Dispose()
Equals                    Method     bool Equals(System.Object obj)
GetHashCode               Method     int GetHashCode()
GetLifetimeService        Method     System.Object GetLifetimeService()
GetType                   Method     type GetType()
InitializeLifetimeService Method     System.Object InitializeLifetimeService()
Kill                      Method     System.Void Kill()
Refresh                   Method     System.Void Refresh()
Start                     Method     bool Start()
ToString                  Method     string ToString()
WaitForExit               Method     bool WaitForExit(int milliseconds), System.Void
                                     WaitForExit()
WaitForInputIdle          Method     bool WaitForInputIdle(int milliseconds), bool
                                     WaitForInputIdle()

Methods from the deserialized object are shown here, where I use the same command I used previously:

PS C:\> $procObj | gm -m method


   TypeName: Deserialized.System.Diagnostics.Process

Name     MemberType Definition
----     ---------- ----------
ToString Method     string ToString(), string ToString(string format, System.IFormatProvider formatProvider)


PS C:\>

A listing of the cmdlets that use the noun job is shown here:

PS C:\> Get-Command -Noun job | select name

Name
----
Get-Job
Receive-Job
Remove-Job
Resume-Job
Start-Job
Stop-Job
Suspend-Job
Wait-Job

When starting a Windows PowerShell job via the Start-Job cmdlet, you can specify a name to hold the returned job object. You can also assign the returned job object in a variable by using a straightforward value assignment. If you do both, you end up with two copies of the returned job object. This is shown here:

PS C:\> $rtn = Start-Job -Name net -ScriptBlock {Get-Net6to4Configuration}
PS C:\> Get-Job -Name net

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
18     net             BackgroundJob   Completed     True            localhost

PS C:\> $rtn

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
18     net             BackgroundJob   Completed     True            localhost

Retrieving the job via the Receive-Job cmdlet consumes the data. You cannot come back and retrieve the returned data again. This code shown here illustrates this concept:

PS C:\> Receive-Job $rtn


RunspaceId         : e8ed4ab6-eb88-478c-b2de-5991b5636ef1
Caption            :
Description        : 6to4 Configuration
ElementName        :
InstanceID         : ActiveStore
AutoSharing        : 0
PolicyStore        : ActiveStore
RelayName          : 6to4.ipv6.microsoft.com.
RelayState         : 0
ResolutionInterval : 1440
State              : 0



PS C:\> Receive-Job $rtn
PS C:\>

The next example illustrates examining the command and cleaning up the job. When you use Receive-Job, an error message is displayed. To find additional information about the code that triggered the error, use the job object stored in the $rtn variable or the Get-Net6to4Configuration job. You may prefer using the job object stored in the $rtn variable, as shown here:

PS C:\> $rtn.Command
Get-Net6to4Configuration

To clean up first, remove the leftover job objects by getting the jobs and removing the jobs. This is shown here:

PS C:\> Get-Job | Remove-Job
PS C:\> Get-Job
PS C:\>

When you create a new Windows PowerShell job, it runs in the background. There is no indication as the job runs whether it ends in an error or it’s successful. Indeed, you do not have any way to tell when the job even completes, other than to use the Get-Job cmdlet several times to see when the job state changes from running to completed. For many jobs, this may be perfectly acceptable. In fact, it may even be preferable, if you wish to regain control of the Windows PowerShell console as soon as the job begins executing. On other occasions, you may wish to be notified when the Windows PowerShell job completes. To accomplish this, you can use the Wait-Job cmdlet. You need to give the Wait-Job cmdlet either a job name or a job ID. Once you have done this, the Windows PowerShell console will pause until the job completes. The job, with its completed status, displays on the console. You can then use the Receive-Job cmdlet to receive the deserialized objects and store them in a variable (cn is a parameter alias for the -computername parameter used in the Get-WmiObject command). The command appearing here starts a job to receive software products installed on a remote server named hyperv1. It impersonates the currently logged-on user and stores the returned object in a variable named $rtn.

PS C:\> $rtn = Start-Job -ScriptBlock {gwmi win32_product -cn hyperv1}
PS C:\> $rtn

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
22     Job22           BackgroundJob   Running       True            localhost

PS C:\> Wait-Job -id 22

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
22     Job22           BackgroundJob   Completed     True            localhost

PS C:\> $prod = Receive-Job -id 22
PS C:\> $prod.Count
2

In a newly open Windows PowerShell console, the Start-Job cmdlet is used to start a new job. The returned job object is stored in the $rtn variable. You can pipeline the job object contained in the $rtn variable to the Stop-Job cmdlet to stop the execution of the job. If you try to use the job object in the $rtn variable directly to get job information, an error will be generated. This is shown here:

PS C:\> $rtn = Start-Job -ScriptBlock {gwmi win32_product -cn hyperv1}
PS C:\> $rtn | Stop-Job
PS C:\> Get-Job $rtn
Get-Job : The command cannot find the job because the job name
System.Management.Automation.PSRemotingJob was not found. Verify the value of the
Name parameter, and then try the command again.
At line:1 char:1
+ Get-Job $rtn
+ ~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (System.Manageme...n.PSRemotingJob:
   String) [Get-Job], PSArgumentException
    + FullyQualifiedErrorId : JobWithSpecifiedNameNotFound,Microsoft.PowerShell.
   Commands.GetJobCommand

You can pipeline the job object to the Get-Job cmdlet and see that the job is in a stopped state. Use the Receive-Job cmdlet to receive the job information and the count property to see how many software products are included in the variable, as shown here:

PS C:\> $rtn | Get-Job

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
2      Job2            BackgroundJob   Stopped       False           localhost


PS C:\> $products = Receive-Job -Id 2
PS C:\> $products.count
0

In the preceding list you can see that no software packages were enumerated. This is because the Get-WmiObject command to retrieve information from the Win32_Product class did not have time to finish.

If you want to keep the data from your job so that you can use it again later, and you do not want to bother storing it in an intermediate variable, use the -keep parameter. In the command that follows, the Get-NetAdapter cmdlet is used to return network adapter information.

PS C:\> Start-Job -ScriptBlock {Get-NetAdapter}

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
4      Job4            BackgroundJob   Running       True            localhost

When checking on the status of a background job, and you are monitoring a job you just created, use the -newest parameter instead of typing a job number, as it is easier to remember. This technique appears here:

PS C:\> Get-Job -Newest 1

Id     Name            PSJobTypeName   State         HasMoreData     Location
--     ----            -------------   -----         -----------     --------
4      Job4            BackgroundJob   Completed     True            localhost

Now, to retrieve the information from the job and to keep the information available, use the -keep switched parameter as illustrated here:

PS C:\> Receive-Job -Id 4 -Keep


ifAlias                                          : Ethernet
InterfaceAlias                                   : Ethernet
ifIndex                                          : 12
ifDesc                                           : Microsoft Hyper-V Network Adapter
ifName                                           : Ethernet_7
DriverVersion                                    : 6.2.8504.0
LinkLayerAddress                                 : 00-15-5D-00-2D-07
MacAddress                                       : 00-15-5D-00-2D-07
LinkSpeed                                        : 10 Gbps
MediaType                                        : 802.3
PhysicalMediaType                                : Unspecified
AdminStatus                                      : Up
MediaConnectionState                             : Connected
DriverInformation                                : Driver Date 2006-06-21 Version
                                                   6.2.8504.0 NDIS 6.30
DriverFileName                                   : netvsc63.sys
NdisVersion                                      : 6.30
ifOperStatus                                     : Up
RunspaceId                                       : 9ce8f8e6-1a09-4103-a508-c60398527
<output truncated>

You can continue to work directly with the output in a normal Windows PowerShell fashion, like so:

PS C:\> Receive-Job -Id 4 -Keep | select name

name
----
Ethernet


PS C:\> Receive-Job -Id 4 -Keep | select transmitlinksp*

                                                                   TransmitLinkSpeed
                                                                   -----------------
                                                                         10000000000