Introduction
My previous article showed how to run PowerShell scripts from C#. That implementation was limited in the sense that it would run scripts synchronously, blocking until the script finished what it was doing. That is fine for short-running scripts, but if you have long-running or even never-ending scripts, you will need asynchronous execution. This article shows just how to do that.
Basic Steps
Here are the basic steps to run a PowerShell script asynchronously:
- Create a
Pipeline
instance by calling Runspace.CreatePipeline()
- Pass the script to the
Pipeline
instance using pipeline.Commands.AddScript()
- Feed the
Pipeline
its input objects using pipeline.Input.Write()
- Close the input by calling
pipeline.Input.Close()
- Call
pipeline.InvokeAsync()
; this will cause the Pipeline
to create a worker thread that will execute the script in the background - Start reading
pipeline.Output
for the results of the script, until pipeline.Output.EndOfPipeline
becomes true
There are two ways in which you can be notified of the availability of new output data:
- First, there is a property
pipeline.Output.WaitHandle
of type System.Threading.WaitHandle
. This handle can be used with the various static Wait***()
methods of System.Threading.WaitHandle
to wait for new data to arrive. - Second,
pipeline.Output
has an event called DataReady
. By subscribing to this event, the PowerShell background thread will call you every time new data becomes available.
The Trouble with Output.WaitHandle
At first hand, Output.WaitHandle
seems like a nice option to choose for retrieving the script output data; it provides complete separation between the producer (the PowerShell thread) and the consumer (the output reading thread), unlike the DataReady
event which is called directly from the PowerShell thread. But there's a problem: if the consumer isn't fast enough in dealing with the output, the producer will happily continue producing at full speed until it consumes all your memory, or when the script ends. While it seems as though there are provisions in the output queue to limit the maximum amount of memory used (there is a MaxCapacity
readonly property), I haven't found a way to actually set this limit.
You may wonder why the consumer would be too slow to process the output. Well, if you're using PowerShell scripting to automate some aspects of your C# program, you'll probably want to display the script output in some way on your user interface. The output of a PowerShell script will easily outrun the refreshing capabilities of any GUI.
Ready for DataReady
Since the DataReady
event is called directly from the PowerShell thread, it allows us to throttle back the PowerShell processing speed to the point where it exactly matches the throughput of the consumer. Of course, this means it will reduce the speed of the PowerShell script, but I feel that is the lesser of two evils, in this case.
PipelineExecutor
All of the above steps have been wrapped in a single helper class called PipelineExecutor
. This class helps you to easily run a PowerShell script in the background, and also provides you with events to receive the output data of the script. It also 'Invokes' the data to the correct thread, so when the events arrive, you no longer have to perform the dreaded 'InvokeRequired
' routine just to display the data. Here's the public
interface of the class:
public class PipelineExecutor
{
public Pipeline Pipeline
{
get;
}
public delegate void DataReadyDelegate(PipelineExecutor sender,
ICollection<psobject> data);
public delegate void DataEndDelegate(PipelineExecutor sender);
public delegate void ErrorReadyDelegate(PipelineExecutor sender,
ICollection<object> data);
public event DataReadyDelegate OnDataReady;
public event DataEndDelegate OnDataEnd;
public event ErrorReadyDelegate OnErrorRead;
public PipelineExecutor
(Runspace runSpace, ISynchronizeInvoke invoker, string command);
public void Start();
public void Stop();
}
Using the Code
The following code shows how to create and asynchronously run a PowerShell script using PipelineExecutor
:
...
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using Codeproject.PowerShell
...
Runspace runSpace = RunspaceFactory.CreateRunspace();
runSpace.Open();
PipelineExecutor pipelineExecutor =
new PipelineExecutor(runSpace, this, textBoxScript.Text);
pipelineExecutor.OnDataReady +=
new PipelineExecutor.DataReadyDelegate(pipelineExecutor_OnDataReady);
pipelineExecutor.OnDataEnd +=
new PipelineExecutor.DataEndDelegate(pipelineExecutor_OnDataEnd);
pipelineExecutor.OnErrorReady +=
new PipelineExecutor.ErrorReadyDelegate(pipelineExecutor_OnErrorReady);
pipelineExecutor.Start();
Terminating the script and cleaning up:
pipelineExecutor.OnDataReady -=
new PipelineExecutor.DataReadyDelegate(pipelineExecutor_OnDataReady);
pipelineExecutor.OnDataEnd -=
new PipelineExecutor.DataEndDelegate(pipelineExecutor_OnDataEnd);
pipelineExecutor.OnErrorReady -=
new PipelineExecutor.ErrorReadyDelegate(pipelineExecutor_OnErrorReady);
pipelineExecutor.Stop();
runSpace.Close();
Please note that to compile the example project, you'll first have to install PowerShell and the Windows Server 2008 SDK. For details, see my previous article.
Error Handling
There are two kinds of errors that you can encounter during execution of a powershell script:
- Errors that occur during execution of a powershell script, but that don't result in the termination of the script.
- Fatal errors that are the result of an invalid powershell syntax
Errors of the first kind will be pushed into the error pipeline. You can listen for those by adding an event handler to PipelineExecutor.OnErrorReady
.
To detect and display errors of the second kind (fatal syntax errors) you need to inspect the property Pipeline.PipelineStateInfo.State
inside your OnDataEnd
event handler. If this value is set to PipelineState.Failed
then the property Pipeline.PipelineStateInfo.Reason
will contain an exception object with detailed information on the cause of the error. The following code snippet shows how this is done in the example project:
private void pipelineExecutor_OnDataEnd(PipelineExecutor sender)
{
if (sender.Pipeline.PipelineStateInfo.State == PipelineState.Failed)
{
AppendLine(string.Format("Error in script: {0}", sender.Pipeline.PipelineStateInfo.Reason));
}
else
{
AppendLine("Ready.");
}
}
If you want to see the error handling in action, please execute the "Error handling demonstration" script of the example project.
Points of Interest
For people interested in the inner workings of PipelineExecutor
, I'd like to point out the private StoppableInvoke
method. It waits for the target thread to process the data, thus causing the throttling effect on the PowerShell script. It also avoids potential deadlock problems that would happen if I had simply used ISynchronizeInvoke.Invoke
because it is interruptible by way of a ManualResetEvent
. This pattern could be useful in other workerthread-to-UI notification situations.
I've also worked on improving the performance of the script execution by separating the 'BeginInvoke
' and 'EndInvoke
' stages of StoppableInvoke
into subsequent DataReady
cycles, and this works like a charm... but the resulting output performance is just too close to the consumer performance, causing lag in the user interface. My theory is that .NET likes to give priority to Invoke
messages before handling the UI update messages. So, if you get close to 100% of the message loop performance, it will starve the UI updates. Solving this problem probably warrants a different article.
History
- April 15, 2007
- August 29, 2008
- Added paragraph on error handling
- Converted project to Visual Studio 2008
- Fixed broken links