Process.StandardOutput.Read не получава данни надеждно и Process.StandardInput.WriteLine не изчиства съдържанието на буфера на своя BaseStream

Опитвам се да направя просто приложение, което комуникира с конзолна програма, която работи по начин, подобен на класическия cmd.exe на Windows.

Очаква се последователността на изпълнение да бъде следната:

  1. Настройте фонов работник и го стартирайте, за да прочете изхода.
  2. Стартирайте процеса и изчакайте за кратко.
  3. Подайте команда към Process.StandardInput
  4. Изчакайте няколко секунди
  5. Излезте от фоновата нишка и убийте процеса

Но това, което се случва, не е според очакванията, защото:

  1. Не целият изход на cmd.exe се чете. Дори когато се изключи стъпката, която записва в StandardInput
  2. Низът не е предаден на командата от StandardInput, въпреки че има AutoFlush = true.

Опитах също Process.StandardInput.Flush(), който не прави нищо. Опитах се също да му предам низ, подплатен с интервали с дължина, по-голяма от 4096, което е размерът на буфера без резултати. Нищо не се случва!! Защо?

Опитах това на dot net 4.5.2 и 4.7.1

Подобни въпроси съществуват тук, тук и тук, но нито един от отговорите не работи. Други са внедрени на друг език. например Java, Delphi и др

Това е опростена версия на моя код:

BackgroundWorker _outputWorker;
Process _process;
StreamWriter _inputWriter;
TextReader _outputReader;

void Main()
{
    _outputWorker = new BackgroundWorker { WorkerSupportsCancellation = true };
    _outputWorker.DoWork += OnOutputWorkerDoWork;
    _outputWorker.RunWorkerAsync();

    _process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo
        {
            FileName = "cmd.exe",
            Arguments = string.Empty,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            WorkingDirectory = Directory.GetCurrentDirectory(),
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        }
    };

    Console.WriteLine("Starting...");
    if (!_process.Start()) return;
    _inputWriter = _process.StandardInput;
    _inputWriter.AutoFlush = true; // does nothing
    _outputReader = TextReader.Synchronized(_process.StandardOutput);

    // You can exclude this step too and still not get the expected output
    Thread.Sleep(500);
    _inputWriter.WriteLine("dir");
    _inputWriter.Flush(); // does nothing, private field carpos = 0
    _inputWriter.BaseStream.Flush(); // does nothing, private field carpos = 5 which is equal to length of "dir" command + 2 characters (NewLine \r\n)
    //_inputWriter.WriteLine("dir".PadLeft(4096)); // does nothing
    // also closing the stream does nothing and does something that I can't afford which is closing the exe
    // _inputWriter.Close();
    //

    _process.WaitForExit(5000);
    _outputWorker.CancelAsync();
    _process.Kill();
    Console.WriteLine("Done");
}

void OnOutput(string data)
{
    // never mind thread safety for now. It's just a single line static call
    Console.WriteLine(data);
}

void OnOutputWorkerDoWork(object sender, DoWorkEventArgs e)
{
    const int BUFFER_SIZE = 4096;

    StringBuilder builder = new StringBuilder(BUFFER_SIZE);

    while (!_outputWorker.CancellationPending)
    {
        /*
        * It'll keep on running untill it's canceled to reduce thread costs
        * because the program will run different executables sequentially which 
        * all are similar to cmd.exe.
        */
        try
        {
            // Simplified version without locking
            if (_outputReader == null) continue;

            TextReader reader = _outputReader;
            if (reader.Peek() < 1) continue;

            char[] buffer = new char[BUFFER_SIZE];

            do
            {
                int count = reader.Read(buffer, 0, buffer.Length);
                if (count > 0) builder.Append(buffer, 0, count);
            }
            while (reader.Peek() > 0);
        }
        catch (Exception ex)
        {
            // handle exception in debug mode
            Console.WriteLine(ex.Message); // no exception generated!
            continue;
        }

        if (builder.Length == 0) continue;
        OnOutput(builder.ToString());
        builder.Length = 0;
    }

    if (!IsWaitable(_process)) return;

    try
    {
        if (_outputReader == null) return;

        TextReader reader = _outputReader;
        if (reader.Peek() < 1) return;

        char[] buffer = new char[BUFFER_SIZE];

        do
        {
            int count = reader.Read(buffer, 0, buffer.Length);
            if (count > 0) builder.Append(buffer, 0, count);
        }
        while (reader.Peek() > 0);
    }
    catch (Exception ex)
    {
        // handle exception in debug mode
        Console.WriteLine(ex.Message); // no exception generated!
        return;
    }

    if (builder.Length > 0) OnOutput(builder.ToString());
}

bool IsWaitable(Process thisValue)
{
    return thisValue != null && !thisValue.HasExited;
}

