Я недавно дал ответ на этот вопрос: C # - перенаправление вывода консоли реального времени .
Как часто случается, объясняя вещи (здесь «вещи» было, как я решил подобную проблему)приводит вас к большему пониманию и / или, как в данном случае, «упс» моментам.Я понял, что в моем решении, как реализовано, есть ошибка.Ошибка имеет мало практического значения, но она имеет чрезвычайно большое значение для меня как для разработчика: я не могу успокоиться, зная, что мой код может взорваться.
Устранение ошибки - цельэтот вопрос.Я прошу прощения за длинное вступление, так что давайте запачкаемся.
Я хотел создать класс, который позволит мне получать входные данные от стандартного вывода консоли Stream
.Консольные выходные потоки имеют тип FileStream
;реализация может привести к этому, если это необходимо.Существует также связанный StreamReader
, уже присутствующий для использования.
Существует только одна вещь, которую мне нужно реализовать в этом классе для достижения желаемой функциональности: асинхронная операция «прочитать все данные, доступные в данный момент».Чтение до конца потока не является жизнеспособным, потому что поток не завершится, пока процесс не закроет дескриптор вывода консоли, и он не сделает этого, потому что он является интерактивным и ожидает ввода перед продолжением.
Я будуиспользуя эту гипотетическую асинхронную операцию для реализации уведомлений на основе событий, что будет более удобным для моих абонентов.
Открытый интерфейс класса такой:
public class ConsoleAutomator {
public event EventHandler<ConsoleOutputReadEventArgs> StandardOutputRead;
public void StartSendingEvents();
public void StopSendingEvents();
}
StartSendingEvents
и StopSendingEvents
делать то, что они рекламируют;для целей этого обсуждения мы можем предположить, что события всегда отправляются без потери общности.
Класс использует эти два поля внутренне:
protected readonly StringBuilder inputAccumulator = new StringBuilder();
protected readonly byte[] buffer = new byte[256];
Функциональность классареализовано в методах ниже.Чтобы получить шарик:
public void StartSendingEvents();
{
this.stopAutomation = false;
this.BeginReadAsync();
}
Чтобы прочитать данные из Stream
без блокировки, а также без использования символа возврата каретки, BeginRead
называется:
protected void BeginReadAsync()
{
if (!this.stopAutomation) {
this.StandardOutput.BaseStream.BeginRead(
this.buffer, 0, this.buffer.Length, this.ReadHappened, null);
}
}
Сложная часть:
BeginRead
требует использования буфера.Это означает, что при чтении из потока возможно, что байты, доступные для чтения («входящий блок»), больше, чем буфер.Помните, что цель здесь - - прочитать все фрагменты и вызвать абонентов событий ровно один раз для каждого фрагмента .
. С этой целью, если буфер заполнен послеEndRead
, мы не отправляем его содержимое подписчикам сразу, а вместо этого добавляем его к StringBuilder
.Содержимое StringBuilder
отправляется обратно только тогда, когда больше нет информации для чтения из потока.
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(); // only send back if we 're sure we got it all
}
this.BeginReadAsync(); // continue "looping" with BeginRead
}
После любого чтения, которого недостаточно для заполнения буфера (в этом случае мы знаем, чтобольше не было данных для чтения во время последней операции чтения), все накопленные данные отправляются подписчикам:
private void OnInputRead()
{
var handler = this.StandardOutputRead;
if (handler == null) {
return;
}
handler(this,
new ConsoleOutputReadEventArgs(this.inputAccumulator.ToString()));
this.inputAccumulator.Clear();
}
(я знаю, что, пока нет подписчиков, данные накапливаются навсегда.является осознанным решением).
Хорошо
Эта схема работает почти отлично:
- Асинхронная функциональность безпорождение любых потоков
- Очень удобно для вызывающего кода (просто подписаться на событие)
- Никогда не более одного события на каждый раз, когда доступны данные для чтения
- Почтине зависит от размера буфера
плохой
последний почти очень большой.Рассмотрим, что происходит, когда есть входящий кусок с длиной, точно равной размеру буфера.Чанк будет прочитан и помещен в буфер, но событие не будет запущено.За этим последует BeginRead
, который ожидает найти больше данных, принадлежащих текущему чанку, чтобы отправить их обратно целиком, но ... в потоке больше не будет данных.
Фактически, пока данные помещаются в поток порциями с длиной, точно равной размеру буфера, данные будут буферизироваться, и событие никогда не будет инициировано.
Этот сценарий может быть маловероятным на практике, тем более что мы можем выбрать любое число для размера буфера, но проблема есть.
Решение
К сожалению, после проверки доступных методов на FileStream
и StreamReader
я не могу найти ничего, что позволило бы мне заглянуть в поток, а также разрешить использование асинхронных методов на нем.
Одним из "решений" было бы ожидание потока на ManualResetEvent
после обнаружения условия "заполнение буфера". Если событие не сообщается (посредством асинхронного обратного вызова) в течение небольшого промежутка времени, больше данных из потока не будет поступать, и данные, накопленные до сих пор, должны быть отправлены подписчикам. Однако это создает необходимость в другом потоке, требует синхронизации потоков и просто неэлегатно.
Также достаточно указать тайм-аут для BeginRead
(время от времени перезванивайте в мой код, чтобы я мог проверить, есть ли данные для отправки; в большинстве случаев делать нечего, поэтому я ожидаю хит производительности должен быть незначительным). Но похоже, что таймауты не поддерживаются в FileStream
.
Так как я представляю, что асинхронные вызовы с тайм-аутами являются опцией в чистом Win32, другой подход мог бы заключаться в том, чтобы PInvoke выпал из проблемы. Но это также нежелательно, так как это создаст сложность и просто вызовет боль в коде.
Есть ли элегантный способ обойти проблему?
Спасибо, что проявили терпение, чтобы прочитать все это.
Обновление:
Я определенно плохо описал сценарий в моей первоначальной записи. С тех пор я немного пересмотрел рецензию, но для большей уверенности:
Вопрос заключается в том, как реализовать асинхронную операцию «чтение всех данных, доступных в данный момент».
Мои извинения людям, которые нашли время, чтобы прочитать и ответить без меня, чтобы мои намерения были достаточно ясны.