C# – Realtime console output redirection

I have had a very similar (possibly the exact) problem as you describe:

  1. I needed the console updates to be delivered to me asynchronously.
  2. I needed the updates to be detected regardless of whether a newline was input.

What I ended up doing goes like this:

  1. Start an “endless” loop of calling StandardOutput.BaseStream.BeginRead.
  2. In the callback for BeginRead, check if the return value of EndRead is 0; this means that the console process has closed its output stream (i.e. will never write anything to standard output again).
  3. Since BeginRead forces you to use a constant-length buffer, check if the return value of EndRead is equal to the buffer size. This means that there may be more output waiting to be read, and it may be desirable (or even necessary) that this output is processed all in one piece. What I did was keep a StringBuilder around and append the output read so far. Whenever output is read but its length is < the buffer length, notify yourself (I do it with an event) that there is output, send the contents of the StringBuilder to the subscriber, and then clear it.

However, in my case I was simply writing more stuff to the console’s standard output. I ‘m not sure what “updating” the output means in your case.

Update: I just realized (isn’t explaining what you are doing a great learning experience?) that the logic outlined above has an off-by-one bug: If the length of the output read by BeginRead is exactly equal to the length of your buffer, then this logic will store the output in the StringBuilder and block while trying to see if there’s more output to append. The “current” output will only be sent back to you when/if more output is available, as part of a larger string.

Obviously some method of guarding against this (or a biggish buffer plus faith in your powers of luck) is needed to do this 100% correctly.

Update 2 (code):

DISCLAIMER:
This code is not production-ready. It is the result of me quickly hacking together a proof of concept solution to do what needed to be done. Please do not use it as it stands in your production application. If this code causes horrible things to happen to you, I will pretend someone else wrote it.

public class ConsoleInputReadEventArgs : EventArgs
{
    public ConsoleInputReadEventArgs(string input)
    {
        this.Input = input;
    }

    public string Input { get; private set; }
}

public interface IConsoleAutomator
{
    StreamWriter StandardInput { get; }

    event EventHandler<ConsoleInputReadEventArgs> StandardInputRead;
}

public abstract class ConsoleAutomatorBase : IConsoleAutomator
{
    protected readonly StringBuilder inputAccumulator = new StringBuilder();

    protected readonly byte[] buffer = new byte[256];

    protected volatile bool stopAutomation;

    public StreamWriter StandardInput { get; protected set; }

    protected StreamReader StandardOutput { get; set; }

    protected StreamReader StandardError { get; set; }

    public event EventHandler<ConsoleInputReadEventArgs> StandardInputRead;

    protected void BeginReadAsync()
    {
        if (!this.stopAutomation) {
            this.StandardOutput.BaseStream.BeginRead(this.buffer, 0, this.buffer.Length, this.ReadHappened, null);
        }
    }

    protected virtual void OnAutomationStopped()
    {
        this.stopAutomation = true;
        this.StandardOutput.DiscardBufferedData();
    }

    private void ReadHappened(IAsyncResult asyncResult)
    {
        var bytesRead = this.StandardOutput.BaseStream.EndRead(asyncResult);
        if (bytesRead == 0) {
            this.OnAutomationStopped();
            return;
        }

        var input = this.StandardOutput.CurrentEncoding.GetString(this.buffer, 0, bytesRead);
        this.inputAccumulator.Append(input);

        if (bytesRead < this.buffer.Length) {
            this.OnInputRead(this.inputAccumulator.ToString());
        }

        this.BeginReadAsync();
    }

    private void OnInputRead(string input)
    {
        var handler = this.StandardInputRead;
        if (handler == null) {
            return;
        }

        handler(this, new ConsoleInputReadEventArgs(input));
        this.inputAccumulator.Clear();
    }
}

public class ConsoleAutomator : ConsoleAutomatorBase, IConsoleAutomator
{
    public ConsoleAutomator(StreamWriter standardInput, StreamReader standardOutput)
    {
        this.StandardInput = standardInput;
        this.StandardOutput = standardOutput;
    }

    public void StartAutomate()
    {
        this.stopAutomation = false;
        this.BeginReadAsync();
    }

    public void StopAutomation()
    {
        this.OnAutomationStopped();
    }
}

Used like so:

var processStartInfo = new ProcessStartInfo
    {
        FileName = "myprocess.exe",
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        UseShellExecute = false,
    };

var process = Process.Start(processStartInfo);
var automator = new ConsoleAutomator(process.StandardInput, process.StandardOutput);

// AutomatorStandardInputRead is your event handler
automator.StandardInputRead += AutomatorStandardInputRead;
automator.StartAutomate();

// do whatever you want while that process is running
process.WaitForExit();
automator.StandardInputRead -= AutomatorStandardInputRead;
process.Close();

Leave a Comment