Capturing all streams in correct sequence with PowerShell SDK

518 views Asked by At

I'm executing a script with the PowerShell SDK, which makes use of all different streams (information, warning, verbose, ..). I can capture the output from them correctly, but not in the sequence they are generated. As an example, here is a console app (C#, .NET 7, installed the NuGet package Microsoft.PowerShell.SDK):

using System.Management.Automation.Runspaces;

var runSpace = RunspaceFactory.CreateRunspace(InitialSessionState.CreateDefault());
runSpace.Open();

var instance = System.Management.Automation.PowerShell.Create(runSpace);

instance.AddScript("""
    $VerbosePreference = 'Continue'

    Write-Verbose "Line 1"
    Write-Output "Line 2"
    Write-Verbose "Line 3"
    Write-Information "Line 4"
    Write-Information "Line 5"
    Write-Verbose "Line 6"
    """
);

var output = instance.Invoke();

foreach (var o in output)
{
    Console.WriteLine($"[N]: {o}");
}

foreach (var v in instance.Streams.Verbose)
{
    Console.WriteLine($"[V]: {v}");
}

foreach (var i in instance.Streams.Information)
{
    Console.WriteLine($"[I]: {i}");
}

As you can see I'm returning different results on different streams. When I output them like that of course, they are no longer in the correct order:

[N]: Line 2
[V]: Line 1
[V]: Line 3
[V]: Line 6
[I]: Line 4
[I]: Line 5

I have been looking at the objects provided by instance.Streams.Information, instance.Streams.Verbose, etc. - but I could not find a property that would let me sort them. Interestingly instance.Streams.Information has a TimeGenerated, but it is missing from all the other stream objects!

So I'm stumped how I could accomplish this, would it be possible to get these sorted based on the time they were generated?

2

There are 2 answers

1
Mathias R. Jessen On BEST ANSWER

You'll want to subscribe to the relevant event on each stream collection up front, rather then waiting until execution has finished.

To do so, register handlers for the DataAdded event on each relevant instance.Streams.* property:

var runSpace = RunspaceFactory.CreateRunspace(InitialSessionState.CreateDefault());
runSpace.Open();

using (var instance = System.Management.Automation.PowerShell.Create(runSpace))
{
    instance.AddScript("""
        $VerbosePreference = 'Continue'

        Write-Verbose "Line 1"
        Write-Output "Line 2"
        Write-Verbose "Line 3"
        Write-Information "Line 4"
        Write-Information "Line 5"
        Write-Verbose "Line 6"
        """
    );

    # register event handler for the DataAdded event on each relevant stream collection
    instance.Streams.Verbose.DataAdded += ConsumeStreamOutput;
    instance.Streams.Information.DataAdded += ConsumeStreamOutput;

    # now create a data collection for standard output
    var outputCollection = new PSDataCollection<PSObject>();
    # ... and register the event handler on that too
    outputCollection.DataAdded += ConsumeStreamOutput;

    # ... and now we're finally ready to invoke the script
    instance.Invoke();
}

Now we just need to define and implement the ConsumeStreamOutput method that will handle the events. It needs to expect different types of output based on stream it's written to.

static void ConsumeStreamOutput(object sender, DataAddedEventArgs evtArgs)
{
    var data = ((System.Collections.IList)sender)[evtArgs.Index];

    switch (data)
    {
        case VerboseRecord vr:
            WriteLine($"Received Verbose stream data     :  '{vr}'");
            break;
        case InformationRecord ir:
            WriteLine($"Received Information stream data :  '{ir}'");
            break;
        default:
            WriteLine($"Received standard output         :  '{data}'");
            break;
    }
}

The above should give you an output like:

Received Verbose stream data     :  'Line 1'
Received standard output         :  'Line 2'
Received Verbose stream data     :  'Line 3'
Received Information stream data :  'Line 4'
Received Information stream data :  'Line 5'
Received Verbose stream data     :  'Line 6'

Of course you can change this method to just collect the data to a single queue or similar if you need to process the data elsewhere.

0
Thomas Glaser On

After reading some more about PS hosts, I found the way to do it! Basically it's possible to redirect all other streams to the standard output stream with this line:

instance.Commands.Commands[0].MergeMyResults(PipelineResultTypes.All,PipelineResultTypes.Output);

With that, instance.Invoke() returns everything, including verbose, information, etc. streams.

Then by checking what each PSObject is, I can do treat the objects differently depending on their type. Full sample:

using System.Management.Automation;
using System.Management.Automation.Runspaces;

var runSpace = RunspaceFactory.CreateRunspace(InitialSessionState.CreateDefault());
runSpace.Open();

var instance = PowerShell.Create(runSpace);

instance.AddScript("""
    $VerbosePreference = 'Continue'

    Write-Verbose "Line 1"
    Write-Output "Line 2"
    Write-Error "Line 3"
    Write-Verbose "Line 4"
    Write-Information "Line 5"
    Write-Information "Line 6"
    Write-Verbose "Line 7"
    """
);

instance.Commands.Commands[0].MergeMyResults(PipelineResultTypes.All,PipelineResultTypes.Output);

var output = instance.Invoke();

foreach (var o in output)
{
    switch (o.ImmediateBaseObject)
    {
        case ErrorRecord er:
            Console.WriteLine($"[ERR]: {er}");
            break;
        case WarningRecord wr:
            Console.WriteLine($"[WRN]: {wr}");
            break;
        case VerboseRecord vr:
            Console.WriteLine($"[VER]: {vr}");
            break;
        case InformationRecord ir:
            Console.WriteLine($"[INF]: {ir}");
            break;
        case DebugRecord dr:
            Console.WriteLine($"[DBG]: {dr}");
            break;
        default:
            // I assume everything that ends up here is uncaptured output.
            Console.WriteLine($"[OUT]: {o}");
            break;
    }
}

And the output is now in the correct order:

[VER]: Line 1
[OUT]: Line 2
[ERR]: Line 3
[VER]: Line 4
[INF]: Line 5
[INF]: Line 6
[VER]: Line 7