Click here to Skip to main content
15,894,907 members
Articles / Programming Languages / C#

Solving Problems of Monitoring Standard Output and Error Streams of a Running Process

Rate me:
Please Sign up or sign in to vote.
4.91/5 (26 votes)
17 Mar 2011CPOL6 min read 97.1K   4.6K   47  
Introduces a class to help solve several problems of reading standard output/input of a running process.
using System;
using System.Diagnostics;
using System.Windows.Forms;
using ProcessReadWriteUtils;
 
/***************************************************************************************
*  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 CmdWindow
{
    /// <summary>
    /// TextBox subclass that uses a ProcessIoManager object to monitor the standard output
    /// and standard error produced by a running background, command-line process.  The control
    /// also allows the user to input text, and to send that text to the standard input of the 
    /// running process.
    /// </summary>
    public partial class CmdWindowBoxSync : TextBox
    {
        // Windows message codes to intercept keystrokes in the overridden WndProc() method
        private const int WM_CHAR = 0x102;
        private const int WM_KEYDOWN = 0x100;
        private const int WM_KEYUP = 0x101;

        // Index into the controls text where the user input starts
        //  (All prior text will be either stdout/stderr text from the running process)
        private int idxStartInputPosition;

        // Index into the controls text area where the user is currently typing.
        // This index will always be great than the idxStartInputPostion index.
        // The user input text will be from 'idxStartInputPosition' to 'idxCurrentInputPosition'
        private int idxCurrentInputPosition;

        // Process that is being executed and monitored for stdout/stdin
        //private Process process;

        // Class that asynchronously monitors the running process, and notifies this  
        // textbox control when stdout/stderr text is read.
        private ProcessIoManager processIoMgr;

        // Convenience array used to pass in parameters via the .Invoke() method
        private object[] singleArgArray;


        // Ignore stdout matching the last input string to running process stdin
        private bool ignoreOutputTextMatchingLastInput = true;

        // Last string that was sent to the standard input of the running process
        private string lastInputTextSentToProcess = "";

        // Delegate declaration, and delegate instance declarations to
        // allow the appending of text to the rich text box.
        // Note: these delegates are invoked (using the this.Invoke() method)
        // so that they run on the rich text box's gui thread.
        private delegate void PutTextCallBackDelegate(string text);
        private PutTextCallBackDelegate PutStdoutTextDelegate;
        private PutTextCallBackDelegate PutStderrTextDelegate;


        #region Public_Behavioral_Properties_And_Events

        /// <summary>
        /// Gets or sets a value indicating whether ignore the output text string that
        /// matches the last string sent to the running process' standard input stream.
        /// This can happen with some command (such as 'cmd.exe') that echos back to stdout
        /// the text that it received as a command on stdin.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if ignore standard output text matching last standard input; otherwise, <c>false</c>.
        /// </value>
        public bool IgnoreOutputTextMatchingLastInput
        {
            get { return ignoreOutputTextMatchingLastInput; }
            set { ignoreOutputTextMatchingLastInput = value; }
        }

        // Event to notify receiver of a string read from stdout stream
        public event StringReadEventHandler StdoutTextRead;

        // Event to notify receiver of a string read from stderr stream
        public event StringReadEventHandler StderrTextRead;

        #endregion

        #region Consructors_And_Initializaation

        /// <summary>
        /// Initializes a new instance of the <see cref="CmdWindowBoxSync"/> class.
        /// </summary>
        public CmdWindowBoxSync()
        {
            InitializeComponent();
            PutStdoutTextDelegate = new PutTextCallBackDelegate(AppendStdoutText);
            PutStderrTextDelegate = new PutTextCallBackDelegate(AppendStderrText);
            singleArgArray = new object[] { 0 };
            Multiline = true;
        }
        #endregion

        #region Process_Interaction

        /// <summary>
        /// Gets or sets the executing process for standard output/standard error
        /// monitoring.
        /// </summary>
        /// <value>The executing process.</value>
        public Process ExecutingProcess
        {
            get
            {
                if (processIoMgr == null)
                    return null;
                return processIoMgr.RunningProcess;
            }
            set
            {
                Clear();

                // Stop any background threads for reading
                if (processIoMgr != null)
                {            
                    // Stop background threads, etc.
                    processIoMgr.StopMonitoringProcessOutput();
                    processIoMgr = null;
                }
                
                if (value != null)
                {
                    // Create a new ProcessIoMgr object, pass in the supplied executing process, and set events to 
                    // receive notification of stdout and stderr text output from the process.
                    processIoMgr = new ProcessIoManager(value);
                    processIoMgr.StderrTextRead += new StringReadEventHandler(OnStderrTextRead);
                    processIoMgr.StdoutTextRead += new StringReadEventHandler(OnStdoutTextRead);
                    // Start the individual threads to monitor process text output
                    processIoMgr.StartProcessOutputRead();
                }
            }
        }

        /// <summary>
        /// Called when [stdout text read].
        /// </summary>
        /// <param name="text">The text.</param>
        private void OnStdoutTextRead(string text)
        {
            singleArgArray[0] = text;
            try
            {
                // Use this.Invoke() to call delegate to prevent cross
                // threading exceptions.
                // This method will put the supplied text into the
                // rich text box.
                this.Invoke(PutStdoutTextDelegate, singleArgArray);
            }
            catch (ObjectDisposedException)
            {
                // Can happen when the application shuts down, the windows
                // get destroyed, but another thread calls this method.
                // The attempt is then made to write to the window 
                // which as been destoryed.             
                processIoMgr.StopMonitoringProcessOutput();
            }
            catch (Exception ex)
            {
                Console.WriteLine("CmdWindowSync.OnStdoutTextRead() - EXCEPTION CAUGHT!\n" + ex.Message);
            }
        }

        /// <summary>
        /// Called when [stderr text read].
        /// </summary>
        /// <param name="text">The text.</param>
        private void OnStderrTextRead(string text)
        {
            singleArgArray[0] = text;
            try
            {
                this.Invoke(PutStderrTextDelegate, singleArgArray);
            }
            catch (ObjectDisposedException)
            {
                processIoMgr.StopMonitoringProcessOutput();
            }
            catch (Exception ex)
            {
                Console.WriteLine("CmdWindowSync.OnStderrTextRead() - EXCEPTION CAUGHT!\n" + ex.Message);
            }
        }
        #endregion

        #region Public_Properties

        /// <summary>
        /// Get the portion of text that is the user input text
        /// (Should be the text at the end of the base.Text string)
        /// </summary>
        public string InputText
        {
            get
            {
                string result = "";
                if (idxStartInputPosition >= 0 && idxCurrentInputPosition > idxStartInputPosition)
                    result = Text.Substring(idxStartInputPosition);
                return result;
            }
        }

        /// <summary>
        /// Gets the output text (without the InputText).
        /// </summary>
        /// <value>The output text.</value>
        public string OutputText
        {
            get
            {
                if (idxStartInputPosition > 0)
                    return base.Text.Substring(0, idxStartInputPosition);
                return base.Text;
            }
        }

        /// <summary>
        /// Gets or sets the current text in the <see cref="T:System.Windows.Forms.TextBox"/>.
        /// </summary>
        /// <value></value>
        /// <returns>The text displayed in the control.</returns>
        public override string Text
        {
            // Note: this property is called from base.Clear()
            get
            {
                // Still returns everything in text box 
                // (input/and output text)
                return base.Text;
            }
            set
            {
                // Replace the entire text - 
                // erase input text as well.
                ClearInputTextIndicies();
                base.Text = ScrubText(value);

                if (String.IsNullOrEmpty(value))
                    // The control is being cleared.
                    ZeroTextPositions();
            }
        }
        #endregion


        /// <summary>
        /// Clears all text from the text box control        
        /// </summary>
        public new void Clear()
        {
            ZeroTextPositions();
            base.Clear();
        }

        /// <summary>
        /// Writes the supplied text to the standard input of the running process' standard input stream.
        /// </summary>
        /// <param name="inputText">The input text string to write to the running process' standard input stream.</param>
        public void WriteTextToProcessStdin(string inputText)
        {
            if (processIoMgr != null && ExecutingProcess != null && ExecutingProcess.HasExited == false && String.IsNullOrEmpty(inputText) == false)
            {
                // Save the text that we're sending
                lastInputTextSentToProcess = inputText;

                // Adjust indexing positions                             
                idxStartInputPosition = GetStartOfInputIndex();
                idxCurrentInputPosition = idxStartInputPosition;
                SelectionStart = idxStartInputPosition;

                Console.WriteLine("CmdWindowBoxSync.SendInputTextToProcess()-Sending Text [" + inputText + "]");

                // Perform acutal stdin write
                processIoMgr.WriteStdin(inputText);
            }
        }

        /// <summary>
        /// The TextBox appears to eliminate the '\r' character
        /// when manipulating text, so just make sure to eliminate
        /// it before doing any other manipulation internally. 
        /// </summary>
        /// <param name="text">The text string to scrub.</param>
        /// <returns>Scrubbed text</returns>
        private string ScrubText(string text)
        {
            string result = text;
            if (result != null)
            {
                result = result.Replace("\r", "");
            }
            return result;
        }

        //public void AppendInputText(string inputText)
        //{
        //    inputText = ScrubText(Text);

        //    string newInputText = InputText + inputText;

        //    // Initialize starting input position if it is not already initialized
        //    if (idxStartInputPosition < 0)
        //        idxStartInputPosition = GetStartOfInputIndex();

        //    // Append the input text to the end of the text on screen
        //    base.Text += newInputText;

        //    // Update the current input text position
        //    idxCurrentInputPosition = idxStartInputPosition + inputText.Length;

        //    // Position the cursor at the end for the text
        //    SelectionStart = idxCurrentInputPosition;
        //    this.ScrollToCaret();
        //}


        /// <summary>
        /// Appends the given text (standard output text from running process), to the text displayed
        /// in this text box.
        /// </summary>
        /// <param name="outputText">The stdout output text of the running process.</param>
        private void AppendStdoutText(string outputText)
        {
            if (ignoreOutputTextMatchingLastInput == true && lastInputTextSentToProcess.Length > 0 && ScrubText(outputText).Trim() == ScrubText(lastInputTextSentToProcess).Trim())
            {
                // The running process has echoed the text that was it was last sent to it (which should be displaying
                // in the text box)
                // Since it is already displayed, then don't repeat the text.
                // The only thing to output is a newline character, to get the text box start displaying
                // subsequent text on the next line (if the text to display ends in a newline character)
                // (This behavior can be observed with the 'cmd.exe' command.
                Console.WriteLine("AppendStdoutText()- Ignoring last input text:" + ScrubText(lastInputTextSentToProcess));
                if (outputText.EndsWith("\n"))
                    AppendOutputText("\n", true);
            }
            else
            {
                AppendOutputText(outputText, true);
            }

            lastInputTextSentToProcess = "";
        }

        /// <summary>
        /// Appends the given text (standard error text from running process), to the text displayed
        /// in this text box.
        /// </summary>
        /// <param name="outputText">The stderr output text of the running process.</param>
        private void AppendStderrText(string outputText)
        {
            AppendOutputText(outputText, false);
        }


        /// <summary>
        /// Appends the output text of a running process, either stdout or stderr text,
        /// to the control.
        /// </summary>
        /// <param name="outputText">The output text from the running process.</param>
        /// <param name="isStdout">if set to <c>true</c> the text is stdout text, otherwise stderr text is assumed.</param>
        private void AppendOutputText(string outputText, bool isStdout)
        {
            if (String.IsNullOrEmpty(outputText) == false)
            {
                outputText = ScrubText(outputText);

                string currentOutputText = this.OutputText;
                string currentInputText = this.InputText;

                if (currentInputText.Length > 0)
                {
                    // Erase the input text: 
                    // This is done because it is too much overhead
                    // to do:  Text += currentOutputText + currentInputText
                    // Way too much flicker and performance degredation - better
                    // to do an append of the text we need to add
                    ClearInputTextIndicies();
                    Text = currentOutputText;
                }

                AppendText(outputText);
                idxStartInputPosition = GetStartOfInputIndex();
                idxCurrentInputPosition = idxStartInputPosition;
                SelectionStart = idxStartInputPosition;

                this.ScrollToCaret();

                // Notify via events of stream text read
                if (isStdout == true && StdoutTextRead != null)
                    StdoutTextRead(outputText);
                else if (isStdout == false && StderrTextRead != null)
                    StderrTextRead(outputText);
            }
        }

        /// <summary>
        /// Sets the SelectionStart, idxStartInputPosition, and idxCurrentInputPosition to zero.
        /// </summary>
        private void ZeroTextPositions()
        {
            SelectionStart = 0;
            idxStartInputPosition = 0;
            idxCurrentInputPosition = 0;
        }

        /// <summary>
        /// Sets idxStartInputPosition and idxCurrentInputPosition indicies to '-1'
        /// </summary>
        private void ClearInputTextIndicies()
        {
            idxStartInputPosition = -1;
            idxCurrentInputPosition = -1;
        }

        /// <summary>
        /// Returns the next position for the user to enter text.
        /// This value is the Text.Length value. The 'Text' being displayed is the
        /// stdout/stderr output of running processes.
        /// </summary>
        /// <returns>Next position in this textbox for user input</returns>
        private int GetStartOfInputIndex()
        {
            int result;
            result = Text.Length;
            return result;
        }

        #region KEYBOARD_HANDLING_METHODS_AND_OVERRIDES


        // true if the control key has been pressed (key down),
        // and false when it is released (key up)
        // Used for detecting CONTROL-C, or other combinations
        private bool controlKeyPressed;

        /// <summary>
        /// Override of base WndProc method, to custom handle
        /// WM_KEYDOWN, WM_CHAR, and WM_KEYUP messages.
        /// </summary>
        /// <param name="m">A Windows Message object.</param>
        protected override void WndProc(ref Message m)
        {
            bool handled = false;
            if (m.Msg == WM_KEYDOWN || m.Msg == WM_CHAR || m.Msg == WM_KEYUP)
            {
                if (ProcessKey((int)m.WParam, m.Msg) == false)
                    handled = true;
            }

            if (handled == true)
            {
                // Eat the message!
                m.Msg = 0;
                m.WParam = IntPtr.Zero;
                m.LParam = IntPtr.Zero;
            }
            else
            {
                base.WndProc(ref m);
            }
        }

        /// <summary>
        /// Figure out if the given key should be processed:
        /// Options:
        /// (1) Do not process the key, if an attempt in being made to modify
        /// the text prior to the set input position. (caret position is less
        /// than the set input position)
        /// (2) Allow processing of arrow keys, so user can move around the
        /// screen, regardless of the position
        /// (3) For BACKSPACE key, only allow a back space if the user is in the
        /// input portion of the text (i.e. - end of text) - caret position is
        /// less than or equal to the starting input position.
        /// </summary>
        /// <param name="keycode">The keycode.</param>
        /// <param name="msg">Message value from WndProc.</param>
        /// <returns>Returns true if the message should be handled by default processing, false if the message is locally handled</returns>
        private bool ProcessKey(int keycode, int msg)
        {
            bool handleKeyEvent = true;

            //Console.WriteLine("ProcessKeyDown: SelectionStart=" + SelectionStart + ", idxCurrent=" + idxCurrentInputPosition + ", idxStart=" + idxStartInputPosition);
            switch (keycode)
            {
                case (int)Keys.Left:
                case (int)Keys.Right:
                case (int)Keys.Down:
                case (int)Keys.Up:
                case (int)Keys.Home:
                case (int)Keys.End:
                case (int)Keys.PageDown:
                case (int)Keys.PageUp:
                    // Allow the movement keys around the box (no modifications to text for these)
                    break;

                case (int)Keys.ControlKey:
                    if (msg == WM_KEYUP)
                        controlKeyPressed = false;
                    else if (msg == WM_KEYDOWN)
                        controlKeyPressed = true;
                    break;

                case (int)Keys.Back:
                    if (SelectionStart <= idxStartInputPosition)
                    {
                        // Do not allow the backspace key to go beyond the
                        // input starting position and erase output text
                        handleKeyEvent = false;
                    }
                    break;

                case (int)Keys.C:
                case (int)Keys.A:
                    if (controlKeyPressed == true)
                    {
                        // Do nothing - Allow normal processing of a CONTROL-C (Copy),
                        // or a CONTROL-A (Select all)
                        Console.WriteLine("CONTROL-" + ((char)keycode).ToString().ToUpper() + " !!!");
                    }
                    else
                    {
                        // Perform the default processing for keys pressed.
                        // (See also default: section)
                        if (SelectionStart < idxStartInputPosition)
                            handleKeyEvent = false;
                    }
                    break;

                case (int)Keys.Enter: // (Same as 'Keys.Return')
                    // User has pressed enter/return - Send input text to the running process
                    if (msg == WM_KEYUP)
                    {
                        // This handling was done more for 'cmd.exe' than anything else.
                        // If a "\n" character is sent to cmd.exe, the that process 
                        // sends back two prompts, instead of the expected one prompt.
                        // Therefore - do not send a newline character to that process,
                        // but just send a blank.
                        //
                        // NOTE: This may have to be adjusted for other types of command line processes,
                        // and what the expect to receive (newlines, etc).
                        string textToSend = InputText;
                        if (InputText.Length <= 0)
                            textToSend = " ";
                        WriteTextToProcessStdin(textToSend);
                    }
                    handleKeyEvent = false;
                    break;

                default:
                    // For all other keys (should be input keys, etc),
                    // Make sure the the modification operation is not 
                    // prior to the input starting position.
                    if (SelectionStart < idxStartInputPosition)
                        handleKeyEvent = false;
                    break;
            }
            return handleKeyEvent;
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.KeyDown"/> event.
        /// </summary>
        /// <param name="e">A <see cref="T:System.Windows.Forms.KeyEventArgs"/> that contains the event data.</param>
        protected override void OnKeyDown(KeyEventArgs e)
        {
            //Console.WriteLine("OnKeyDown");
            //Console.WriteLine("SelectionStart=" + SelectionStart + ", idxCurrent=" + idxCurrentInputPosition + ", idxStart=" + idxStartInputPosition);
            base.OnKeyDown(e);
            idxCurrentInputPosition = SelectionStart;
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.KeyUp"/> event.
        /// </summary>
        /// <param name="e">A <see cref="T:System.Windows.Forms.KeyEventArgs"/> that contains the event data.</param>
        protected override void OnKeyUp(KeyEventArgs e)
        {
            //Console.WriteLine("OnKeyUp");
            //Console.WriteLine("SelectionStart=" + SelectionStart + ", idxCurrent=" + idxCurrent + ", idxStart=" + idxStart);
            base.OnKeyUp(e);
            idxCurrentInputPosition = SelectionStart;
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.KeyPress"/> event.
        /// </summary>
        /// <param name="e">A <see cref="T:System.Windows.Forms.KeyPressEventArgs"/> that contains the event data.</param>
        protected override void OnKeyPress(KeyPressEventArgs e)
        {
            //Console.WriteLine("OnKeyPress");
            //Console.WriteLine("SelectionStart=" + SelectionStart + ", idxCurrent=" + idxCurrentInputPosition + ", idxStart=" + idxStartInputPosition);
            base.OnKeyPress(e);
            idxCurrentInputPosition = SelectionStart;
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Senior Software Developer with background history in developing software for both Unix and Windows based systems using a variety of languages and technologies, including: C#, C++, C, and Java.

Comments and Discussions