Не мога да използвам Process.BeginOutputReadLine, защото чака NewLine да присъства в потока, което не се случва за последния ред на изхода. Вижте това и това за подробности

Получавам Microsoft Windows [Версия xxxxx]

(c) линия за авторски права

И програмата не показва подканата на командния ред, освен ако не идва повече резултат от процеса, който съдържа NewLine

Точките на интерес са:

1. Защо този код не чете целия изход както трябва до края?

След като опитах много неща, изглежда, че буферът не съдържа повече текст за четене и в потока изглежда липсват някои данни от оригиналния изход на изпълнимия файл. Странното е, че се случва случайно. Понякога получавам втория ред, а понякога не. Във всеки случай никога не получих резултата, който трябва да е резултат от подаването на вътрешни команди към cmd процеса.

2. StandardInput (StreamWriter) всъщност изчиства буфера (или си мисли, че го прави) и настройва своите charpos на нула, докато BaseStream все още има своето charpos поле = [дължина на буфера]. Защо се случва това?

Всякакви идеи за това какво може да е проблемът или заобиколно решение ще бъдат много оценени.


person Community    schedule 13.03.2018    source източник


Отговори (1)


Е, в крайна сметка го накарах да работи след 2 дни разочарование. Много просто решение:

Process _process;
StreamWriter _inputWriter;

void Main()
{
    _process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo
        {
            FileName = "cmd.exe",
            Arguments = string.Empty,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            WorkingDirectory = Directory.GetCurrentDirectory(),
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        }
    };

    _process.OutputDataReceived += (s, e) => // instead of using a background worker
    {
        if (e.Data == null) return;
        Console.WriteLine(e.Data);
    };

    Console.WriteLine("Starting...");
    if (!_process.Start()) return;
    _process.BeginOutputReadLine(); // <- using BeginOutputReadLine
    _inputWriter = _process.StandardInput;
    _inputWriter.AutoFlush = true;
    _inputWriter.WriteLine(); // <- my little trick here

    // using LINQPad, replace it with Console.ReadLine();
    string input = Util.ReadLine<string> ("Enter command:");
    if (!string.IsNullOrEmpty(input)) _inputWriter.WriteLine(input);
    _process.WaitForExit(5000);
    _process.Kill();
    Console.WriteLine("Done");
}

void OnOutput(string data)
{
    Console.WriteLine(data);
}

Забравих да спомена, че трябва да изчакам въвеждането на потребителя, в противен случай ще работи нормално с Process.BeginOutputReadLine(), без да използвам BackgroundWorker.

Както и да е, надявам се никой да не премине през 2-дневното изживяване потребител/разработчик и да губи 2 дни от живота си за това.


Редактиране:

За съжаление предишното решение е погрешно. Проблемът е причинен от вътрешния клас AsyncStreamReader. Изходният код може да бъде намерен тук

След като модифицира кода си, за да пропусне разделянето на съдържанието на буфера си на редове и да пропусне низове на опашка, той работи според очакванията и е дори по-бърз!

Ето модифицираната версия:

//
//   Copyright (c) Microsoft Corporation.  All rights reserved.
//
// ==--== 
/*============================================================
** 
** Class:  AsyncStreamReader 
**
** Purpose: For reading text from streams using a particular 
** encoding in an asychronous manner used by the process class
**
**
===========================================================*/
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;

namespace System.Diagnostics
{
    /// <summary>
    /// http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/fx/src/Services/Monitoring/system/Diagnosticts/AsyncStreamReader@cs/1/AsyncStreamReader@cs
    /// </summary>
    public sealed class AsyncStreamReader : DisposableBase, IDisposable
    {
        internal const int DEFAULT_BUFFER_SIZE = 4096;  // Byte buffer size
        private const int MIN_BUFFER_SIZE = 128;

        private Decoder _decoder;
        private byte[] _byteBuffer;
        private char[] _charBuffer;
        // Record the number of valid bytes in the byteBuffer, for a few checks. 

        // This is the maximum number of chars we can get from one call to
        // ReadBuffer.  Used so ReadBuffer can tell when to copy data into
        // a user's char[] directly, instead of our internal char[]. 
        private int _maxCharsPerBuffer;

        // Store a backpointer to the process class, to check for user callbacks 
        private Process _process;
        private StringBuilder _sb;

        // Delegate to call user function.
        private Action<string> _userCallBack;

        // Internal Cancel operation 
        private bool _cancelOperation;
        private ManualResetEvent _eofEvent;

        public AsyncStreamReader(Process process, Stream stream, Action<string> callback, Encoding encoding)
            : this(process, stream, callback, encoding, DEFAULT_BUFFER_SIZE)
        {
        }


