【问题标题】:C# - Realtime console output redirectionC# - 实时控制台输出重定向
【发布时间】:2011-05-28 22:35:14
【问题描述】:

我正在开发一个 C# 应用程序,我需要启动一个外部 控制台 程序来执行一些任务(提取文件)。我需要做的是重定向控制台程序的输出。像this one 这样的代码不起作用,因为它仅在控制台程序中写入新行时引发事件,但我使用“更新”控制台窗口中显示的内容,而不写入任何新行。每次更新控制台中的文本时如何引发事件?还是每隔 X 秒获取一次控制台程序的输出?提前致谢!

【问题讨论】:

    标签: c# redirect console


    【解决方案1】:

    我遇到了与您描述的非常相似(可能完全一样)的问题:

    1. 我需要异步发送控制台更新。
    2. 无论是否输入了换行符,我都需要检测更新。

    我最终做的事情是这样的:

    1. 开始调用StandardOutput.BaseStream.BeginRead 的“无休止”循环。
    2. BeginRead的回调中,检查EndRead的返回值是否为0;这意味着控制台进程已关闭其输出流(即永远不会再向标准输出写入任何内容)。
    3. 由于BeginRead 强制您使用恒定长度的缓冲区,请检查EndRead 的返回值是否等于缓冲区大小。这意味着可能有更多的输出等待读取,并且可能希望(甚至有必要)将此输出全部处理为一体。我所做的是保留StringBuilder 并附加到目前为止读取的输出。每当读取输出但其长度小于缓冲区长度时,通知自己(我通过事件执行此操作)有输出,将StringBuilder 的内容发送给订阅者,然后将其清除。

    然而,就我而言,我只是将更多内容写入控制台的标准输出。我不确定在您的情况下“更新”输出意味着什么。

    更新:我刚刚意识到(不是在解释你在做什么很好的学习体验吗?)上面概述的逻辑有一个错误:如果输出的长度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();
    

    【讨论】:

    • 通过更新我的意思是:控制台程序有一行说:“提取文件:1”。然后数字变为 2、3、4 等(不写任何新行)。您还可以发布您的代码示例吗? (我不是专家,我可能会感到困惑)。
    • 非常感谢!我会尽快尝试。
    【解决方案2】:

    根据keep it simple的原则,我发布了更紧凑的代码。

    在我看来,在这种情况下阅读就足够了。

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

    兴趣点:

    • 更改读取缓冲区大小
    • 做一堂好课
    • 让活动更精彩
    • 在另一个线程中启动进程(这样ui线程不会被Process.WaitForExit阻塞)

    【讨论】:

      【解决方案3】:

      或者,根据保持理智的原则,您可以阅读文档并正确执行:

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

      【讨论】:

      • 问题在于 ReadLine()、ReadToend()、Peek() 以及任何您尝试使用锁且不返回文档中描述的内容。可以在文件流和内存流上工作,但当您附加到控制台应用程序的流阅读器时效果不是很好。如果您想从控制台捕获原始数据,最好获取所有换行符。
      • 你失去了新的行字符!
      • 更糟糕的是:ReadLine() 有时会阻塞,有时如果没有数据,它会返回 null。废话!
      • 它应该阻止!
      【解决方案4】:

      斗争结束了

      感谢上述示例,我能够解决 StandardOutput 和 StandardError 流读取器阻塞且无法直接使用的问题。

      MS 在这里承认了锁定问题: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();
              }
      
      
          }
      

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

      【讨论】:

      • 您能否更清楚地解释一下“但我错过了换行符”的意思???
      • 看看我发布的内容。使用二进制更容易。对吗?
      【解决方案5】:

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

      实际上,这可能并不比 Marchewek 的答案更好,但我想我还是会把它留在这里。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-09-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-04-23
        • 2013-11-14
        相关资源
        最近更新 更多