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 и біць спосабам у кампілятара пры дапамозе опцыі --version пытаецца хто ён такі, а калі опцыя не падтрымліваецца, то падстаўляецца заглушка "Please use GCC or 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, да сустрэчы восенню!

Дзякуй berez за выпраўленне ачапятак і памылак.
КДПВ ад Георгій А.

Крыніца: habr.com

Дадаць каментар