        // Creates a new AsyncStreamReader for the given stream.  The 
        // character encoding is set by encoding and the buffer size,
        // in number of 16-bit characters, is set by bufferSize. 
        public AsyncStreamReader(Process process, Stream stream, Action<string> callback, Encoding encoding, int bufferSize)
        {
            Debug.Assert(process != null && stream != null && encoding != null && callback != null, "Invalid arguments!");
            Debug.Assert(stream.CanRead, "Stream must be readable!");
            Debug.Assert(bufferSize > 0, "Invalid buffer size!");
            Init(process, stream, callback, encoding, bufferSize);
        }

        private void Init(Process process, Stream stream, Action<string> callback, Encoding encoding, int bufferSize)
        {
            _process = process;
            BaseStream = stream;
            CurrentEncoding = encoding;
            _userCallBack = callback;
            _decoder = encoding.GetDecoder();
            if (bufferSize < MIN_BUFFER_SIZE) bufferSize = MIN_BUFFER_SIZE;
            _byteBuffer = new byte[bufferSize];
            _maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize);
            _charBuffer = new char[_maxCharsPerBuffer];
            _sb = new StringBuilder(_charBuffer.Length);
            _cancelOperation = false;
            _eofEvent = new ManualResetEvent(false);
        }

        public void Close()
        {
            Dispose(true);
        }

        void IDisposable.Dispose()
        {
            Dispose(true);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (BaseStream != null)
                {
                    BaseStream.Close();
                    BaseStream = null;
                }
            }

            if (BaseStream != null)
            {
                BaseStream = null;
                CurrentEncoding = null;
                _decoder = null;
                _byteBuffer = null;
                _charBuffer = null;
            }

            if (_eofEvent != null)
            {
                _eofEvent.Close();
                _eofEvent = null;
            }
        }

        public Encoding CurrentEncoding { get; private set; }

        public Stream BaseStream { get; private set; }

        // User calls BeginRead to start the asynchronous read
        public void BeginRead()
        {
            _cancelOperation = false;
            BaseStream.BeginRead(_byteBuffer, 0, _byteBuffer.Length, ReadBuffer, null);
        }

        public void CancelOperation()
        {
            _cancelOperation = true;
        }

        // This is the async callback function. Only one thread could/should call this.
        private void ReadBuffer(IAsyncResult ar)
        {
            if (_cancelOperation) return;

            int byteLen;

            try
            {
                byteLen = BaseStream.EndRead(ar);
            }
            catch (IOException)
            {
                // We should ideally consume errors from operations getting cancelled
                // so that we don't crash the unsuspecting parent with an unhandled exc. 
                // This seems to come in 2 forms of exceptions (depending on platform and scenario),
                // namely OperationCanceledException and IOException (for errorcode that we don't 
                // map explicitly). 
                byteLen = 0; // Treat this as EOF
            }
            catch (OperationCanceledException)
            {
                // We should consume any OperationCanceledException from child read here
                // so that we don't crash the parent with an unhandled exc
                byteLen = 0; // Treat this as EOF 
            }

            if (byteLen == 0)
            {
                // We're at EOF, we won't call this function again from here on.
                _eofEvent.Set();
            }
            else
            {
                int charLen = _decoder.GetChars(_byteBuffer, 0, byteLen, _charBuffer, 0);

                if (charLen > 0)
                {
                    _sb.Length = 0;
                    _sb.Append(_charBuffer, 0, charLen);
                    _userCallBack(_sb.ToString());
                }

                BaseStream.BeginRead(_byteBuffer, 0, _byteBuffer.Length, ReadBuffer, null);
            }
        }

        // Wait until we hit EOF. This is called from Process.WaitForExit 
        // We will lose some information if we don't do this.
        public void WaitUtilEof()
        {
            if (_eofEvent != null)
            {
                _eofEvent.WaitOne();
                _eofEvent.Close();
                _eofEvent = null;
            }
        }
    }
}

Употреба:

Process _process;
StreamWriter _inputWriter;
AsyncStreamReader _output;

void Main()
{
    _process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo
        {
            FileName = "cmd.exe",
            Arguments = string.Empty,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            WorkingDirectory = Directory.GetCurrentDirectory(),
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        }
    };

    Console.WriteLine("Starting...");
    if (!_process.Start()) return;
    BeginRead();
    _inputWriter = _process.StandardInput;
    _inputWriter.AutoFlush = true;

    Thread.Sleep(500);

    string input = Util.ReadLine<string>("Type a command:");
    if (!string.IsNullOrEmpty(input)) _inputWriter.WriteLine(input);

    _process.WaitForExit(5000);
    CancelRead();
    _process.Kill();
    Console.WriteLine("Done");
}

void OnOutput(string data)
{
    Console.Write(data);
}

void BeginRead()
{
    if (_output == null) _output = new AsyncStreamReader(_process, _process.StandardOutput.BaseStream, OnOutput, _process.StandardOutput.CurrentEncoding);
    _output.BeginRead();
}

void CancelRead()
{
    _output.CancelOperation();
}
person Community    schedule 13.03.2018