C # - перенаправление вывода консоли в реальном времени - PullRequest
15 голосов
/ 21 декабря 2010

Я занимаюсь разработкой приложения на C #, и мне нужно запустить внешнюю консольную программу для выполнения некоторых задач (извлечения файлов).Что мне нужно сделать, это перенаправить вывод консольной программы.Код типа этот не работает, потому что он вызывает события, только когда в консольной программе записывается новая строка, а тот, который я использую, «обновляет» то, что отображается в окне консоли, без записи новых строк,Как я могу вызвать событие каждый раз, когда текст в консоли обновляется?Или просто получать вывод консольной программы каждые X секунд?Заранее спасибо!

Ответы [ 5 ]

13 голосов
/ 21 декабря 2010

У меня была очень похожая (возможно, точная) проблема, как вы описываете:

  1. Мне нужно, чтобы обновления консоли доставлялись мне асинхронно.
  2. Мне нужно, чтобы обновления были обнаружены независимо от того, был ли введен символ новой строки.

То, что я закончил, выглядит так:

  1. Запустить «бесконечный» цикл вызова StandardOutput.BaseStream.BeginRead.
  2. В обратном вызове для BeginRead проверьте, является ли возвращаемое значение EndRead равным 0; это означает, что консольный процесс закрыл свой выходной поток (то есть никогда больше не будет записывать что-либо в стандартный вывод).
  3. Поскольку BeginRead заставляет вас использовать буфер постоянной длины, проверьте, равно ли возвращаемое значение EndRead размеру буфера. Это означает, что может быть больше выходных данных, ожидающих чтения, и может быть желательно (или даже необходимо), чтобы эти выходные данные обрабатывались целиком. Что я сделал, так это оставил StringBuilder и добавил результат, прочитанный до сих пор. Всякий раз, когда вывод читается, но его длина <длина буфера, сообщите себе (я делаю это с событием), что есть вывод, отправьте содержимое <code>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();
9 голосов
/ 27 августа 2014

Или, в качестве альтернативы, в соответствии с принципом «оставайся в здравом уме», ты можешь прочитать документацию и сделать это правильно:

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();
3 голосов
/ 13 декабря 2012

Согласно принципу простоты Я публикую более компактный код.

На мой взгляд, в данном случае достаточно Read.

    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)
1 голос
/ 01 ноября 2016

Борьба окончена

Благодаря приведенным выше примерам мне удалось решить проблемы с блокировкой потоковых ридеров StandardOutput и StandardError, которую невозможно использовать напрямую.

MS признает здесь о проблемах блокировки: system.io.stream.beginread

Подписка на события StandardOutput и StandardError с использованием process.BeginOutputReadLine () и process.BeginErrorReadLine () и подписка на 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();
0 голосов
/ 16 января 2019

Джон сказал: " Я не уверен, что означает" обновление "вывода в вашем случае ", и я тоже не знаю, что это значит для него.Поэтому я написал программу, которую можно использовать для перенаправления ее вывода, чтобы мы могли четко определить требования.

Можно перемещать курсор в консоли, используя свойство Console.CursorLeft ,Однако, когда я использовал это, я не смог перенаправить вывод, я получил ошибку;что-то о недопустимом потоке, я думаю.Тогда я попробовал символы возврата, как уже было предложено.Таким образом, программа, которую я использую для перенаправления вывода, выглядит следующим образом.

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, но я полагаю, что все равно оставлю это здесь.

...