Noch ein Heisenbug am Krokodil vorbei

Noch ein Heisenbug am Krokodil vorbei

$> set -o pipefail

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

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

Hier fortune bedingtes Programm ohne exit(rand()).

Können Sie erklären? Was ist hier los??

Lyrisch-historischer Exkurs

Ich habe diesen Heisenbug vor einem Vierteljahrhundert zum ersten Mal kennengelernt. Dann war es für das Gateway in FaxNET notwendig, mehrere Dienstprogramme über zu erstellen Rohre „Dame spielen“ unter FreeBSD. Wie erwartet hielt ich mich für einen fortgeschrittenen und ziemlich erfahrenen Programmierer. Deshalb wollte ich alles so sorgfältig und sorgfältig wie möglich machen und dabei besonders auf die Fehlerbehandlung achten ...

Meine früheren Erfahrungen im Umgang mit Fehlern in sendmail und uucp/uupc trugen zu meiner Sorgfalt bei der „gründlichen Fehlerbehandlung“ bei. Es macht keinen Sinn, in die Details dieser Geschichte einzutauchen, aber ich habe zwei Wochen lang 10 bis 14 Stunden lang mit diesem Heisenbug zu kämpfen. Deshalb wurde daran erinnert und gestern kam dieser alte Bekannte erneut zu Besuch vorbei.

TL;DR Antwort

Dienstprogramm head können Schließen Sie den Kanal ab fortune auf einmal sobald er die erste Zeile liest. Wenn fortune Gibt mehr als eine Zeile aus, dann der entsprechende Aufruf write() gibt einen Fehler zurück oder meldet, dass weniger Bytes ausgegeben werden als angefordert. Im Gegenzug mit sorgfältiger Fehlerbehandlung geschrieben fortune hat das Recht, diese Situation in seinem Austrittsstatus widerzuspiegeln. Dann aufgrund der Installation set -o pipefail wird funktionieren || echo "Вы проиграли".

Jedoch head wird es vielleicht nicht rechtzeitig schaffen vorher schließen fortune beendet die Ausgabe der Daten. Dann wird es funktionieren && echo "Повезло!".

In einem meiner heutigen GNUMakefile es gibt solche ein Fragment:

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

Ins Menschliche übersetzt

Es ist hier üblich für GNU machen и bash im Compiler-Weg mit der Option --version Es fragt, wer er ist, und wenn die Option nicht unterstützt wird, wird ein Stub eingefügt „Bitte verwenden Sie einen GCC- oder CLANG-kompatiblen Compiler“.

Ähnlich Kochplatte kann überall gefunden werden. Es erschien vor langer Zeit an dieser Stelle und funktionierte überall perfekt (Linux, Solaris, OSX, FreeBSD, WSL usw.). Aber gestern in altlinux auf der Plattform Elbrus 2000 (E2K) Ich bemerkte:

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

Ehrlich gesagt habe ich meinen alten „Bekannten“ nicht sofort wiedererkannt. Darüber hinaus wurde das Projekt bereits viele Male auf Elbrus und unter vielen verschiedenen Distributionen, einschließlich Alt, getestet. Mit verschiedenen Compilern, Versionen von GNU Make und Bash. Deshalb wollte ich meinen Fehler hier nicht sehen.

Beim Versuch, das Problem zu reproduzieren und/oder zu verstehen, was vor sich ging, begannen noch seltsamere Dinge zu passieren.
Befehlszeilenzauber:

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

Hin und wieder wurde zusätzlicher Text erzeugt, dann aber nicht ... Oft blieb eine der Optionen ziemlich lange hängen, aber wenn man länger herumstocherte, bekam man immer beides!

Natürlich strace unser Alles! Und nachdem ich eine Strace-Tirade getippt hatte, aber keine Zeit hatte, die Eingabetaste zu drücken, erkannte ich meinen alten Freund Herrn Heisenbug und die Entwickler Compiler Ich selbst vor 25 Jahren, Nostalgie… Und ich beschloss, traurig zu sein und diese Notiz zu schreiben 😉

