C# - 实时控制台输出重定向

时间:2010-12-21 16:30:13

标签: c# redirect console

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

5 个答案:

答案 0 :(得分:13)

我有一个与你描述的非常相似(可能是确切的)问题:

  1. 我需要将控制台更新异步传送给我。
  2. 无论是否输入了换行符,我都需要检测更新。
  3. 我最终做的是这样的:

    1. 开始调用StandardOutput.BaseStream.BeginRead
    2. 的“无休止”循环
    3. BeginRead的回调中,检查EndRead的返回值是否为0;这意味着控制台进程已关闭其输出流(即永远不会再向标准输出写入任何内容)。
    4. 由于BeginRead强制您使用常量长度缓冲区,因此请检查EndRead的返回值是否等于缓冲区大小。这意味着可能有更多的输出等待读取,并且可能希望(或甚至必要)将该输出全部处理成一个整体。我所做的是保持StringBuilder并附加到目前为止的输出读取。每当读出输出但其长度<1时。缓冲区长度,通知自己(我用事件做)有输出,将StringBuilder的内容发送给订阅者,然后清除它。
    5. 然而,在我的情况下,我只是在控制台的标准输出上写了更多东西。我不确定在你的情况下输出意味着什么“更新”。

      更新:我刚刚意识到(并没有解释你在做什么,这是一次很好的学习经历?)上面列出的逻辑有一个错误的错误:如果输出的长度由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 :(得分:9)

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

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

答案 2 :(得分:3)

根据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 :(得分:1)

斗争结束

由于上面的示例,我能够解决阻塞且无法直接使用的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();

答案 4 :(得分:0)

乔恩说“ 我不确定在您的情况下输出的“更新”是什么”,我也不知道这对他意味着什么。因此,我编写了一个程序,可用于重定向其输出,以便我们可以清楚地定义需求。

可以使用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的回答更好,但我想我还是会把它留在这里。