Saturday, September 8, 2012

Controlling Console Apps with the Process Class


By now, hopefully you are thinking of console applications as building blocks that can be strung together with pipes. Indeed, this is typically how they are used. They can, however, be used in other ways. The .NET Framework class library's Process class allows .NET Framework-based programs to interoperate with console applications, regardless of their origin. The Process class communicates with the console application by way of—you guessed it—the standard input and output streams.
One occasion when this might be useful is when a console application already exists to perform a desired task. Rather than recode it in a CLR-compliant language, it might be easier to interoperate with it via the Process class. Or perhaps using the .NET Framework isn't the best way to implement a particular bit of functionality. Consider this example. Suppose you were asked to create a graphing calculator application. How would you do it using the .NET Framework? Aside from the expected tasks of laying out forms and working with the System.Drawing library, you would find yourself faced with having to figure out how to evaluate expressions. That is, how can you find out the value of (sin(x) + 3)/16 for all values of x?
It isn't an easy problem. You would probably find yourself having to write an expression parser or attempting to compile the expression to Microsoft intermediate language (MSIL) on the fly. Or, you could take the easy way out and use the venerable VBScript Eval function. It isn't part of the .NET Framework, but it sure is easy. It turns out that it takes about 20 lines of VBScript to implement a simple calculator that works on standard input and output. If you were to run this script in a console window, you would find that you can type expressions like "1+1" or "sin(23) + 232.23 / 17" into standard input and see results like "2" or "12.8143678311189" appear on standard output. Since it loops forever, you must type Ctrl-C to end the program when using it in interactive mode.
Meanwhile, back in the .NET world, you need a way of communicating with the VBScript calculator. This is where the Process class comes in. The following code snippet starts up an instance of the VBScript calculator:
System.Diagnostics.Process calcProc = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo i = 
    new System.Diagnostics.ProcessStartInfo();
i.FileName = "cscript.exe";
i.Arguments = "//NoLogo calc.vbs";
i.RedirectStandardOutput = true;
i.RedirectStandardInput = true;
i.RedirectStandardError = true;
i.CreateNoWindow = true;
i.UseShellExecute = false;
calcProc.StartInfo = i;
calcProc.Start();
Let's look at what's going on here. The first step is to create an instance of the Process class, called calcProc. In order to specify detailed start-up parameters for Process, the .NET class library provides a helper class, ProcessStartInfo. The next seven lines create and populate an instance of this class. The properties are described in Figure 8. Finally, the instance of the Process class is handed the instance of the ProcessStartInfo class, and the Process is started.

