One more Heisenbug повз крокодил

One more Heisenbug повз крокодил

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли

Тут fortune умовна програма без exit(rand()).

Чи можете пояснити що тут глючить?

Лірично-історичний відступ

Перший раз із цим Гейзенбагом я познайомився чверть століття тому. Тоді для шлюзу в FaxNET потрібно зробити кілька утиліт через труби "грають у шашки" під FreeBSD. Як ведеться, я вважав себе просунутим і досить досвідченим програмістом. Тому мав намір зробити все максимально акуратно і ретельно, приділивши особливу увагу обробці помилок.

Завзяття у «ретельній обробці помилок» мені тоді додав попередній досвід боротьби з багами в 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')"'

У перекладі на людську

Тут звичайним для GNU Make и бити способом у компілятора за допомогою опції --version питається хто він такий, а якщо опція не підтримується, то підставляється заглушка "Please use GCC або CLANG compatible compiler".

Подібний котельня можна зустріти де завгодно. У цьому місці він з'явився давно і чудово скрізь працював (Linux, Solaris, OSX, FreeBSD, WSL і т.д.). Але вчора в altlinux на платформі Ельбрус 2000 (E2K) я помітив:

#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-тираду, але не встигнувши натиснути Enter я впізнав свого старого знайомого містера Гейзенбага, а в розробниках компілятора себе самого 25 років тому, Ностальгія… І вирішив засумувати написати цю замітку 😉

До речі, як будь-хто поважає себе Гейзенбаг, під 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?"

правильно:

  1. (fortune && echo "Успешно" || echo "Ошибка") | head -1

    Тут раннє закриття каналу буде оброблено інтерпретатором команд під час обслуговування вкладеного конвеєра ("всередині" дужок). Відповідно, якщо fortune повідомить статус про помилку запису в закритий канал, то висновок || echo "Ошибка" вже нікуди не потрапить, оскільки канал уже закрито.

  2. fortune | cat - | head -1 && echo "Успешно" || echo "Ошибка"

    Тут утиліта cat виступає демпфером, оскільки ігнорує помилку EPIPE під час виведення. Цього достатньо поки що висновок fortune невеликий (кілька рядків) і міститься в канальному буфері (від 512 байт до ≈64К, у більшості ОС ≥4К). В іншому випадку, проблема може повернутися.

Як правильно обробляти EPIPE та інші помилки під час запису?

Єдиного правильного рішення немає, але є прості рекомендації:

  • EPIPE потрібно обов'язково обробляти (і відображати у статусі виходу) при виведенні даних, що потребують цілісності. Наприклад, під час роботи архіваторів або утиліт резервного копіювання.
  • EPIPE краще ігнорувати при виведенні інформаційних та допоміжних повідомлень. Наприклад, при виведенні інформації щодо опцій --help або --version.
  • Якщо код, що розробляється, допустимо використовувати в конвеєрі перед | head, То EPIPE краще ігнорувати, інакше краще обробляти та відображати у статусі виходу.

Користуючись нагодою, хочу висловити подяку колективам. МЦСТ и altlinux за велику результативну роботу. Ваша цілеспрямованість захоплює!
Так тримати Camarades, до зустрічі восени!

Дякуємо берез за виправлення очеп'яток та помилок.
КДПВ від Георгій О.

Джерело: habr.com

Додати коментар або відгук