Още един Хайзенбуг покрай крокодила

Още един Хайзенбуг покрай крокодила

$> 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 добави към усърдието ми в „задълбочено обработване на грешки“. Няма смисъл да се гмуркам в подробностите на тази история, но се борих с този Heisenbug две седмици по 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 пита кой е той и ако опцията не се поддържа, тогава се вмъква мъниче „Моля, използвайте GCC или CLANG съвместим компилатор“.

като стереотипния може да се намери навсякъде. Появи се на това място преди много време и работеше перфектно навсякъде (Linux, Solaris, OSX, FreeBSD, ПСЖ и т.н.). Но вчера в altlinux на платформата Елбрус 2000 (E2K) Забелязах:

#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"

Честно казано, не разпознах веднага стария си „познат“. Освен това проектът вече е тестван многократно на Елбрус и под много различни дистрибуции, включително Alt. С различни компилатори, версии на 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, разпознах моя стар приятел г-н Heisenbug и разработчиците компилатор себе си преди 25 години, носталгия… И реших да се натъжа и да напиша тази бележка 😉

Впрочем като всеки себеуважаващ се Хайзенбуг, подс strace предпочита да не се размножава.

Е, какво става?

  • Полезност head има право (или по-скоро дори е принуден) да затвори канала, който се чете, веднага щом прочете заявения брой редове.
  • Програмата за генериране на данни (в този случай cc) мога отпечатайте няколко реда и Безплатно направете това чрез множество обаждания write().
  • ако читателят ще има време да затвори канала от своя страна преди края на записа от страната на писателя, след което писателят ще получи грешка.
  • Програма Writer има право на и двете игнорират грешката при запис на канала и я отразяват във вашия код за завършване.
  • Поради монтаж 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) не е излишен. В много случаи е по-добре да бъдете внимателни, въпреки че това разкрива внезапни недостатъци в модела, предназначен за традиционно поведение.

Какво да правя?

Грешен:

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 байта до ≈64K, в повечето OS ≥4K). В противен случай проблемът може да се върне.

Как да обработваме правилно EPIPE и други грешки при записа?

Няма едно правилно решение, но има прости препоръки:

  • EPIPE длъжен трябва да бъдат обработени (и отразено в състоянието на изход) при извеждане на данни, които изискват цялост. Например по време на работа на архиватори или помощни програми за архивиране.
  • EPIPE по-добре да се игнорира при показване на информация и спомагателни съобщения. Например, когато се показва информация за опции --help или --version.
  • Ако кодът, който се разработва, може да се използва в конвейер преди | head, Тогава EPIPE По-добре е да се игнорира, в противен случай е по-добре да се обработи и отрази в състоянието на изход.

Използвам възможността да изразя своята благодарност към екипите MCST и altlinux за много продуктивна работа. Вашата решителност е невероятна!
Продължавайте така Камарадс, напред срещи през есента!

Благодаря берез за коригиране на печатни грешки и грешки.
КДПВ от Георги А.

Източник: www.habr.com

Добавяне на нов коментар