PowerShell Using Threads in C# to Launch a Process and Read Output Streams

The other day I wanted to write a PowerShell script that required repeatedly launching an application to process a list of items. The application needed to capture both stdout and stderr from the application so that I could echo that information to my log file for future reference. Seems like something that would be pretty trivial in PowerShell...

But I found this task to be surprisingly challenging.  (Maybe it's just because I don't work with PowerShell as much as I should?) The hard part was to capture both output streams from the launched application. It's pretty easy to capture stdout using any one of several ways to launch stuff in PowerShell.  With some additional effort, it's also not that hard to capture stderr. But the hard part was to capture both. To do this you need to 'resort' to using the .net diagnostics library, and even then the approach seemed to be prone to hangs.

The problem is that you need to read from both streams at the same time. Even though there are lots of code samples out there showing calls to ReadToEnd on the standard streams, my experience is that one or the other call will hang forever waiting for input that never comes.

In the past, I've solved this problem in .Net using helper threads to read the streams while the main thread waits on the helper threads and the launched process to finish.  Unfortunately, it appears that creating multi-threaded apps in PowerShell is not supported.

Or is it? After some more digging I found a simple way to combine my old .Net threaded approach to capturing those streams with the new and modern PowerShell world.

The solution uses the very nifty PowerShell cmdlet call Add-Type. This cmdlet lets me create a Frankenstein like solution that incorporates a fragment of C# code that handles the running of the command and that captures of the output streams using some simple background threads. Once I have that output captured, I can log it to my heart's content.

The PowerShell script has the following components:

  • Source code for a C# class in a string literal assigned to a variable.
  • The C# class has one method. Call this method to launch a process. That method wraps the use of background threads to capture the output streams from the process.
  • Use the Add-Type cmdlet to 'hydrate' that code into a class that can be instantiated in the script.
  • Use New-Object to instantiate an object of this new class.
  • Call a method on the new object to launch the process. When the method returns, you can harvest the captured output streams from properties of the object, and do what you want with them.

You can download the script from here:  https://blogs.msdn.com/cfs-file.ashx/__key/CommunityServer-Blogs-Components-WeblogFiles/00-00-01-03-40-PowerShell+with+C_2300_/3666.RunCommand-PowerShell.zip

Here is the source code. It shows how to launch two programs.  Hopefully you can imagine how this can be applied in your scripts to run commands and to get at all those exciting output streams.

 #A powershell script that shows how to:

# - use Add-Type to define and use a class define in c#

# - launch a program from Powershell and capture both stdout and stderr





# Place C# definition of a class into a local string variable.

# This class provide one method that can be used to launch a process and waits for it to complete.

# The output streams stdout and stderr from the process are captured and made available via properties of the class.

#

$source = @"    

using System;

using System.IO;

using System.Threading;

using System.Diagnostics;



    public class RunCommand

    {

        private class AllAboutIO

        {

            public AllAboutIO(StreamReader reader)

            {

                this.reader = reader;

            }



            public string result;

            public StreamReader reader;

        }



        public string stdout;

        public string stderr;



        public void AsyncStreamReader(object data)

        {

            AllAboutIO myData = (AllAboutIO)data;

            myData.result = myData.reader.ReadToEnd();

        }



        public int Run(string Program, string Arguments)

        {       

            Process myProc = new Process();



            myProc.StartInfo.UseShellExecute = false;

            myProc.StartInfo.FileName = Program;

            myProc.StartInfo.Arguments = Arguments;

            myProc.StartInfo.CreateNoWindow = true;

            myProc.StartInfo.RedirectStandardError = true;

            myProc.StartInfo.RedirectStandardOutput = true;



            try

            {

                if (myProc.Start())

                {

                    AllAboutIO stdoutReader = new AllAboutIO(myProc.StandardOutput);

                    AllAboutIO stderrReader = new AllAboutIO(myProc.StandardError);



                    Thread readStdOutThread = new Thread(this.AsyncStreamReader);

                    Thread readStdErrThread = new Thread(this.AsyncStreamReader);



                    readStdOutThread.Start(stdoutReader);

                    readStdErrThread.Start(stderrReader);



                    myProc.WaitForExit();



                    readStdErrThread.Join();

                    readStdOutThread.Join();



                    stdout = stdoutReader.result;

                    stderr = stderrReader.result;



                    return myProc.ExitCode;

                }

                else

                {

                    stderr = "Process did not start. Verify that the program exists at the specified location.";

                    return 1;

                }

            }

            catch (System.Exception excp)

            {

                stderr = "Failed to launch the process: " + excp.Message;

                return 1;

            }

        }

    }

"@



# Create a type from the C#.

Add-Type -TypeDefinition $source -ReferencedAssemblies ("System.Core")



# Helper function for launching a a program.

function Start-Proc  {   

    param (   

    $exe,   

    $arguments,

    [ref]$stdout,

    [ref]$stderr

     ) 



    $runCommand = New-Object RunCommand

    

    $runCommandResult = $runCommand.Run($exe, $arguments)

    

    $stdout.value = $runCommand.stdOut

    $stderr.value = $runCommand.stdErr

     

    return $runCommandResult       

 }

 



$stderr = ""

$stdout = ""



 

#A test of running a program that shows capture of stderr

$exitcode = Start-Proc ($env:windir + "\system32\net.exe") "/notacommand" ([ref]$stdout) ([ref]$stderr)

write-host "Exit code was: " $exitcode

write-host -ForegroundColor Green $stdout

write-host -ForegroundColor Red $stderr



#A test of running a command that shows capture of stdout

$exitcode = Start-Proc ($env:windir + "\system32\net.exe") "help" ([ref]$stdout) ([ref]$stderr)

write-host "Exit code was: " $exitcode

write-host -ForegroundColor Green $stdout

write-host -ForegroundColor Red $stderr

After seeing this you might think: Gosh that's a lot of work for just capturing some output from a command. It is. But it does illustrates some interesting capabilities of PowerShell. Thanks for reading.