$> set -o pipefail
$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!
$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли
Здесь fortune
условная программа без exit(rand())
.
Cможете объяснить что здесь глючит?
Лирично-историческое отступление
Первый раз с этим Гейзенбагом я познакомился четверть века назад. Тогда для шлюза в FaxNET требовалось сделать несколько утилит через
Усердия в «тщательной обработке ошибок» мне тогда добавил предыдущий опыт борьбы с багами в sendmail и uucp/uupc. Погружаться в детали той истории смысла нет, но боролся с этим Гейзенбагом я недели две по 10-14 часов. Поэтому запомнилось, и вот вчера этот старый знакомый снова заглянул в гости.
TL;DR Ответ
Утилита head
может закрыть канал от fortune
сразу как только прочитает первую строку. Если fortune
выводит более одной строки, то соответствующий вызов write()
вернет ошибку, либо сообщит о выводе меньшего количества байт чем запрошено. В свою очередь, написанная с тщательной обработкой ошибок fortune
вправе отразить эту ситуацию в своем статусе выхода. Тогда из-за установки set -o pipefail
отработает || echo "Вы проиграли"
.
Однако, head
может не успеть закрыть до того, как fortune
закончит вывод данных. Тогда отработает && echo "Повезло!"
.
В одном из моих сегодняшних GNUMakefile
echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'
В переводе на человеческий
Здесь обычным для --version
спрашивается кто он такой, а если опция не поддерживается, то подставляется заглушка "Please use GCC or CLANG compatible compiler".
Подобный
#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"
Признаться я не сразу разглядел старого "знакомого". Более того, проект уже многократно проверялся на "Эльбрусах" и под массой различных дистрибутивов, в том числе с "Альтом". С различными компиляторами, версиями GNU Make и bash. Поэтому своей оплошности я тут видеть никак не хотел.
При попытке воспроизвести проблему и/или понять в чем дело странностей стало больше.
Заклинание в командной строке:
echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"
Через-раз то выдавало лишний текст, то нет… Нередко один из вариантов залипал достаточно долго, но если потыкать подольше то всегда получались оба!
Конечно, strace
Кстати, как любой уважающий себя strace
предпочитает не воспроизводиться.
Так что же происходит?
- Утилита
head
вправе (скорее даже вынуждена) закрыть читаемый канал, сразу как только прочитает запрошенное количество строк. - Генерирующая данные программа-писатель (в данном случае
cc
) может выводить несколько строк и вольна делать это посредством нескольких вызововwrite()
. - Если читатель успеет закрыть канал со своей стороны до окончания записи на стороне писателя, то писателю прилетит ошибка.
- Программа-писатель вправе как проигнорировать ошибку записи в канал, так и отразить её в своем коде завершения.
- Вследствие установки
set -o pipefail
код завершения конвейера будет ненулевым (ошибочным) при ненулевом результате хотя бы от одного элемента, и тогда отработает|| echo "Please use GCC or CLANG compatible compiler"
.
Могут быть вариации, в зависимости от того как программа-писатель работает с сигналами. Например, программа может быть аварийно завершена (с автоматическим формированием ненулевого/ошибочного статуса завершения), либо write()
вернет результат о записи меньшего количества байтов чем запрошено и установит errno = EPIPE
.
Кто виноват?
В описываемом случае все понемногу. Обработка ошибок в cc
(lcc:1.23.20:Sep—4-2019:e2k-v3-linux) не является избыточной. Во многих случаях лучше перебдеть, хотя это и выявляет внезапные недочеты в boilerplate рассчитанным на традиционное поведение.
Что делать?
Неправильно:
fortune | head -1 && echo "Повезло, но вы рискуете!" || echo "WTF?"
Правильно:
-
(fortune && echo "Успешно" || echo "Ошибка") | head -1
Здесь раннее закрытие канала будет обработано интерпретатором команд при обслуживании вложенного конвейера ("внутри" скобок). Соответственно, если
fortune
сообщит в статусе об ошибки записи в закрытый канал, то вывод|| echo "Ошибка"
уже никуда не попадет, так как канал уже закрыт. -
fortune | cat - | head -1 && echo "Успешно" || echo "Ошибка"
Здесь утилита
cat
выступает демпфером, так как игнорирует ошибкуEPIPE
при выводе. Этого достаточно пока выводfortune
небольшой (несколько строк) и помещается в канальном буфере (от 512 байт до ≈64К, в большинстве ОС ≥4К). В противном случае проблема может вернуться.
Как правильно обрабатывать EPIPE
и другие ошибки при записи?
Единственно верного решения нет, но есть простые рекомендации:
EPIPE
требуется обязательно обрабатывать (и отражать в статусе выхода) при выводе данных требующих целостности. Например, в ходе работы архиваторов или утилит резервного копирования.EPIPE
лучше игнорировать при выводе информационных и вспомогательных сообщений. Например, при выводе информации по опциям--help
или--version
.- Если разрабатываемый код допустимо использовать в конвейере перед
| head
, тоEPIPE
лучше игнорировать, иначе лучше обрабатывать и отражать в статусе выхода.
Пользуясь случаем хочу выразить благодарность коллективам
Так держать Camarades, до
Спасибо
КДПВ от
Источник: habr.com