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:
That's a lot of typing to view an RSS feed. You might consider creating a batch file with that in it, like this:
C:\> GenericChassis "Engines" "Engines.FetchEngine" "http://
msdn.microsoft.com/netframework/rss.xml" | GenericChassis "Engines"
"Engines.CacheEngine" "netframework.xml" | GenericChassis "Engines"
"Engines.TransformEngine" "FormatForConsole.xslt"
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:
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.
C:\> rss.bat "http:// msdn.microsoft.com/netframework/ rss.xml" "netframework.xml"
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:
The complete code for the Pipe class is available as a part of the code download.
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
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:
The code for the Pipeline class is also available in the code download.
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();
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