С# - перенаправление на консоль в реальном времени

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

Ответ 1

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

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

То, что я закончил, выглядит следующим образом:

  • Запустите "бесконечный" цикл вызова StandardOutput.BaseStream.BeginRead.
  • В обратном вызове BeginRead проверьте, является ли возвращаемое значение EndRead 0; это означает, что консольный процесс закрыл свой выходной поток (т.е. никогда больше ничего не напишет на стандартный вывод).
  • Так как 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();

Ответ 2

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

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

В соответствии с сохранить это просто принцип Я отправляю более компактный код.

По-моему, 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);
        }
    }

Достопримечательности:

  • изменение размера буфера чтения
  • создание приятного класса
  • создание более приятного события
  • запуск в другом потоке (так что нить ui не заблокирована Process.WaitForExit)

Ответ 4

Борьба завершена

Благодаря вышеприведенным образцам я смог решить проблемы с блокировщиками потоков 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();

Ответ 5

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

Переместить курсор в консоли можно с помощью свойства 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, но я полагаю, что все равно оставлю это здесь.