C# – 实时控制台输出重定向
我正在开发一个C#应用程序,我需要启动一个外部控制台程序来执行一些任务(提取文件)。 我需要做的是重定向控制台程序的输出。 像这样的代码不起作用,因为它仅在控制台程序中写入新行时引发事件,但是我使用的那个“更新”控制台窗口中显示的内容,而不写任何新行。 每次更新控制台中的文本时,如何引发事件? 或者每隔X秒获取一次控制台程序的输出? 提前致谢!
我有一个与你描述的非常相似(可能是确切的)问题:
- 我需要将控制台更新异步传递给我。
- 无论是否输入了换行符,我都需要检测更新。
我最终做的是这样的:
- 启动一个调用
StandardOutput.BaseStream.BeginRead
的“无限”循环。 - 在
BeginRead
的回调中,检查EndRead
的返回值是否为0
; 这意味着控制台进程已关闭其输出流(即永远不会再向标准输出写入任何内容)。 - 由于
BeginRead
强制您使用常量长度缓冲区,因此请检查EndRead
的返回值是否等于缓冲区大小。 这意味着可能有更多的输出等待读取,并且可能希望(或甚至必要)将该输出全部处理成一个整体。 我所做的是保持一个StringBuilder
并附加到目前为止的输出读取。 每当读取输出但其长度为<缓冲区长度时,请通知自己(我用事件做)有输出,将StringBuilder
的内容发送给订阅者,然后清除它。
但是 ,就我而言,我只是在控制台的标准输出上写了更多内容。 我不确定在你的情况下“更新”输出意味着什么。
更新:我刚刚意识到(并没有解释你在做什么,这是一次很好的学习经历?)上面列出的逻辑有一个错误的错误:如果BeginRead
读取的输出长度恰好等于长度你的缓冲区,然后这个逻辑将输出存储在StringBuilder
并阻塞,同时试图查看是否有更多的输出要追加。 只有当有更多输出可用时,“当前”输出才会发回给您,作为较大字符串的一部分。
显然,为了100%正确地做到这一点,需要一些防范这种方法(或者是一个巨大的缓冲加上对你的运气能力的信念)。
更新2(代码):
免责声明:此代码不适合生产。 这是我快速将概念validation解决方案拼凑在一起以完成需要完成的工作的结果。 请不要在生产应用程序中使用它。 如果这段代码会让你发生可怕的事情,我会假装其他人写了它。
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 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 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(); } }
像这样使用:
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();
或者,根据保持理智的原则,您可以阅读文档并正确执行:
var startinfo = new ProcessStartInfo(@".\consoleapp.exe") { CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, }; var process = new Process { StartInfo = startinfo }; process.Start(); var reader = process.StandardOutput; while (!reader.EndOfStream) { // the point is that the stream does not end until the process has // finished all of its output. var nextLine = reader.ReadLine(); } process.WaitForExit();
根据保持简单的原则我发布更紧凑的代码。
在我看来,在这种情况下,阅读就够了。
private delegate void DataRead(string data); private static event DataRead OnDataRead; static void Main(string[] args) { OnDataRead += data => Console.WriteLine(data != null ? data : "Program finished"); Thread readingThread = new Thread(Read); ProcessStartInfo info = new ProcessStartInfo() { FileName = Environment.GetCommandLineArgs()[0], Arguments = "/arg1 arg2", RedirectStandardOutput = true, UseShellExecute = false, }; using (Process process = Process.Start(info)) { readingThread.Start(process); process.WaitForExit(); } readingThread.Join(); } private static void Read(object parameter) { Process process = parameter as Process; char[] buffer = new char[Console.BufferWidth]; int read = 1; while (read > 0) { read = process.StandardOutput.Read(buffer, 0, buffer.Length); string data = read > 0 ? new string(buffer, 0, read) : null; if (OnDataRead != null) OnDataRead(data); } }
兴趣点:
- 改变读缓冲区大小
- 做一个好class级
- 做出更好的事件
- 在另一个线程中启动进程(这样ui线程不会被
Process.WaitForExit
阻塞)
斗争结束了
由于上面的示例,我能够解决StandardOutput和StandardError流读取器阻塞而无法直接使用的问题。
MS在这里承认锁定问题: system.io.stream.beginread
使用process.BeginOutputReadLine()和process.BeginErrorReadLine()订阅StandardOutput和StandardError事件以及对OutputDataReceived和ErrorDataReceived的订阅工作正常但我错过了换行符并且无法模拟正在收听的原始控制台上发生的事情。
此类接受StreamReader的引用,但捕获StreamReader.BaseStream的控制台输出。 DataReceived事件将在到达时永久提供流数据。 在外国控制台应用程序上测试时不阻塞。
/// /// Stream reader for StandardOutput and StandardError stream readers /// Runs an eternal BeginRead loop on the underlaying stream bypassing the stream reader. /// /// The TextReceived sends data received on the stream in non delimited chunks. Event subscriber can /// then split on newline characters etc as desired. /// class AsyncStreamReader { public delegate void EventHandler(object sender, string Data); public event EventHandler DataReceived; protected readonly byte[] buffer = new byte[4096]; private StreamReader reader; /// /// If AsyncStreamReader is active /// public bool Active { get; private set; } public void Start() { if (!Active) { Active = true; BeginReadAsync(); } } public void Stop() { Active=false; } public AsyncStreamReader(StreamReader readerToBypass) { this.reader = readerToBypass; this.Active = false; } protected void BeginReadAsync() { if (this.Active) { reader.BaseStream.BeginRead(this.buffer, 0, this.buffer.Length, new AsyncCallback(ReadCallback), null); } } private void ReadCallback(IAsyncResult asyncResult) { var bytesRead = reader.BaseStream.EndRead(asyncResult); string data = null; //Terminate async processing if callback has no bytes if (bytesRead > 0) { data = reader.CurrentEncoding.GetString(this.buffer, 0, bytesRead); } else { //callback without data - stop async this.Active = false; } //Send data to event subscriber - null if no longer active if (this.DataReceived != null) { this.DataReceived.Invoke(this, data); } //Wait for more data from stream this.BeginReadAsync(); } }
当AsyncCallback退出而不是发送空字符串时,可能是一个显式事件,这很好但是基本问题已经解决了。
4096大小的缓冲区可能更小。 回调将循环,直到所有数据都被提供。
使用这样:
standardOutput = new AsyncStreamReader(process.StandardOutput); standardError = new AsyncStreamReader(process.StandardError); standardOutput.DataReceived += (sender, data) => { //Code here }; standardError.DataReceived += (sender, data) => { //Code here }; StandardOutput.Start(); StandardError.Start();