Wednesday, November 21, 2007

PowerShell V2.0 Background Processes: Tiny Overview

Just Now?

For many years I have hoped for some construct in windows that works as well as in *nix to put long running processes in the background. There are ways to hack it in Windows, but all of them seem to have issues. Essentially, if you are running a long-running application from a console with Windows, no matter how you slice it, you are going to have a console window somewhere. Or you could make your operation a proper first-class Windows service, but that's hardly helpful for a long-running WSH script. Happily, we now have backgrounding in the PowerShell V2.0 along with lots of other new features. What follows is a slice of how background jobs known as PSJobs in PowerShell behave.

Typical of PowerShell, the implementers didn't just copy what was done in *nix, they took the opportunity to make things better, or at least more complicated. With *nix (Trademarknix?), capturing output from background jobs was sort of messy with that 2>&1 approach to redirect stdout and stderr, and then you still have to redirect that output somewhere. Even when you manage to track all the output of a process, once jobs are put in background there isn't much else you can do except kill them.

Consistent with the PowerShell Way, creating a background process actually returns a handle to an object with methods and properties. This handle also goes into a table that can be directly inspected and manipulated or as part of a pipeline. This is a distinct improvement over the traditional *nix-style where the job is placed into an opaque system process table which is more complicated to access. Of course in *nix you can get the process ID of a running process by parsing the output of the 'ps' command, but that metadata on the process doesn't get you much. PowerShell does it better. Read on for a simple example.

There is a family of PSJob cmdlets, shown here for fun:

  • Start-PSJob
  • Get-PSJob
  • Receive-PSJob
  • Stop-PSJob
  • Wait-PSJob
  • Remove-PSJob

This post will focus on Start-, Get-, and Receive-PSJob.


Basic Tour


What happens when you use this feature? We have to have some cmdlet to run in the background for demonstration purposes, and I picked get-service as an example even though it doesn't really run very long. It's a useful example because it returns somewhat familiar output.


But First

The PSJob infrastructure depends on the new PowerShell remoting feature, and, for that, you need WinRM installed and running on your system. You probably already have WinRM installed, but it may not be running so you may have to start it up by visiting the services controller (services.msc) or use start-service in PowerShell. If you don't even have WinRM, you can download it here: http://www.microsoft.com/downloads/details.aspx?FamilyId=845289CA-16CC-4C73-8934-DD46B5ED1D33&displaylang=en . And of course you need to have uninstalled any PowerShell 1x and installed the PowerShell 2.0 CTP bits. Note that PowerShell doesn't show up in XP on the add/remove programs list unless you have "show updates" checked.


If the remote management service is running, you likely will also need to run a WinRM setup script delivered as part of the PowerShell 2.0 CTP. This is in $pshome and you can run it from a PowerShell prompt this way:
PS C:\> & $pshome\Configure-wsman.ps1

(mysterious output)


Onward

Do this in a PowerShell prompt:

PS C:\> start-psjob -command "get-service"

You should wind up with something like this, your numbers will of course be different, and your font, and the spacing, and your time of day, but do I really need to tell you that?


PS C:\> start-psjob -command "get-service"

SessionId Name State HasMoreData Command
--------- ---- ----- ----------- -------
26 Running True get-service


So what happened to the output of the get-service command? It gets pulled into the PSJob identified by Session Id in the table for later retrieval. Note that the State is reported as running. The Start-PSJob returns to you the handle to the job you just launched. This handle is also stored in the PSJob internal table, which is accessible in the session using Get-PSJob.




PS C:\> get-psjob
SessionId Name State HasMoreData Command
--------- ---- ----- ----------- -------
1 Failed False get-service
3 Failed False get-process
5 Failed False get-process



...many other failed attempts because I had neglected to actually turn on the WinRM service silly me...

24 Completed False get-service
26 Completed True get-service



You can get the handle to the job either by grabbing it at launch time, like this:
PS C:\> $svc_job = start-psjob -command "get-service"
PS C:\> $svc_job.SessionId
30

Or by using the -SessionID argument to the get-psjob cmdlet:
PS C:\> $svc_job2 = Get-Psjob -SessionId
30
PS C:\> $svc_job2
SessionId Name State HasMoreData Command
--------- ---- ----- ----------- -------
30 Completed True get-service

However you arrive at the handle, you have what you need to check the running state, the command issued, and a HasMoreData flag which is useful to see if the handle's output buffer has been drained. All this is good to know, but where did the output go?

To locate the output for this run, use the Receive-PSJob cmdlet. This cmdlet parses the handle to the job and returns the output and error result of the execution. In the example below, it's just returned to the console via default ToString behaviors, but you would normally capture the Receive-PSJob output and do Important Work with it. Be aware that this is, by default, a "destructive" read in that it drains the buffers that it reads. (There is a -keep parameter for Receive-PSJob you can use if you want to read the buffers multiple times. For that matter, you can inspect and read from the member directly like this: $svc_job.ChildJobs[0].Output).

It's just a little disconcerting at first glance:

PS C:\> $svc_job receive-psjob

...
Running winmgmt Windows Management Instrumentation
Running WinRM Windows Remote Management (WS-Manag...
Stopped WLSetupSvc Windows Live Setup Service
Stopped WmdmPmSN Portable Media Serial Number Service
Stopped Wmi Windows Management Instrumentation...
Stopped WmiApSrv WMI Performance Adapter
Stopped WMPNetworkSvc Windows Media Player Network Sharin...
Stopped wscsvc Security Center
Running wuauserv Automatic Updates
Running WudfSvc Windows Driver Foundation - User-mo...
Running WZCSVC Wireless Zero Configuration
Stopped xmlprov Network Provisioning Service

And then running the same Receive-PSJob on the same handle:
PS C:\> $svc_job receive-psjob
PS C:\>

No output the second time we call it, because the first invocation already drained that output buffer. Note also that if you inspect the $svc_job handle after the Receive-PSJob call, $svc_job.HasMoreData is False.

As I mentioned earlier, unless you are a one-liner alpha-geek in PowerShell, you'll probably use (like me) constructs like this inside of short scripts:

$svc_listing = Receive-PSJob $svc_job

And then process the resulting array to locate something by service name to checks its state.
Of course, the practical application of using Receive-PSJob will depend on the output of the operation being backgrounded. The main goal here is to show the basic recipe to background a process, and what to do with the result. I haven't looked closely (yet) at how the results from backgrounded cmdlets are serialized, except to see that they are serialized, and that it warrants more investigation. In the example of get-services, it appears that the serialized version of a service handle is available as opposed to a live reference to a service-- you wouldn't (for example) be able to invoke a Start() on a service after retrieving it with Receive-PSJob.

That's one quick tour of one small part of the process backgrounding in PowerShell v2.0. I am very happy to see this feature, and it looks like they did a nice job with it. I am also very happy about the remoting feature, which I'll look at another time.