Property Description
FileName The name of the executable to start when the Start method is called on the Process class.
Arguments Command-line arguments to pass to the executable specified in FileName.
RedirectStandardOutput Indicates whether the standard output for the target application (that is calc.vbs) should be routed to the instance of the Process class. The alternative is to have it go to the default location for standard output—the console.
RedirectStandardInput Same as RedirectStandardOutput, but for standard input instead.
CreateNoWindow Causes the target application to execute invisibly. Not setting this property causes a command window to appear.
UseShellExecute Indicates whether the Windows shell or the Process class should be used to start the process. This property must be set to false in order to redirect the input and output streams.
Thereafter, the Framework-based application can communicate with the VBScript calculator via the StandardInput and StandardOutput properties of calcProc. For example, the following code sends an expression to the calculator and reads the result:
calcProc.StandardInput.WriteLine("1+1");
string result = calcProc.StandardOutput.ReadLine();
To turn the expression evaluator into a graphing calculator, you just need to send the expression multiple times with different values of x. Remember, the VBScript calculator stays running for the lifetime of the Process class. You just keep sending it standard input and it keeps returning standard output.
Figure 9 Graphing Calculator 
Figure 9 shows the finished graphing calculator. The majority of the code deals with drawing the graph and is tangential to this article (it's included in the code download at the link at the top of this article). There is, however, one more important point to make: the VBScript calculator will continue to run even after you close the Framework-based application that invoked it. To ensure that you don't have an orphaned process, you can send Ctrl-Z in input, or explicitly end it by invoking the Kill method:
calcProc.Kill();
I'll bet you never thought you'd see interoperability between the .NET Framework and VBScript! While this example is contrived, I don't think it is entirely far-fetched. There are many useful VBScript programs (not to mention Perl, Python, and C programs) already written for systems management tasks. While these will eventually get ported to managed code, it won't be for a long time. Moreover, when they do get ported, it won't be all at once. Interoperability with legacy code will always be important and standard input and output are simple, effective ways to achieve it.

Yes, But Can it Read an RSS Feed?
In case you aren't familiar with it, RSS is a way of syndicating information on the Internet in a machine-readable format. It takes advantage of the addressing and transport aspects of the Web (URLs and HTTP) while eschewing its markup language (HTML) in favor of something more structured (the RSS dialect of XML). Programs that read RSS feeds have become commonplace and are frequently considered archetypal examples of how to create XML and network-savvy applications. Figure 10 shows the XML code for a shortened RSS feed.
<rss version="2.0">
    <channel>
        <title>MSDN: .NET Framework and CLR</title>
        <link>http://msdn.microsoft.com/netframework/</link>
        <description>
            The latest information for developers on the Microsoft 
            .NET Framework and Common Language Runtime (CLR).
        </description>
        <language>en-us</language>
        <ttl>1440</ttl>
        <item>
            <title>
                Improving Web Application Security:  
                Threats and Countermeasures
            </title>
            <pubDate>Thu, 12 Jun 2003 07:00:00 GMT</pubDate>
            <description>
                Bake security into your application lifecycle. 
                You'll get guidance that is task-based and modular,
                with tons of implementation steps.
            </description>
            <link>...</link>
        </item>
    </channel>
</rss>
Reading an RSS feed is really a matter of fetching a URL, transforming its contents from machine-readable to human-readable form, and displaying it to the user. Usually there is a caching step between steps one and two so that the whole thing can work offline as well. This pattern of fetch, cache, transform, and display is repeated over and over for every RSS feed in which a particular user is interested. Fetch, cache, transform, and display. It starts to sound like a pipeline. Can the world's first command-line RSS reader be far behind?
Like any good pipeline, the steps are not RSS specific. Figure 11 provides a description of how the first three steps should work. I have left out the "display" step because the console takes care of it in this case. These three console applications fit together in a pipeline, like the one shown in Figure 12.

Step Description
Fetch URL Retrieves the contents of the URL given in the first argument. Writes contents to standard out. If an error is encountered, writes error information to standard error and writes nothing to standard out.
Cache file name Takes contents of standard in, writes it to the file given in the first argument, and passes data through to standard out. If standard in is empty, attempts to read the file given in the first argument and sends its contents to standard out. File access errors are written to standard error.
Transform stylesheet Expects valid XML on standard in. If XML is valid, transforms it with the XSLT stylesheet given in the first argument and sends the results to standard out. Writes errors to standard error.
Figure 12 An Application Pipeline 
When implementing fetch, cache, and transform, you should continue with your design patterns as I've described them. To refresh your memory, all engine classes should be implemented as subclasses of the ConsoleEngineBase class. Figure 13 shows the class library so far. Code for all engines is included in the download for this article.
Figure 13 Class Library 
As I've shown, it isn't very difficult to add functionality to the engine classes. In all cases, it is primarily a matter of overriding PreProcess, PostProcess, and ProcessLine. All three of these new engines take advantage of PreProcess to create instance-level private resources: in the case of FetchEngine, an instance of the WebClient class; in the case of CacheEngine, a file handle; in the case of TransformEngine, an instance of the XslTransform class. The code for all three engines is included in the download for this article.
Now that the engines are implemented, you can test them. The following command line uses the generic chassis class to put the RSS reader through its paces:
C:\> GenericChassis "Engines" "Engines.FetchEngine" "http://
     msdn.microsoft.com/netframework/rss.xml" | GenericChassis "Engines" 
     "Engines.CacheEngine" "netframework.xml" | GenericChassis "Engines" 
     "Engines.TransformEngine" "FormatForConsole.xslt"
That's a lot of typing to view an RSS feed. You might consider creating a batch file with that in it, like this:
GenericChassis "Engines" "Engines.FetchEngine" "%1" | GenericChassis 
   "Engines" "Engines.CacheEngine" 
   "%2" | GenericChassis "Engines" 
   "Engines.TransformEngine" 
   "FormatForConsole.xslt"
Assuming you called your batch file rss.bat, you could execute it by running:
C:\> rss.bat "http://
  msdn.microsoft.com/netframework/  
  rss.xml" "netframework.xml"
If you run it while connected to the Internet, you should see the contents of the RSS feed scroll past your eyes in a somewhat readable format. If you disconnect, the cache engine should kick in, allowing you to still see the feed.
Now let's say you want to take advantage of Windows Forms to improve the user interface. At first, this sounds straightforward: create an instance of the Process class, feed it the command text for the pipeline, read standard output, and show it to the user. Unfortunately, it doesn't prove to be that simple. Unlike the calculator example, this one involves three distinct processes: fetch, cache, and transform. Remember, when you run these via cmd.exe they actually appear in Task Manager as three distinct processes. Cmd.exe takes care of routing the standard data streams among them. The operating system itself has no notion of pipes. Consequently, the Process class has no notion of pipes. Trying to send a pipeline to a Process instance will not get you what you want.
There are two different ways to get around this. First, you could invoke cmd.exe via the Process class and send commands to its standard input. Cmd.exe is, after all, just another console application. It differs from other console applications only in that it expects commands rather than data on standard input. In this scenario, you would let cmd.exe do all the work of parsing the command lines and routing the standard data streams. You could monitor cmd.exe's standard output to get the results.
The second option is to do the work of routing the standard streams yourself. That is, you could create a class called Pipe that joins one Process to another. The Pipe class would basically have the job of looking for standard output of one Process and immediately sending it to standard input of the other Process. You could use a combination of processes and pipes to build complex pipelines in your code.
I tried both options and can confirm that they both work. For the first option, I created a class called CommandRunner that references an instance of Process internally. It has a single method, called RunCommand, that takes a command string and returns the output of the command as a string. The internal Process class serves as a proxy to a running instance of cmd.exe. The RunCommand method sends commands to it using standard input and reads the results on standard output.
Figure 14 Reading Dir Listing 
But there's a problem here. The standard output stream of cmd.exe never ends, though it frequently contains no data to read. What does this mean? Basically, that read operations on standard output will block indefinitely when the end of the output is reached, effectively locking up the program. Figure 14 shows a typical interaction with cmd.exe via the standard streams. Cmd.exe is sent a command (dir in this case) and responds, as expected, with a directory listing. Since a directory listing can contain any number of lines, the client program doesn't know when to stop reading. Eventually, the client program will attempt to read a line that doesn't exist and will wait for it, blocking execution of the entire program.
For this reason, the code shown in Figure 15 will not work. One way around this is to run an output "monitor" on another thread, but that still leaves ambiguity as to whether the current command is finished or is just in the middle of a long operation. If you run cmd with the /c option, it will execute and exit immediately. However, you can also check this manually. To find out if cmd.exe is finished with the current command, Figure 16 shows an effective, if ugly, solution. The trick here is to insert a known string into the output of the command and then look for it. When it is found, you know you have reached the end of the current command's output.
public string RunCommand(string cmd)
{
    this.InnerProcess.StandardInput.WriteLine(cmd);
    this.InnerProcess.StandardInput.WriteLine("echo ---end---");
    StringBuilder output = new StringBuilder();
    string currentLine = this.InnerProcess.StandardOutput.ReadLine();
    while(currentLine != "---end---")
    {
        output.Append(currentLine);
        output.Append("\r\n");
        currentLine = this.InnerProcess.StandardOutput.ReadLine();
    }
    return output.ToString();
}
public string RunCommand(string cmd)
{
    this.InnerProcess.StandardInput.WriteLine(cmd);
    StringBuilder output = new StringBuilder();
    string currentLine = this.InnerProcess.StandardOutput.ReadLine();
    while(currentLine != null)
    {
        output.Append(currentLine);
        output.Append("\r\n");
        //the next line will block when there is no more 
        //output from the command
        currentLine = this.InnerProcess.StandardOutput.ReadLine();
    }
    return output.ToString();
}
Because of all the subterfuge required to get cmd.exe to cooperate, it's tempting to try to implement your own piping functionality. To accomplish this, I created two new classes. The first class, Pipe, connects two Processes. It continuously reads the output from the "left-side" Process and writes it to the input of the "right-side" Process. Because the Read operation will block as it waits for more input, the read/write loop executes on a separate thread. The following example connects the two commands via the Pipe class:
ConsoleProcess atProc = new ConsoleProcess("at.exe", "/?");
ConsoleProcess findProc = new ConsoleProcess("findstr.exe", "\/delete");
Pipe pipe = new Pipe(atProc, findProc);
atProc.Start();
findProc.Start();
pipe.Start();
string output = findProc.StandardOutput.ReadToEnd(); 
The complete code for the Pipe class is available as a part of the code download.
The second new class, Pipeline, automates some of these actions and allows an arbitrary number of Processes to be strung together. Internally, it keeps a list of Processes and Pipes. Each Pipe connects two of the Processes in the list. Processes are added to the Pipeline via the Add method. As you would expect, the Pipeline class exposes StandardInput and StandardOutput properties, which are just proxies for the StandardInput of the first Process and the StandardOutput of the last Process, respectively. The example I just showed could be recast using the Pipeline class as follows:
ConsoleProcess atProc = new ConsoleProcess("at.exe", 
   "/?");
ConsoleProcess findProc = new ConsoleProcess("findstr.exe", "\/delete");
Pipeline pipeline = new Pipeline();
pipeline.Add(atProc);
pipeline.Add(findProc);
pipeline.Start(); //pipes are started automatically
string output = pipeline.StandardOutput.ReadToEnd();
The code for the Pipeline class is also available in the code download.
Figure 17 An RSS Reader 
Either of these techniques (sending commands to cmd.exe or using the Pipe and Pipeline classes) could be used to hook the RSS pipeline into another .NET Framework-based application. The Pipe/Pipeline version is cleaner, but is slightly harder to implement. The cmd.exe version feels like a workaround, but you get all the features of cmd.exe for free. I built a Windows Forms user interface to the RSS pipeline that works both ways (shown in Figure 17 and included as a download), complete with support for clickable hyperlinks.

No comments:

Post a Comment