我正在开发一个C#应用程序,需要启动外部的控制台程序来执行一些任务(提取文件)。我需要做的是重定向控制台程序的输出。像这样的代码不起作用,因为它只在控制台程序中写入新行时才引发事件,但我使用的程序“更新”了控制台窗口中显示的内容,而没有写入任何新行。如何在每次更新控制台文本时引发事件?或者每X秒获取一次控制台程序的输出?谢谢!
我遇到了一个非常类似(可能是完全相同的)你所描述问题:
我最终做的事情如下:
StandardOutput.BaseStream.BeginRead
。BeginRead
的回调函数中,检查 EndRead
的返回值是否为 0;这意味着控制台进程已关闭其输出流(即不会再向标准输出写入任何内容)。BeginRead
强制您使用具有恒定长度的缓冲区,因此检查 EndRead
的返回值是否等于缓冲区大小。这意味着可能有更多的输出正在等待读取,而且可能需要(甚至是必要的)将此输出全部处理一次。我所做的是保留一个 StringBuilder
并将已经读取的输出附加到其中。每当读取输出但其长度小于缓冲区长度时,通知自己(我使用事件进行通知),告诉订阅者发送 StringBuilder
的内容,然后清除它。然而,在我的情况下,我只是将更多的东西写到控制台的标准输出中。我不确定在你的情况下“更新”输出是什么意思。
更新:我刚意识到(解释你正在做什么是一次很好的学习体验吗?)上面概述的逻辑存在一个错位一的 bug:如果由 BeginRead
读取的输出长度恰好等于您的缓冲区长度,则该逻辑将把输出存储在 StringBuilder
中并在尝试查看是否有更多输出可附加时发生阻塞。当前的输出只有在/如果有更多的输出可用时作为较大字符串的一部分返回给您。
显然需要某种方法来防范这种情况(或者使用较大的缓冲区加信任自己的运气)才能以 100% 的正确性完成此操作。
更新 2(代码):
免责声明: 此代码不适用于生产环境。它是我快速编写的概念验证解决方案的结果。请不要将其原样用于您的生产应用程序中。如果此代码对您造成了可怕的影响,我将会假装别人写了它。
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();
}
}
使用方法如下:
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);
}
}
注意事项:
Process.WaitForExit
阻塞)斗争结束了
由于上述示例,我解决了使用StandardOutput和StandardError流阅读器时的阻塞和无法直接使用的问题。
微软在这里承认了锁定问题:system.io.stream.beginread
使用process.BeginOutputReadLine()和process.BeginErrorReadLine()订阅StandardOutput和StandardError事件,并订阅OutputDataReceived和ErrorDataReceived可以正常工作,但我错过了换行符并且无法模拟正在被监听的原始控制台上发生的情况。
此类接受StreamReader的引用,但从StreamReader.BaseStream中捕获控制台输出。 DataReceived
事件将永远传递流数据,因为它到达。在外部控制台应用程序上测试时不会阻塞。
/// <summary>
/// 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.
/// </summary>
class AsyncStreamReader
{
public delegate void EventHandler<args>(object sender, string Data);
public event EventHandler<string> DataReceived;
protected readonly byte[] buffer = new byte[4096];
private StreamReader reader;
/// <summary>
/// If AsyncStreamReader is active
/// </summary>
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();
}
}
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();
Jon说:“我不确定在你的情况下“更新”输出意味着什么”,我也不知道他是什么意思。因此,我编写了一个程序,可以用于重定向其输出,以便我们可以清楚地定义要求。
可以使用Console.CursorLeft Property在控制台中移动光标。但是,当我使用它时,无法重定向输出,我收到了一个错误消息;大概是关于无效流的问题。然后我尝试了退格字符,就像已经建议的那样。因此,我正在使用以下程序来重定向输出。
class Program
{
static readonly string[] Days = new [] {"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"};
static int lastlength = 0;
static int pos = 0;
static void Main(string[] args)
{
Console.Write("Status: ");
pos = Console.CursorLeft;
foreach (string Day in Days)
{
Update(Day);
}
Console.WriteLine("\r\nDone");
}
private static void Update(string day)
{
lastlength = Console.CursorLeft - pos;
Console.Write(new string((char)8, lastlength));
Console.Write(day.PadRight(lastlength));
Thread.Sleep(1000);
}
}
class Program
{
static Stream BinaryStdOut = null;
static void Main(string[] args)
{
const string TheProgram = @" ... ";
ProcessStartInfo info = new ProcessStartInfo(TheProgram);
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
Console.WriteLine($"Started process {p.Id} {p.ProcessName}");
BinaryStdOut = p.StandardOutput.BaseStream;
string Message = null;
while ((Message = GetMessage()) != null)
Console.WriteLine(Message);
p.WaitForExit();
Console.WriteLine("Done");
}
static string GetMessage()
{
byte[] Buffer = new byte[80];
int sizeread = BinaryStdOut.Read(Buffer, 0, Buffer.Length);
if (sizeread == 0)
return null;
return Encoding.UTF8.GetString(Buffer);
}
}