Using piped output redirection on the Process/ProcessStartInfo classes...

Well, I recently found the need to use the command line in order to get some information (file names) that needed to be processed by my application.  My purpose was to find all non read-only files (files that had changed) that matched a set of criterion.  You see I'm working on a source controlled project outside of source control, so it becomes hard to remember which files I've changed, especially in a VS environment where multiple files that I'm not actually using get changed (though they do warn me before changing the read-only flag).

Normally, I would just use Perl, since it has a qc// syntax which allows you to run commands and pipe the output into a Perl array.  I love this syntax because it is so fast and easy.  .NET simply doesn't have these in-grained features unless of course you are using a .NET language that contains them (Perl.NET anyone?).  So what goes into a command like qc// anyway?  Well, a bunch of IO redirection and that is pretty much it, so that is what I wanted to implement in .NET.  I came up with two classes, ProcessSnoop and CommandLineSnoop.  ProcessSnoop lets you work with any process you want, while CommandLineSnoop has command line only features like automagically building your command line string for you.  Let's check these out:

public class ProcessSnoop {
    protected string stdOut = null;
    protected string stdErr = null;
    protected ProcessStartInfo psi = null;
    protected Process activeProcess = null;
   
    public ProcessSnoop(ProcessStartInfo psi) {
        this.psi = psi;
    }
   
    public int Run() {
        Thread thread_ReadStandardError = new Thread(new ThreadStart(Thread_ReadStandardError));
        Thread thread_ReadStandardOut = new Thread(new ThreadStart(Thread_ReadStandardOut));
   
        Console.WriteLine(psi.FileName);
        Console.WriteLine(psi.Arguments);

        activeProcess = Process.Start(psi);
        if ( psi.RedirectStandardError ) {
            thread_ReadStandardError.Start();
        }
        if ( psi.RedirectStandardOutput ) {
            thread_ReadStandardOut.Start();
        }
        activeProcess.WaitForExit();
       
        thread_ReadStandardError.Join();
        thread_ReadStandardOut.Join();
       
        return activeProcess.ExitCode;
    }
   
    public void Thread_ReadStandardError() {
        if ( activeProcess != null ) {
            stdErr = activeProcess.StandardError.ReadToEnd();
        }
    }
   
    public void Thread_ReadStandardOut() {
        if ( activeProcess != null ) {
            stdOut = activeProcess.StandardOutput.ReadToEnd();
        }
    }
   
    public string StandardOut {
        get {
            return this.stdOut;
        }
    }
   
    public string StandardError {
        get {
            return this.stdErr;
        }
    }
}

You start with the basics.  You need to redirect the IO and you need to read it out.  ProcessSnoop assumes you've set up a ProcessStartInfo with all of the vitals, and it will redirect either StdOut and StdErr depending on whether you've marked them as redirect in the ProcessStartInfo.  We have to start threads for each read since there is a blocking type race condition mentioned in the documentation.  I think my implementation meets the specs and overcomes this condition.  I simply spawn and wait for a thread for each IO queue.  Works out fine as long as I wait for the threads to finish using a Join at the end (you could use another sync routine, but I find Join's to work fine for this process).

The IO queues are simply read into strings, which can later be processed by your application.  The next step is the command line implementation that takes some of the work out of the process of setting up a ProcessStartInfo.  I was surprised at how easy this step was, since it really enables a lot of power with access to any command line program, with relatively few lines of code.  I could probably set up a cool event driven system to allow for StdIn overrides as well, but I don't need that feature set at this time, and if I did, I'd probably just use a form of pipe redirection since there are few times where you actually need to respond to prompts and actually need an event driven input mechanism.  So here is the derived CommandLineSnoop class.

public class CommandLineSnoop : ProcessSnoop {
    private static string program = "\"%COMSPEC%\"";
    private static string args = "/c [command]";
   
    public CommandLineSnoop(string command) :
        base(
            new ProcessStartInfo(
                Environment.ExpandEnvironmentVariables(program),
                args.Replace("[command]", command)
            )
        )
    {
        if ( this.psi != null ) {
            this.psi.CreateNoWindow = true;
            this.psi.UseShellExecute = false;
            this.psi.RedirectStandardOutput = true;
            this.psi.RedirectStandardError = true;
        }
    }
}

I generally compile this into a library, however, I realized recently that I actually use unit testing in nearly all of my applications.  How do I do this you might ask?  Well, I simply include a LibTest class in all of my library code that has a Main method that matches the requirements for an executable.  That way if I compile the code as an executable then we can test the basic operation of the library to make sure it works.  My implementation focuses on doing a file listing of all non read-only files (excluding directories), recursively from the current directory and using the bare format so I can easily process the results.  You can pass a new command line in via the args, but there is a basic command line in case you don't specify one.  Then I process the output and print out very specific files that don't match additional search criterion.  It isn't a very powerful application or use of the CommandLineSnoop class, but it does the job of testing the functionality.

public class LibTest {
    private static void Main(string[] args) {
        string commandLine = "dir /a-r-d /s /b";
        if ( args.Length > 0 ) {
            commandLine = string.Join(" ", args);
        }
       
        CommandLineSnoop cls = new CommandLineSnoop(commandLine);
        cls.Run();
       
        ProcessReturn(cls.StandardOut);
        // Console.WriteLine(cls.StandardOut);
        // Console.WriteLine(cls.StandardError);
    }
   
    private static void ProcessReturn(string output) {
        using(StringReader sr = new StringReader(output)) {
            string line = null;
            while((line = sr.ReadLine()) != null) {
           
                if ( ValidExtension(line) && ValidPath(line) ) {
                    Console.WriteLine(line);
                }
            }
        }
    }
   
    private static string[] extensionExclusionList = new string[] { ".exe", ".pdb", ".dll", ".user", ".InstallLog", ".ncb", ".suo", ".webinfo" };
    private static bool ValidExtension(string line) {
        for(int i = 0; i < extensionExclusionList.Length; i++) {
            if ( line.EndsWith(extensionExclusionList[i]) ) {
                return false;
            }
        }
       
        return true;
    }

    private static string[] pathExclusionList = new string[] { "\\bin\\Debug\\", "\\bin\\Release\\", "\\AsmCheck\\Debug\\", "\\AsmCheck\\Release\\", "\\obj\\" };
    private static bool ValidPath(string line) {
        for(int i = 0; i < pathExclusionList.Length; i++) {
            if ( line.IndexOf(pathExclusionList[i]) > -1 ) {
                return false;
            }
        }
       
        return true;
    }
}

Well, since I feel bad for being extremely sick this week, I figured I'd share some code that everyone might find useful.  Hopefully I'm over this stupid flu and I'll be able to get back to my normal blogging and coding patterns.

Published Friday, February 27, 2004 5:58 PM by Justin Rogers
Filed under:

Comments

No Comments