Click here to Skip to main content
Click here to Skip to main content

Asynchronously Execute PowerShell Scripts from C#

, 29 Aug 2008
Rate this:
Please Sign up or sign in to vote.
How to host and asynchronously run PowerShell scripts from C#
Screenshot - AsyncPowerShell_scr.png

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:

/// Class that assists in asynchronously executing
/// and retrieving the results of a powershell script pipeline.

public class PipelineExecutor
{
    /// Gets the powershell Pipeline associated with this 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);

    /// Occurs when there is new data available from the powershell script.
    public event DataReadyDelegate OnDataReady;

    /// Occurs when powershell script completed its execution.
    public event DataEndDelegate OnDataEnd;

    /// Occurs when there is error data available.
    public event ErrorReadyDelegate OnErrorRead;

    /// Constructor, creates a new PipelineExecutor for the given powershell script.
    public PipelineExecutor
        (Runspace runSpace, ISynchronizeInvoke invoker, string command);

    /// Start executing the script in the background.
    public void Start();

    /// Stop executing the script.
    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
    ...

    // create Powershell runspace
    Runspace runSpace = RunspaceFactory.CreateRunspace();

    // open it
    runSpace.Open();

    // create a new PipelineExecutor instance

    // 'this' is the form that will show the output of the script.
    // it is needed to marshal the script output data from the
    // powershell thread to the UI thread
    PipelineExecutor pipelineExecutor =
      new PipelineExecutor(runSpace, this, textBoxScript.Text);

    // listen for new data
    pipelineExecutor.OnDataReady +=
      new PipelineExecutor.DataReadyDelegate(pipelineExecutor_OnDataReady);

    // listen for end of data
    pipelineExecutor.OnDataEnd +=
      new PipelineExecutor.DataEndDelegate(pipelineExecutor_OnDataEnd);

    // listen for errors
    pipelineExecutor.OnErrorReady +=
      new PipelineExecutor.ErrorReadyDelegate(pipelineExecutor_OnErrorReady);

    // launch the script
    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();
// close the powershell runspace
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:

    // OnDataEnd event handler
    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
    • First release
  • August 29, 2008
    • Added paragraph on error handling
    • Converted project to Visual Studio 2008
    • Fixed broken links

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

jpmik
Architect
Netherlands Netherlands
No Biography provided

Comments and Discussions

 
Question[My vote of 1] A winform, seriously Pinmembermsdevtech21-May-14 3:18 
GeneralMy vote of 4 Pinmemberzoom66281-Nov-13 23:34 
QuestionPLEASE HELP PinmemberMember 1033561616-Oct-13 11:03 
AnswerRe: PLEASE HELP Pinmemberjpmik16-Oct-13 11:47 
GeneralRe: PLEASE HELP PinmemberMember 1033561616-Oct-13 11:54 
GeneralRe: PLEASE HELP PinmemberMember 1033561616-Oct-13 12:01 
GeneralRe: PLEASE HELP Pinmemberjpmik16-Oct-13 12:06 
GeneralRe: PLEASE HELP PinmemberMember 1033561616-Oct-13 12:11 
GeneralRe: PLEASE HELP Pinmemberjpmik16-Oct-13 12:25 
GeneralRe: PLEASE HELP PinmemberMember 1033561616-Oct-13 12:32 
GeneralRe: PLEASE HELP Pinmemberjpmik16-Oct-13 12:46 
GeneralRe: PLEASE HELP PinmemberMember 1033561616-Oct-13 12:49 
GeneralRe: PLEASE HELP PinmemberMember 1033561616-Oct-13 12:43 
GeneralRe: PLEASE HELP Pinmemberjpmik16-Oct-13 12:56 
GeneralRe: PLEASE HELP PinmemberMember 1033561616-Oct-13 13:03 
QuestionPowercli PinmemberChiefnico4-Apr-13 12:08 
QuestionHow i can do PinmemberSharki17-Jun-12 11:46 
QuestionLaunch a script on 6000 machine Pinmemberevangile27-May-12 5:00 
Questionhow to add argument? Pinmemberevangile9-May-12 4:30 
QuestionHow can I get the output of the powershell execution right before calling Stop() method? PinmemberSeungweon Park11-Apr-12 9:25 
QuestionGet-User Throwing Exception using PipeLine Executor Pinmemberameerbashans3-Apr-12 13:05 
QuestionPowershell version? PinmemberStiv Ostenberg27-Feb-12 5:16 
AnswerRe: Powershell version? Pinmemberjpmik27-Feb-12 6:47 
QuestionPowerCLI Pinmemberpicasso23-Dec-11 19:26 
AnswerRe: PowerCLI Pinmemberjpmik5-Dec-11 6:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 29 Aug 2008
Article Copyright 2007 by jpmik
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid