MrFinch Ответов: 3

C# - Асинхронное Чтение Текста Из Окна Консоли


Привет!
У меня есть приложение C#, которое взаимодействует с консольным окном (через SetForegroundWindow и sendKeys). Это отлично работает, и я использую его для отправки команд в окно консоли (я также запускаю другую программу DOS внутри командной строки).

Проблема в том, что я хотел бы прочитать Результат после каждой команды!
Я пытался перенаправить стандартный вывод, но это не работает, так как мне нужно делать это с открытым окном в течение длительного времени.

Может ли кто-нибудь указать мне правильное направление? Можно ли этого вообще добиться?
Я подозреваю, что это можно сделать с помощью некоторых вызовов API, но это выходит за рамки моих текущих знаний.

С уважением
/Кипарисы

Andy Lanng

Значит окна должны быть открыты? разве вы не можете запустить команду cmd в коде, чтобы терминал не подключался?

Matbe это linux-сторона моего выхода, но я думаю, что это гораздо проще, если терминал не используется. Во всяком случае - на этот случай я отвечу ниже

MrFinch

Привет, Энди!
Спасибо за быстрый ответ. Ну, я начинаю "другую" программу с самого начала. cmd.exe команда и от этого мне нужно отправлять и получать данные. Я не совсем понимаю, что ты имеешь в виду, говоря, что не пользуешься терминалом. Не могли бы вы объяснить, пожалуйста! Боюсь, что Linux здесь не подходит.

Andy Lanng

Это только терминология терминала linux - я объясняю немного лучше в своем ответе. Дайте мне знать, если вы нуждаетесь в разъяснении, но, похоже, ответ соответствует вашим потребностям :)

MrFinch

Еще раз привет!
Спасибо, что помог мне. Это почти работает, мне пришлось сделать эти модификации:
startInfo.RedirectStandardOutput = true;// false;
startInfo.RedirectStandardError = true; // false;
startInfo.RedirectStandardInput = true; // new

Но я могу запустить приложение только один раз с помощью одной команды (процесс, похоже, закрывается сразу после этого). Мне нужно уметь посылать несколько команд и читать несколько ответов. Я попытался разорвать этот процесс и начать делать их публичными, но это не помогло.

Что Касается Сайпрес

Andy Lanng

ах, тогда вам нужно будет передать буферы потоком. Это позволит сохранить процесс открытым. Я обновлю информацию с ответом.
извините за редиректы - мой код делает что - то немного другое- вы правы

Andy Lanng

С другой стороны: вы уверены, что процесс ждет? Разве вы не можете просто начать процесс несколько раз?
Могу я посмотреть ваши команды, пожалуйста ^_^

MrFinch

Еще раз привет!
Мои команды выглядят примерно так. RunExternalApplication.exe, listUnit, ReadCommand, WriteCommand и т. д.
После каждой отправленной команды приложение возвращается с информацией об успехе и фактическими данными (вот почему мне нужно держать "линию" открытой).

Нет, я не могу (или, скорее, не хочу) входить в систему каждый раз, так как для завершения каждой процедуры входа в систему может потребоваться до одной минуты (глупая система, которая взаимодействует с другим оборудованием).
Поэтому, если это возможно, однократный вход в систему, сопровождаемый несколькими командами, имеет большое значение.

Тем не менее, спасибо за разъяснение того, как работает перенаправление, очень ценю.

С уважением
Кипарисы

Andy Lanng

Хм, ладно. процесс.WaitForExit();" будет ждать, пока мы не закроем трубы, которые мы читаем. Я опубликую второй ответ с асинхронным материалом.

Я не буду делать это слишком кричащим - вы можете это сделать ;)

PS: один из способов сделать его очень ярким-это использовать Observable.FromEvent. Загляни в него ^_^

Andy Lanng

omg - первый раз, когда я был в топ-5! и ты забрал все это #плач-теперь #justkidding

Я вернусь туда, если вы примете следующее решение ^_^

MrFinch

Ух ты!
Я впечатлен всеми усилиями, которые вы вложили в это дело.
Я должен признать, что ты не в моей лиге, и мне нужно кое-что наверстать. ;)

Я был вынужден сделать некоторые небольшие корректировки, чтобы сделать ваш код компилируемым, и, возможно, я все испортил (я уверен, что ошибки на моей стороне):
Я заменил выходной очереди 'с 'Выход', подтягивались _process.Start() перед '_inputStreamWriter = _process.StandardInput;' строка.

