Introduction
This tip describes a simple C# class that can be used to run console commands and get the standard output and standard error text of the command. Most of the functionality
to do this is provided by the Process class. However, it is useful to have the surrounding
code wrapped up in a class that can be easily reused in different projects. The functionality of the CommandPrompt
class can be seen in the screenshot
of the form above. It allows you to run a command with optional parameters and a timeout value. The command can be run synchronously or asynchronously.
The code is described in the next section.
Description of the Code
DataEventArgs Class
public class DataEventArgs : EventArgs
{
public string Data { get; private set; }
public DataEventArgs(string data)
{
Data = data;
}
}
DataEventArgs
is a small class that inherits from EventArgs
. It is used in the OutputDataReceived
and ErrorDataReceived
events for storing standard output and standard error text.
Class Declaration and Members
internal class CommandPrompt
{
private Process _process;
private ProcessStartInfo _startInfo;
private StringBuilder _standardOutput;
private StringBuilder _standardError;
public const int NoTimeOut = 0;
The class consists of the Process
and ProcessStartInfo
objects to keep track of the underlying process info. A couple of StringBuilder
objects contain the contents of the standard output and standard error data streams. Also, a public
constant is provided for specifying that there should be no timeout
period when running a command synchronously.
Properties
public bool IsRunning { get; private set; }
public bool HasExited { get; private set; }
public int ProcessId { get; private set; }
public int ExitCode { get; private set; }
public string StandardOutput
{
get
{
return _standardOutput.ToString();
}
}
public string StandardError
{
get
{
return _standardError.ToString();
}
}
Several public
properties are provided to retrieve the state of the command and the contents of the command output.
Events
public event EventHandler<DataEventArgs> OutputDataReceived = (sender, args) => { };
public event EventHandler<DataEventArgs> ErrorDataReceived = (sender, args) => { };
public event EventHandler Exited = (sender, args) => { };
Events are provided for users of the class to receive notifications when command output is received and when it exits.
The events are hooked up to dummy delegates so that we don't have to check if the event is null
when calling it.
Constructor
public CommandPrompt(string exe, string arguments = "", string workingDirectory = "")
{
_standardOutput = new StringBuilder();
_standardError = new StringBuilder();
_startInfo = new ProcessStartInfo()
{
FileName = exe,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
};
_process = new Process()
{
StartInfo = _startInfo,
EnableRaisingEvents = true,
};
_process.OutputDataReceived += _process_OutputDataReceived;
_process.ErrorDataReceived += _process_ErrorDataReceived;
_process.Exited += _process_Exited;
}
The constructor takes in the EXE to run, parameters to pass in and the working directory that the command should be run in. The constructor initializes
the private
members and hooks up event handlers for the Process
events. In order to redirect standard output and error to our program,
we need to set UseShellExecute
to true
.
Public Methods
public void Run(int timeOutInMilliseconds = NoTimeOut)
{
if (!IsRunning && !HasExited)
{
BeginRun();
if (timeOutInMilliseconds == NoTimeOut)
{
_process.WaitForExit();
}
else
{
_process.WaitForExit(timeOutInMilliseconds);
}
}
}
public void BeginRun()
{
if (!IsRunning && !HasExited)
{
if (_process.Start())
{
IsRunning = true;
ProcessId = _process.Id;
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
}
}
}
public void WriteToStandardInput(string command)
{
if (IsRunning && !HasExited)
{
_process.StandardInput.Write(command);
}
}
public void Kill(bool killChildProcesses = false)
{
if (killChildProcesses && ProcessId != 0)
{
KillChildProcesses(ProcessId);
}
else if (IsRunning && !HasExited)
{
_process.Kill();
}
}
The Run
and BeginRun
methods provide ways for the command to be run both synchronously and asynchronously.
When run synchronously, a timeout can be specified so that the caller is not blocked forever in case the command doesn't die.
When run asynchronously, the caller should subscribe to the Exited
event to determine when the command has finished running.
Processes starting using this class can be killed using the Kill
method. This can optionally kill any child processes that were started from the main process.
The WriteToStandardInput
method allows you to send a command to the standard input of the process.
For example, this can be useful if you started a command prompt using cmd.exe /k and want to run commands on it. By the way, if you want to run built-in shell commands like del
,
dir
, etc., you have to use cmd.exe as the exe name and '/c del ...' as the arguments. This is because the Process
class
doesn't know about the shell commands so it will throw an exception if you try to run them directly.
Private Methods
private void KillChildProcesses(int parentPid)
{
using (var searcher = new ManagementObjectSearcher(
"select ProcessId from Win32_Process where ParentProcessId=" + parentPid))
using (ManagementObjectCollection objCollection = searcher.Get())
{
foreach (ManagementObject obj in objCollection)
{
int pid = Convert.ToInt32(obj["ProcessID"]);
KillChildProcesses(pid);
}
}
try
{
Process.GetProcessById(parentPid).Kill();
}
catch (ArgumentException)
{
}
}
The only private
method here is a method that recursively kills child processes started from the main process.
Event Handlers
private void _process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
_standardOutput.AppendLine(e.Data);
OutputDataReceived(this, new DataEventArgs(e.Data));
}
private void _process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
_standardError.AppendLine(e.Data);
ErrorDataReceived(this, new DataEventArgs(e.Data));
}
private void _process_Exited(object sender, EventArgs e)
{
HasExited = true;
IsRunning = false;
ExitCode = _process.ExitCode;
Exited(this, e);
}
Finally, we declare the event handlers for the events of the underlying Process
object. In each case, we record the state internally and then forward
the events to the caller of the CommandPrompt
class.
Conclusion and Improvements
As you can see, the CommandPrompt
class is basically a simple wrapper around Process
. I'm sure there is more functionality that can be added
to it to make it even more useful and feature-rich. Is there any other functionality you would like to see in this class? Let me know
in the comments
History
- November 9, 2012
- Added the
WriteToStandardInput
and
KillChildProcesses
methods
- October 6, 2012