Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

redirect - C# - Realtime console output redirection

I'm developing a C# application and I need to start an external console program to perform some tasks (extract files). What I need to do is to redirect the output of the console program. Code like this one does not work, because it raises events only when a new line is writen in the console program, but the one I use "updates" what's shown in the console window, without writting any new lines. How can I raise an event every time the text in the console is updated? Or just get the output of the console program every X seconds? Thanks in advance!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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();

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

1.4m articles

1.4m replys

5 comments

57.0k users

...