A script or executable can be run using System.Diagnostics.Process
. The string FileName
is set to the executable (e.g. perl.exe, Run.bat, ConsoleApplication.exe ). The string Arguments
is set to the command-line arguments for that executable (e.g. perlscript.pl, filename1.txt filename2.txt , etc). The following code will start that executable.
Process process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.FileName = FileName;
process.StartInfo.Arguments = Arguments;
process.StartInfo.WorkingDirectory = WorkingDirectory;
process.Start();
Notice that the standard output and standard error have both been redirected. There are two StreamReaders in the Process class that can be used to read the output: Process.StandardOutput
and Process.StandardError
. Often, the output is not read until after the process has finished, as in the following:
string output = process.StandardOutput.ReadToEnd();
Reading to the end will not work for this application, since we want to read the data as the process is running.
Multiple threads
The solution is to use multiple threads. One thread is dedicated to running the process and two more threads are dedicated to reading the standard error and standard output. This is mentioned in MSDN documentation. Each of these two threads will run a simple function that sits in a loop reading from the stream until the stream is closed.
void ReadStdOut()
{
string str;
while ((str = process.StandardOutput.ReadLine()) != null)
{
// do something with str
}
}
After each line is read into str, we would like to notify a windows form to display the text. Raising an event is probably the best way to accomplish this. For every new line of text received (on either StandardOutput
or StandardError
) an event will be raised. A windows form class can subscribe to these events and update a text box. Simple, but it won't quite work without some additional work.
Important rule of windows forms
There is an important rule of windows forms and multithreading. Controls are (almost entirely) not thread safe. This means that an event raised from any thread other than the UI Thread cannot use methods or properties of a control. There are a few methods guaranteed to be safe including Control.Invoke
and Control.BeginInvoke
. These methods are used to run a function on the UI thread.
Thankfully, we can inherit from the class AsyncOperation
(written by I.D. Griffiths from the above mentioned MSDN article) to solve several problems. First, this class allows us to raise an event on a UI thread of a hosting or target control. The above function becomes:
public delegate void DataReceivedHandler(object sender,
DataReceivedEventArgs e);
public event DataReceivedHandler StdOutReceived;
void ReadStdOut()
{
string str;
while ((str = process.StandardOutput.ReadLine()) != null)
{
FireAsync(StdOutReceived, this, new DataReceivedEventArgs(str));
}
}
FireAsync
is a method provided by the class AsyncOperation
. It raises an event (or more specifically invokes any delegate) on the UI thread of a form or control. StdOutReceived
is the event that will be raised. DataReceivedEventArgs
is a class derived from EventArgs
that has a single string containing the text to be displayed (its definition is not shown here for brevity).
The second thing AsyncOperation
provides is a method of canceling a process. Let's take a look at that class in more detail.
Inheriting from AsyncOperation
AsyncOperation
is an abstract base class that assists in creating cancelable worker threads that can fire events back on a UI control (or form). It provides two main methods that are called by a form class: Start()
and Cancel()
.
AsyncOperation
requires that one method be overridden: DoWork()
. This method is called when the Start()
method is called. As the method runs, it is expected to watch for a flag CancelRequested
that is set from a call to Cancel()
. If the flag is true, the method should acknowledge the cancel and return.
The implementation of DoWork()
is as follows:
void protected override void DoWork()()
{
// Start a new process for the cmd
Process process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.FileName = FileName;
process.StartInfo.Arguments = Arguments;
process.StartInfo.WorkingDirectory = WorkingDirectory;
process.Start();
// Invoke stdOut and stdErr readers - each
// has its own thread to guarantee that they aren't
// blocked by, or cause a block to, the actual
// process running (or the gui).
new MethodInvoker(ReadStdOut).BeginInvoke(null, null);
new MethodInvoker(ReadStdErr).BeginInvoke(null, null);
// Wait for the process to end, or cancel it
while (! process.HasExited)
{
Thread.Sleep(SleepTime); // sleep
if (CancelRequested)
{
// Not a very nice way to end a process,
// but effective.
process.Kill();
AcknowledgeCancel();
}
}
}
The methods DoWork()
, ReadStdOut()
, and ReadStdErr()
, the properties FileName
and Arguments
, and the events StdOutReceived
and StdErrReceived
are all added to a class ProcessCaller
which derives from AsyncOperation
. Both classes can be downloaded as part of the zipfile at the top of the page.
Comments