Процесс все еще "кажется" закрытым, потому что я читаю действительные данные только в первый раз, и независимо от того, сколько раз я запускаю функцию SendCommand, она, кажется, не "кусается". Я попытался отладить, но так и не нашел, в чем именно заключается проблема. Когда я отлаживаю, я получаю здесь исключение:
если (args.Data == null)
{
//труба закрылась, то есть процесс закончился
_waitHandles[1].Set();
}

И чтобы избавиться от этого мне нужно было добавить вот это:
_waitHandles[1] = новый ManualResetEvent(false);

Не поймите меня неправильно, я действительно ценю вашу помощь, я просто пытаюсь объяснить свою точку зрения. Я уверен, что проблема где-то на моей стороне.
Завтра я буду копать глубже.

Хороших выходных.

Andy Lanng

Ах, я должен был создать экземпляр waithandles перед началом чтения буфера.
Я тоже поиграю с ним и посмотрю, почему командир не работает.
PS: Если вы получаете _waitHandles[0].Set(); то программа вышла

MrFinch

А, может быть, это все и объясняет. Я замечаю, что появляется окно консоли, и когда я закрываюсь, я могу прочитать первый результат. Я думаю, что именно поэтому SendCommand не работает, "процесс" или консоль закрыты. Я пытаюсь понять, почему и как это предотвратить.

Спасибо за помощь.

MrFinch

Еще раз привет!
Я даю вам 4 за быстрый ответ и за полный класс. Это не совсем так, как я хочу, но это может быть внутренней проблемой моего приложения.

Andy Lanng

Звучит справедливо. Если у меня будет время сегодня я попытаюсь найти эту неуловимую пятую звезду ;)

MrFinch

Отлично! Хм, мне было интересно. Как я уже сказал, Я подозреваю, что проблема может быть в моем внешнем приложении, но не должен иметь возможности читать и записывать в стандартное окно cmd? Я имею в виду если я просто пройду мимо cmd.exe как командование? Однако и это не работает. Я могу читать только один раз после того, как окно cmd будет закрыто (мной). Может быть, это может быть ключом к тому, что происходит...

Твоя "пятерка" приближается тааак ;)

3 Ответов

Рейтинг:
2

Andy Lanng

Обновлено до 2017 года:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace ProcessWithOutput
{

    public class ProcessWrapper : Process, IDisposable
    {
        public enum PipeType { StdOut, StdErr }
        public class Output{
            
            public string Message { get; set; }
            public PipeType Pipe { get; set; }
            public override string ToString(){
                return $"{Pipe}: {Message}";
            }
        }

        private readonly string _command;
        private readonly string _args;
        private bool _isDisposed;

        private readonly Queue<Output> _outputQueue = new Queue<Output>();
        
        
        private readonly ManualResetEvent[] _waitHandles = new ManualResetEvent[2];
        private readonly ManualResetEvent _outputSteamWaitHandle = new ManualResetEvent(false);

        public ProcessWrapper(string startCommand, string args){
            _command = startCommand;
            _args = args;
        }
      
        public IEnumerable<string> GetMessages(){

            while (!_isDisposed){

                _outputSteamWaitHandle.WaitOne();
                if (_outputQueue.Any())
                    yield return _outputQueue.Dequeue().ToString();
            }
        }
        
        public void SendCommand(string command){

            StandardInput.Write(command);
            StandardInput.Flush();
        }

        public new int Start(){

            ProcessStartInfo startInfo = new ProcessStartInfo{

                FileName = _command,
                Arguments = _args,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true
            };
            
            StartInfo = startInfo;
            
            OutputDataReceived += delegate (object sender, DataReceivedEventArgs args){

                if (args.Data == null){

                    _waitHandles[0].Set();
                }
                else if (args.Data.Length > 0){

                    _outputQueue.Enqueue(new Output { Message = args.Data, Pipe = PipeType.StdOut });
                    _outputSteamWaitHandle.Set();
                }
            };

            ErrorDataReceived += delegate (object sender, DataReceivedEventArgs args){

                if (args.Data == null){

                    _waitHandles[1].Set();
                }
                else if (args.Data.Length > 0){

                    _outputSteamWaitHandle.Set();
                    _outputQueue.Enqueue(new Output { Message = args.Data, Pipe = PipeType.StdErr });
                }
            };
            
            base.Start();
            
            _waitHandles[0] = new ManualResetEvent(false);
            BeginErrorReadLine();
            _waitHandles[1] = new ManualResetEvent(false);
            BeginOutputReadLine();
            
            return Id;
        }
        
        public new void Dispose(){

            StandardInput.Flush();
            StandardInput.Close();
            if (!WaitForExit(1000)){
                Kill();
            }
            if (WaitForExit(1000)){
                WaitHandle.WaitAll(_waitHandles);
            }
            base.Dispose();
            _isDisposed = true;
        }
    }
}