Übrigens, wie jeder, der etwas auf sich hält Heisenbugunter strace zieht es vor, sich nicht zu reproduzieren.

Was ist denn los?

  • Dienstprogramm head hat das Recht (bzw. wird sogar gezwungen), den gelesenen Kanal zu schließen, sobald er die angeforderte Anzahl Zeilen liest.
  • Der datengenerierende Programmschreiber (in diesem Fall cc) können mehrere Zeilen drucken und frei Tun Sie dies durch mehrere Anrufe write().
  • wenn Der Leser hat Zeit, den Kanal auf seiner Seite zu schließen, bevor die Aufzeichnung auf der Seite des Autors endet. Dann erhält der Autor eine Fehlermeldung.
  • Autorenprogramm kann Beide ignorieren den Kanalschreibfehler und geben ihn in Ihrem Abschlusscode wieder.
  • Aufgrund der Installation set -o pipefail Der Pipeline-Abschlusscode ist ungleich Null (fehlerhaft), wenn das Ergebnis von mindestens einem Element ungleich Null ist, und dann funktioniert es || echo "Please use GCC or CLANG compatible compiler".

Abhängig davon, wie das Schreibprogramm mit Signalen arbeitet, kann es zu Abweichungen kommen. Beispielsweise kann das Programm abnormal beendet werden (mit automatischer Generierung eines Beendigungsstatus ungleich Null/Fehler) oder write() gibt das Ergebnis zurück, dass weniger Bytes geschrieben wurden als angefordert und festgelegt errno = EPIPE.

Wer ist schuld?

Im beschriebenen Fall ein bisschen von allem. Fehlerbehandlung in cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) nicht überflüssig. In vielen Fällen ist es besser, auf Nummer sicher zu gehen, auch wenn dadurch plötzliche Mängel in einem auf traditionelles Verhalten ausgelegten Boilerplate aufgedeckt werden.

Was zu tun ist?

Falsch:

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

Richtig:

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

    Hier wird das vorzeitige Schließen einer Pipe vom Befehlsinterpreter gehandhabt, wenn die verschachtelte Pipeline („innerhalb“ der Klammern) bedient wird. Dementsprechend, wenn fortune meldet einen Fehler beim Schreiben in einen geschlossenen Kanal im Status und dann in der Ausgabe || echo "Ошибка" es wird nicht weiterkommen, da der Kanal bereits geschlossen ist.

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

    Hier ist das Dienstprogramm cat wirkt als Dämpfer, da es den Fehler ignoriert EPIPE beim Rückzug. Dies reicht für die Schlussfolgerung fortune klein (mehrere Zeilen) und passt in den Kanalpuffer (von 512 Byte bis ≈64 KB, in den meisten Betriebssystemen ≥ 4 KB). Andernfalls kann das Problem erneut auftreten.

So verarbeiten Sie es richtig EPIPE und andere Aufnahmefehler?

Es gibt keine allgemeingültige Lösung, aber einfache Empfehlungen:

  • EPIPE erforderlich müssen bearbeitet werden (und spiegelt sich im Exit-Status wider), wenn Daten ausgegeben werden, die Integrität erfordern. Zum Beispiel beim Betrieb von Archivierern oder Backup-Dienstprogrammen.
  • EPIPE besser ignorieren bei der Anzeige von Informationen und Zusatzmeldungen. Zum Beispiel bei der Anzeige von Informationen zu Optionen --help oder --version.
  • Wenn der zu entwickelnde Code zuvor in einer Pipeline verwendet werden kann | headdann EPIPE Es ist besser, es zu ignorieren, andernfalls ist es besser, es zu verarbeiten und im Exit-Status zu reflektieren.

Ich möchte diese Gelegenheit nutzen, um den Teams meinen Dank auszusprechen MCST и altlinux für großartige produktive Arbeit. Deine Entschlossenheit ist erstaunlich!
Weiter so, Kameraden, weiter so Treffen im Herbst!

Danke berez zum Korrigieren von Tippfehlern und Fehlern.
KDPV von Georgy A.

Source: habr.com

Kommentar hinzufügen