$> set -o pipefail
$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!
$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли
Тут fortune
умовна програма без exit(rand())
.
Чи можете пояснити що тут глючить?
Лірично-історичний відступ
Перший раз із цим Гейзенбагом я познайомився чверть століття тому. Тоді для шлюзу в 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 або 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