Тестовое консольное приложение с выводом данных:
using System;
using System.Threading;
namespace ConsoleWithOutput
{
    class Program
    {

        static void Main(string[] args){

            var count = 0;
            while (true){

                Console.Out.WriteLine($"{count++:0000} + some output");
                Thread.Sleep(1000);

            }
        }
    }
}


Тестовое консольное приложение, использующее processwrapper:
using System;

namespace ProcessWithOutput
{
    class Program
    {
        static void Main(string[] args)
        {
            var process = new ProcessWrapper(args[0],"");
            process.OutputDataReceived += (sender, eventArgs) => Console.WriteLine($"Received: {eventArgs.Data}");
            process.Start();

            Console.ReadLine();
        }
    }
}


К вашему сведению: никаких функциональных изменений в код не было внесено. Самое большое отличие заключается в том, что теперь он реализует класс Process вместо того, чтобы только оборачивать его


Рейтинг:
19

Andy Lanng

Я хочу начать с 5 ^_^

Это целый класс. Он не использует никаких сборок за пределами стандартных библиотек. это может быть сделано еще лучше с помощью RX, например, что позволяет вам превращать события в IEnumberables гораздо лучше, чем я сделал здесь.

Если вы ищете это, то ищите Observable.FromEvents

    public class ProcessWithOutput :IDisposable
    {
        public enum PipeType { StdOut, StdErr }
        public class Output
        {
            //Theres a few ways to arrange the data.  Play with this or just output a string
            public string Message { get; set; }
            public PipeType Pipe { get; set; }
            public override string ToString()
            {
                return string.Format("{0}: {1}", Pipe, Message);
            }
        }

        private readonly string _command;
        private readonly string _args;
        private readonly string _workingDirectory;
        private Process _process;
        private bool _isDisposed = false;

        private readonly Queue<output> _outputQueue = new Queue<output>();

        private StreamWriter _inputStreamWriter;

        //These stop us rushing ahead before async threads are ready
        private ManualResetEvent[] _waitHandles = new ManualResetEvent[2];
        private ManualResetEvent _outputSteamWaitHandle = new ManualResetEvent(false);

        public ProcessWithOutput(string startCommand, string args, string workingDirectory)
        {
            _command = startCommand;
            _args = args;
            _workingDirectory = workingDirectory;
        }

        //This enumerable might be handy.  Whatever reads from it will pause when _outputSteamWaitHandle is not set, 
        //which might be ideal, or could cause your program to lock up if no output is ever received >_<        
public IEnumerable<string> GetMessages()
        {
            while (!_isDisposed)
            {
                _outputSteamWaitHandle.WaitOne();
                if (_outputQueue.Any())
                    yield return _outputQueue.Dequeue().ToString();
            }
            yield break;
        }

        // Easy interface for command input
        public void SendCommand(string command)
        {
            _inputStreamWriter.Write(command);
            _inputStreamWriter.Flush();
        }

        //You might want to start in the ctor, even ^_^
        public int StartProcess()
        {
            ProcessStartInfo startInfo = new ProcessStartInfo
            {
                FileName = _command,
                Arguments = _args,
                WorkingDirectory = _workingDirectory,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true
            };

            _process = new Process { StartInfo = startInfo };


            //Outputs will be async
            _process.OutputDataReceived += delegate(object sender, DataReceivedEventArgs args)
            {
                if (args.Data == null)
                {
                    //the pipe has closed. i.e. the process has ended
                    _waitHandles[0].Set();
                }
                else if (args.Data.Length > 0)
                {
                    _outputQueue.Enqueue(new Output { Message = args.Data, Pipe = PipeType.StdOut });
                    _outputSteamWaitHandle.Set();
                }
            };
            _process.ErrorDataReceived += delegate(object sender, DataReceivedEventArgs args)
            {
                if (args.Data == null)
                {
                    //the pipe has closed. i.e. the process has ended
                    _waitHandles[1].Set();
                }
                else if (args.Data.Length > 0)
                {
                    _outputSteamWaitHandle.Set();
                    _outputQueue.Enqueue(new Output { Message = args.Data, Pipe = PipeType.StdErr });
                }
            };

            //This will be your hook in to write commands
            _inputStreamWriter = _process.StandardInput;

            _process.Start();

            //Now the process has started and those buffers are beginning to fill.
            //These two lines start reading the buffers
            _waitHandles[0] = new ManualResetEvent(false);
            _process.BeginErrorReadLine();
            _waitHandles[1] = new ManualResetEvent(false);
            _process.BeginOutputReadLine();

            return _process.Id;
        }

        //Notice I have implemented the disposable interface to make sure that everything get cleaned up properly.
        public void Dispose()
        {
            _inputStreamWriter.Flush();
            _inputStreamWriter.Close();
            if(!_process.WaitForExit(1000))
                _process.Kill();
            //This next bit might seem odd, but trust me: You HAVE to clear those bufferes!
            if(_process.WaitForExit(1000))
                WaitHandle.WaitAll(_waitHandles);
            _isDisposed = true;
        }
    }


