ProgramQueuer/ProgramQueuer/Queuer/ProcessIOManager.cs

342 lines
13 KiB
C#
Executable File
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;

/***************************************************************************************
* Author: Curt C.
* Email : harpyeaglecp@aol.com
*
* Project File: "Solving Problems of Monitoring Standard Output and Error Streams Of A Running Process"
* for http://www.codeproject.com
*
* This software is released under the Code Project Open License (CPOL)
*
* See official license description at: http://www.codeproject.com/info/cpol10.aspx
*
* This software is provided AS-IS without any warranty of any kind.
*
* >>> Please leave this header intact when using this file in other projects <<<
***************************************************************************************/
namespace ProgramQueuer.Queuer
{
// Delegate definition used for events to receive notification
// of text read from stdout or stderr of a running process.
public delegate void StringReadEventHandler(string text);
/// <summary>
/// Class that manages the reading of the output produced by a given 'Process'
/// and reports the output via events.
/// Both standard error (stderr) and standard output (stdout) are
/// managed and reported.
/// The stdout and stderr monitoring and reading are each performed
/// by separate background threads. Each thread blocks on a Read()
/// method, waiting for text in the stream being monitored to become available.
///
/// Note the Process.RedirectStandardOutput must be set to true in order
/// to read standard output from the process, and the Process.RedirectStandardError
/// must be set to true to read standard error.
/// </summary>
public class ProcessIOManager
{
#region Private_Fields
// Command line process that is executing and being
// monitored by this class for stdout/stderr output.
private Process runningProcess;
// Thread to monitor and read standard output (stdout)
private Thread stdoutThread;
// Thread to monitor and read standard error (stderr)
private Thread stderrThread;
// Buffer to hold characters read from either stdout, or stderr streams
private StringBuilder streambuffer;
#endregion
#region Public_Properties_And_Events
/// <summary>
/// Gets the process being monitored
/// </summary>
/// <value>The running process.</value>
public Process RunningProcess
{
get { return runningProcess; }
set { runningProcess = value; }
}
// Event to notify of a string read from stdout stream
public event StringReadEventHandler StdoutTextRead;
// Event to notify of a string read from stderr stream
public event StringReadEventHandler StderrTextRead;
#endregion
#region Constructor_And_Initialization
/// <summary>
/// Initializes a new instance of the <see cref="ProcessIOManager"/> class.
/// </summary>
/// <seealso cref="StartProcessOutputRead"/>
public ProcessIOManager()
{
this.streambuffer = new StringBuilder(256);
}
/// <summary>
/// Initializes a new instance of the <see cref="ProcessIOManager"/> class.
/// </summary>
/// <param name="process">The process.</param>
/// <remarks>
/// Does not automatically start listening for stdout/stderr.
/// Call StartProcessOutputRead() to begin listening for process output.
/// </remarks>
/// <seealso cref="StartProcessOutputRead"/>
public ProcessIOManager(Process process)
{
if (process == null)
throw new Exception("ProcessIoManager unable to set running process - null value is supplied");
if (process.HasExited == true)
throw new Exception("ProcessIoManager unable to set running process - the process has already existed.");
this.runningProcess = process;
this.streambuffer = new StringBuilder(256);
}
#endregion
#region Public_Methods
/// <summary>
/// Starts the background threads reading any output produced (standard output, standard error)
/// that is produces by the running process.
/// </summary>
public void StartProcessOutputRead()
{
// Just to make sure there aren't previous threads running.
StopMonitoringProcessOutput();
// Make sure we have a valid, running process
CheckForValidProcess("Unable to start monitoring process output.", true);
// If the stdout is redirected for the process, then start
// the stdout thread, which will manage the reading of stdout from the
// running process, and report text read via supplied events.
if (runningProcess.StartInfo.RedirectStandardOutput == true)
{
stdoutThread = new Thread(new ThreadStart(ReadStandardOutputThreadMethod));
// Make thread a background thread - if it was foreground, then
// the thread could hang up the process from exiting. Background
// threads will be forced to stop on main thread termination.
stdoutThread.IsBackground = true;
stdoutThread.Start();
}
// If the stderr is redirected for the process, then start
// the stderr thread, which will manage the reading of stderr from the
// running process, and report text read via supplied events.
if (runningProcess.StartInfo.RedirectStandardError == true)
{
stderrThread = new Thread(new ThreadStart(ReadStandardErrorThreadMethod));
stderrThread.IsBackground = true;
stderrThread.Start();
}
}
/// <summary>
/// Writes the supplied text string to the standard input (stdin) of the running process
/// </summary>
/// <remarks>In order to be able to write to the Process, the StartInfo.RedirectStandardInput must be set to true.</remarks>
/// <param name="text">The text to write to running process input stream.</param>
public void WriteStdin(string text)
{
// Make sure we have a valid, running process
CheckForValidProcess("Unable to write to process standard input.", true);
if (runningProcess.StartInfo.RedirectStandardInput == true)
runningProcess.StandardInput.WriteLine(text);
}
#endregion
#region Private_Methods
/// <summary>
/// Checks for valid (non-null Process), and optionally check to see if the process has exited.
/// Throws Exception if process is null, or if process has existed and checkForHasExited is true.
/// </summary>
/// <param name="errorMessageText">The error message text to display if an exception is thrown.</param>
/// <param name="checkForHasExited">if set to <c>true</c> [check if process has exited].</param>
private void CheckForValidProcess(string errorMessageText, bool checkForHasExited)
{
errorMessageText = (errorMessageText == null ? "" : errorMessageText.Trim());
if (runningProcess == null)
throw new Exception(errorMessageText + " (Running process must be available)");
if (checkForHasExited && runningProcess.HasExited)
throw new Exception(errorMessageText + " (Process has exited)");
}
/// <summary>
/// Read characters from the supplied stream, and accumulate them in the
/// 'streambuffer' variable until there are no more characters to read.
/// </summary>
/// <param name="firstCharRead">The first character that has already been read.</param>
/// <param name="streamReader">The stream reader to read text from.</param>
/// <param name="isstdout">if set to <c>true</c> the stream is assumed to be standard output, otherwise assumed to be standard error.</param>
private void ReadStream(int firstCharRead, StreamReader streamReader, bool isstdout)
{
// One of the streams (stdout, stderr) has characters ready to be written
// Flush the supplied stream until no more characters remain to be read.
// The synchronized/ locked section of code to prevent the other thread from
// reading its stream at the same time, producing intermixed stderr/stdout results.
// If the threads were not synchronized, the threads
// could read from both stream simultaneously, and jumble up the text with
// stderr and stdout text intermixed.
lock (this)
{
// Single character read from either stdout or stderr
int ch;
// Clear the stream buffer to hold the text to be read
streambuffer.Length = 0;
//Console.WriteLine("CHAR=" + firstCharRead);
streambuffer.Append((char)firstCharRead);
// While there are more characters to be read
while (streamReader.Peek() > -1)
{
// Read the character in the queue
ch = streamReader.Read();
if (ch != '\n' && streambuffer.Length > 0 && streambuffer[streambuffer.Length - 1] == '\r')
NotifyAndFlushBufferText(streambuffer, isstdout);
// Accumulate the characters read in the stream buffer
streambuffer.Append((char)ch);
// Send text one line at a time - much more efficient than
// one character at a time
if (ch == '\n')
NotifyAndFlushBufferText(streambuffer, isstdout);
}
// Flush any remaining text in the buffer
NotifyAndFlushBufferText(streambuffer, isstdout);
} // End lock()
}
/// <summary>
/// Invokes the OnStdoutTextRead (if isstdout==true)/ OnStderrTextRead events
/// with the supplied streambuilder 'textbuffer', then clears
/// textbuffer after event is invoked.
/// </summary>
/// <param name="textbuffer">The textbuffer containing the text string to pass to events.</param>
/// <param name="isstdout">if set to true, the stdout event is invoked, otherwise stedrr event is invoked.</param>
private void NotifyAndFlushBufferText(StringBuilder textbuffer, bool isstdout)
{
if (textbuffer.Length > 0)
{
try
{
if (isstdout == true && StdoutTextRead != null)
{ // Send notificatin of text read from stdout
StdoutTextRead(textbuffer.ToString());
}
else if (isstdout == false && StderrTextRead != null)
{ // Send notificatin of text read from stderr
StderrTextRead(textbuffer.ToString());
}
}
catch (Exception e)
{
}
// 'Clear' the text buffer
textbuffer.Length = 0;
}
}
/// <summary>
/// Method started in a background thread (stdoutThread) to manage the reading and reporting of
/// standard output text produced by the running process.
/// </summary>
private void ReadStandardOutputThreadMethod()
{
// Main entry point for thread - make sure the main entry method
// is surrounded with try catch, so an uncaught exception won't
// screw up the entire application
try
{
// character read from stdout
int ch;
// The Read() method will block until something is available
while (runningProcess != null && (ch = runningProcess.StandardOutput.Read()) > -1)
{
// a character has become available for reading
// block the other thread and process this stream's input.
ReadStream(ch, runningProcess.StandardOutput, true);
}
}
catch (Exception ex)
{
Console.WriteLine("ProcessIoManager.ReadStandardOutputThreadMethod()- Exception caught:" +
ex.Message + "\nStack Trace:" + ex.StackTrace);
}
}
/// <summary>
/// Method started in a background thread (stderrThread) to manage the reading and reporting of
/// standard error text produced by the running process.
/// </summary>
private void ReadStandardErrorThreadMethod()
{
try
{
// Character read from stderr
int ch;
// The Read() method will block until something is available
while (runningProcess != null && (ch = runningProcess.StandardError.Read()) > -1)
ReadStream(ch, runningProcess.StandardError, false);
}
catch (Exception ex)
{
Console.WriteLine("ProcessIoManager.ReadStandardErrorThreadMethod()- Exception caught:" +
ex.Message + "\nStack Trace:" + ex.StackTrace);
}
}
/// <summary>
/// Stops both the standard input and stardard error background reader threads (via the Abort() method)
/// </summary>
public void StopMonitoringProcessOutput()
{
// Stop the stdout reader thread
try
{
if (stdoutThread != null)
stdoutThread.Abort();
}
catch (Exception ex)
{
Console.WriteLine("ProcessIoManager.StopReadThreads()-Exception caught on stopping stdout thread.\n" +
"Exception Message:\n" + ex.Message + "\nStack Trace:\n" + ex.StackTrace);
}
// Stop the stderr reader thread
try
{
if (stderrThread != null)
stderrThread.Abort();
}
catch (Exception ex)
{
Console.WriteLine("ProcessIoManager.StopReadThreads()-Exception caught on stopping stderr thread.\n" +
"Exception Message:\n" + ex.Message + "\nStack Trace:\n" + ex.StackTrace);
}
stdoutThread = null;
stderrThread = null;
}
#endregion
}
}