Jeszcze jeden Heisenbug za krokodylem

Jeszcze jeden Heisenbug za krokodylem

$> set -o pipefail

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

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

Tutaj fortune program warunkowy bez exit(rand()).

Możesz wytłumaczyć? co tu jest nie tak?

Dygresja liryczno-historyczna

Po raz pierwszy zetknąłem się z tym Heisenbugiem ćwierć wieku temu. Następnie dla bramy w FaxNET konieczne było utworzenie kilku narzędzi via Rury „gra w warcaby” pod FreeBSD. Zgodnie z oczekiwaniami uważałem się za zaawansowanego i dość doświadczonego programistę. Dlatego też zamierzałem zrobić wszystko możliwie najdokładniej i dokładnie, zwracając szczególną uwagę na obsługę błędów...

Moje wcześniejsze doświadczenie w radzeniu sobie z błędami w sendmailu i uucp/uupc zwiększyło moją staranność w „dokładnej obsłudze błędów”. Nie ma sensu zagłębiać się w szczegóły tej historii, ale męczyłem się z tym Heisenbugiem przez dwa tygodnie po 10-14 godzin. Dlatego też o tym przypomniano i wczoraj ten stary znajomy odwiedził nas ponownie.

Odpowiedź TL;DR

Użyteczność head może zamknij kanał z fortune od razu gdy tylko przeczyta pierwszą linijkę. Jeśli fortune wyprowadza więcej niż jedną linię, a następnie odpowiednie wywołanie write() zwróci błąd lub zgłosi, że wyprowadzono mniej bajtów niż żądano. Z kolei napisany z ostrożną obsługą błędów fortune ma prawo odzwierciedlić tę sytuację w swoim statusie wyjścia. Potem z powodu instalacji set -o pipefail będzie działać || echo "Вы проиграли".

Jednak head może nie zdążyć zamknąć wcześniej fortune zakończy wysyłanie danych. Wtedy to zadziała && echo "Повезло!".

W jednym z moich dzisiejszych GNUMakefile jest jeden fragment:

echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'

Przetłumaczone na człowieka

Jest to tutaj powszechne Marka GNU и bash w sposób kompilatora za pomocą opcji --version pyta, kim jest, a jeśli opcja nie jest obsługiwana, wstawiany jest kod pośredniczący „Użyj kompilatora kompatybilnego z GCC lub CLANG”.

jak płyta kotłowa można znaleźć wszędzie. Pojawił się w tym miejscu dawno temu i działał doskonale wszędzie (Linux, Solaris, OSX, FreeBSD, WSL itp.). Ale wczoraj w altlinux na platformie Elbrus 2000 (E2K) Zauważyłem:

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

Szczerze mówiąc, nie od razu rozpoznałem mojego starego „znajomego”. Co więcej, projekt był już wielokrotnie testowany na Elbrusie i pod wieloma różnymi dystrybucjami, w tym Alt. Z różnymi kompilatorami, wersjami GNU Make i bash. Dlatego nie chciałem widzieć tutaj mojego błędu.

Kiedy próbowałem odtworzyć problem i/lub zrozumieć, co się dzieje, zaczęły się dziać dziwniejsze rzeczy.
Zaklęcie wiersza poleceń:

echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"

Od czasu do czasu pojawiał się dodatkowy tekst, a potem nie... Często jedna z opcji pozostawała w pamięci przez dłuższy czas, ale jeśli naciskałeś dłużej, zawsze miałeś obie!

Oczywiście strace nasze wszystko! I po napisaniu strace tyrady, ale nie mając czasu na naciśnięcie Enter, rozpoznałem mojego starego przyjaciela, pana Heisenbuga, i programistów kompilator ja 25 lat temu, Nostalgia… A ja postanowiłam się posmucić i napisać tę notatkę 😉

Nawiasem mówiąc, jak każdy szanujący się Heisenbug, pod strace woli się nie rozmnażać.

Więc co się dzieje?

  • Użyteczność head ma prawo (a raczej jest zmuszony) zamknąć czytany kanał, gdy tylko odczyta żądaną liczbę wierszy.
  • Twórca programu generującego dane (w tym przypadku cc) może wydrukuj wiele linii i bezpłatny zrób to poprzez wiele połączeń write().
  • jeśli czytelnik zdąży zamknąć kanał po swojej stronie przed zakończeniem nagrywania po stronie piszącego, wówczas piszący otrzyma błąd.
  • Program pisarza jest uprawniony do oba ignorują błąd zapisu kanału i odzwierciedlają go w kodzie zakończenia.
  • Ze względu na instalację set -o pipefail kod zakończenia potoku będzie niezerowy (błędny), jeśli wynik będzie różny od zera przynajmniej w jednym elemencie i wtedy zadziała || echo "Please use GCC or CLANG compatible compiler".

Mogą występować różnice w zależności od sposobu, w jaki program piszący współpracuje z sygnałami. Na przykład program może zakończyć się nieprawidłowo (z automatycznym wygenerowaniem niezerowego statusu zakończenia/błędu) lub write() zwróci wynik zapisania mniejszej liczby bajtów niż żądano i ustawiono errno = EPIPE.

Kto jest winny?

W opisywanym przypadku trochę wszystkiego. Błąd podczas obsługi cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) nie jest zbędny. W wielu przypadkach lepiej jest zachować ostrożność, chociaż ujawnia to nagłe błędy w szablonie zaprojektowanym dla tradycyjnego zachowania.

Co robić?

Źle:

fortune | head -1 && echo "Повезло, но вы рискуете!" || echo "WTF?"

Poprawnie:

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

    W tym przypadku wcześniejsze zamknięcie potoku będzie obsługiwane przez interpreter poleceń podczas obsługi zagnieżdżonego potoku („w” nawiasach). Odpowiednio, jeśli fortune zgłosi błąd zapisu do zamkniętego kanału w statusie, a następnie na wyjściu || echo "Ошибка" nigdzie nie dotrze, ponieważ kanał jest już zamknięty.

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

    Oto narzędzie cat działa jak tłumik, ponieważ ignoruje błąd EPIPE po wycofaniu. To tyle na razie, podsumowując fortune mały (kilka linii) i mieści się w buforze kanału (od 512 bajtów do ≈64K, w większości systemów operacyjnych ≥4K). W przeciwnym razie problem może powrócić.

Jak prawidłowo przetwarzać EPIPE i inne błędy nagrywania?

Nie ma jednego właściwego rozwiązania, ale istnieją proste zalecenia:

  • EPIPE wymagane musi zostać przetworzony (i odzwierciedlone w statusie wyjścia) podczas wysyłania danych wymagających integralności. Na przykład podczas działania archiwizatorów lub narzędzi do tworzenia kopii zapasowych.
  • EPIPE lepiej zignorować podczas wyświetlania informacji i komunikatów pomocniczych. Na przykład podczas wyświetlania informacji o opcjach --help lub --version.
  • Jeśli opracowywany kod może zostać wcześniej użyty w potoku | headnastępnie EPIPE Lepiej zignorować, w przeciwnym razie lepiej przetworzyć i odzwierciedlić status wyjścia.

Chciałbym skorzystać z okazji, aby wyrazić wdzięczność zespołom MCST и altlinux za wspaniałą produktywną pracę. Twoja determinacja jest niesamowita!
Tak trzymać, Camarades, tak trzymać spotkania jesienią!

Dzięki Berez w celu poprawienia literówek i błędów.
KDPV z Georgy A.

Źródło: www.habr.com

Dodaj komentarz