Объяснение должно быть ясно изложено в комментариях. Пожалуйста, дайте мне знать, если вам понадобится еще какая-нибудь помощь.

Удача ^_^


Member 13743237

Если решение не работает, то сколько звезд оно должно получить?

В своем первом фрагменте кода Вы написали:
Информацию.RedirectStandardOutput = ложь;

> тогда как же может работать приведенный ниже фрагмент кода? Мой компилятор Visual Studio 2017 выдает мне ошибку.
вывод строки = процесс.StandardOutput.ReadToEnd();

Затем во 2-м коде

_inputStreamWriter = _process.StandardInput;

> Как вышеприведенный код может работать без перенаправления? Мой компилятор просит меня redeirect, но я не знаю, как это сделать?

А это
_inputStreamWriter.Промывать();

> Throw object reference not set to an instance of a object..

Andy Lanng

Виола - обновлено ниже

Andy Lanng

ТБХ я написал это 3 года назад. 2017 года не существовало (год или версия vs :Þ)
Я посмотрю, вспомню ли я, что он делает, и исправлю его в 2017 году.
->
Похоже, это сработало для операции

Рейтинг:
1

Andy Lanng

Процесс, выполняемый в терминале cmd, состоит из двух частей: процесса и самого терминала.

Когда вы запускаете команду в терминале, эта команда выводится в буфер. если этот буфер заполняется, то процесс ждет, пока буфер будет считан, чтобы он мог продолжить работу.

Терминал считывает данные из выходных буферов и выводит их на экран. Это позволяет процессу продолжаться, но весь этот прекрасный результат тратится впустую.

Выведите терминал из уравнения. Забудь об этом, его не существует. Вот как вы получаете результат от процесса:

public class ProcessWithOutput
{
    private string _command;
    private string _args;
    private string _workingDirectory;

    public ProcessWithOutput(string command, string args, string workingDirectory)
    {
        _command = command;
        _args = args;
        _workingDirectory = workingDirectory;
    }

    public string RunId()
    {
        ProcessStartInfo startInfo = new ProcessStartInfo();
        startInfo.FileName = _command;
        startInfo.Arguments = _args;
        startInfo.WorkingDirectory = _workingDirectory;
        startInfo.UseShellExecute = false;
        startInfo.RedirectStandardOutput = false;
        startInfo.RedirectStandardError = false;

        Process process = new Process();
        process.StartInfo = startInfo;

        process.Start();

        string output = process.StandardOutput.ReadToEnd();

        //even if you don't use this you need to keep that buffer cleared
        string error = process.StandardError.ReadToEnd();

        process.WaitForExit();
        return output;
    }
}


Это отлично подходит для небольших быстрых команд. Если ваш вывод огромен, то вы можете транслировать его вместо этого. Дайте мне знать, если вам нужен этот ответ, включенный здесь.

Удача ^_^