$> 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 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