Repository navigation
Гонка в ПрочитатьСтроку() потока вывода процесса: вставляется лишняя пустая строка #1726
Description
Опишите ошибку
ПотокВывода.ПрочитатьСтроку() у процесса иногда возвращает лишнюю пустую строку в середине вывода. Это гонка между потоком, наполняющим буфер, и читателем.
ProcessOutputWrapper.StreamDataReceived получает от .NET уже разбитые строки (e.Data приходит без терминатора) и склеивает их, добавляя разделитель перед каждой следующей:
private void StreamDataReceived(object sender, sys.DataReceivedEventArgs e)
{
if (e.Data != null)
{
lock(_buffer)
{
if (_buffer.Length != 0)
_buffer.Append(System.Environment.NewLine);
_buffer.Append(e.Data);
}
}
}Буфер растёт так: L1 → L1L2 → L1L2L3. То есть последняя строка в буфере всегда лежит без терминатора.
Теперь ProcessOutputWrapper.ReadLine(). Если читатель вычерпал буфер ровно на конце L2:
int ch = ReadInternal();
if (ch == -1) break; // буфер кончился, терминатора после L2 ещё нет
...
if (sb.Length > 0)
return sb.ToString(); // вернули "L2" - пока верноДальше продюсер дописывает L3, и _bufferIndex оказывается на разделителе. Следующий вызов ReadLine() первым же символом видит \r, подглядывает \n, съедает оба и возвращает пустую строку.
Воспроизведение ошибки
Плавающее, зависит от того, как лягут порции вывода. Проявляется на существующем тесте tests/process.os → ТестДолжен_ПрочитатьВыводOscriptПострочно, который читает штатный вывод oscript построчно:
Различия в позиции 195.
Ожидали: `ents...]\n\nModes:\n -measure `
Получено:`ents...]\n\nModes:\n\n -measure `
Позиция значения не имеет — это просто место, где в том прогоне совпало вычерпывание буфера.
Ожидаемое поведение
Построчное чтение возвращает ровно те строки, которые вывел процесс, без вставки пустых.
Соседний тест ТестДолжен_ПрочитатьВыводOscriptСразу, читающий тот же вывод целиком через Прочитать(), стабильно зелёный — проблема именно в построчной нарезке.
Окружение
- ОС: воспроизводилось на Windows (Jenkins-агент). Гонка платформонезависимая, но на Windows
Environment.NewLineэто\r\n, вывод приходит бо́льшим числом порций, и шанс попасть в стык выше. На Linux те же тесты стабильно зелёные. - Версия:
develop
Дополнительная информация
Возможное решение — дописывать разделитель после строки, а не перед следующей:
_buffer.Append(e.Data);
_buffer.Append(System.Environment.NewLine);Тогда каждая полная строка в буфере всегда терминирована: читатель не вернёт незавершённый хвост как готовую строку и не наткнётся на осиротевший разделитель.
Побочный эффект — Прочитать() начнёт отдавать текст с завершающим переводом строки, а ТестДолжен_ПрочитатьВыводOscriptСразу сравнивает результат с эталоном без него. То есть правка тянет за собой либо СокрП в тесте, либо подрезание хвоста в ReadToEnd. Что из этого правильнее — вопрос к тому, каким контракт Прочитать() задумывался.
Найдено при разборе падения в PR #1725; к изменениям того PR отношения не имеет — каталог src/OneScript.StandardLibrary/Processes/ там не затрагивается.
Activity
EvilBeaver commentedon Aug 27, 2026
Контракт задумывался таким, что ничего подрезать не надо, а результат не содержит синтетических символов в конце.
Потоки вывода процесса переведены на сырое чтение StandardOutput
Исправлена гонка в ПрочитатьСтроку() потока вывода процесса
Потоки вывода процесса переведены на сырое чтение StandardOutput
Исправлена гонка в ПрочитатьСтроку(): потоки вывода процесса переведе…
Исправлена гонка в ПрочитатьСтроку(): потоки вывода процесса переведе…
Исправлена гонка в ПрочитатьСтроку(): потоки вывода процесса переведе…
Исправлена гонка в ПрочитатьСтроку(): потоки вывода процесса переведе…
Исправлена гонка в ПрочитатьСтроку(): потоки вывода процесса переведе…