Nog een Heisenbug voorbij de krokodil

Nog een Heisenbug voorbij de krokodil

$> set -o pipefail

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

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

Hier fortune voorwaardelijk programma zonder exit(rand()).

Kun je uitleggen? wat is hier mis?

Lyrisch-historische uitweiding

Een kwart eeuw geleden maakte ik voor het eerst kennis met deze Heisenbug. Vervolgens was het voor de gateway in FaxNET nodig om verschillende hulpprogramma's via te maken leidingen "dammen spelen" onder FreeBSD. Zoals verwacht beschouwde ik mezelf als een gevorderde en redelijk ervaren programmeur. Daarom was ik van plan alles zo zorgvuldig en zorgvuldig mogelijk te doen, met speciale aandacht voor de foutafhandeling...

Mijn eerdere ervaring met het omgaan met bugs in sendmail en uucp/uupc droeg bij aan mijn ijver in “grondige foutafhandeling.” Het heeft geen zin om in de details van dat verhaal te duiken, maar ik worstelde twee weken lang met deze Heisenbug gedurende 10-14 uur. Daarom werd het herdacht, en gisteren kwam deze oude bekende langs om opnieuw op bezoek te komen.

TL; DR Antwoord

Nut head kan sluit het kanaal af fortune onmiddelijk zodra hij de eerste regel leest. Als fortune meer dan één lijn uitvoert, en vervolgens de bijbehorende oproep write() zal een fout retourneren of rapporteren dat er minder bytes worden uitgevoerd dan gevraagd. Op zijn beurt geschreven met zorgvuldige foutafhandeling fortune heeft het recht om deze situatie weer te geven in zijn exitstatus. Dan vanwege installatie set -o pipefail zal werken || echo "Вы проиграли".

Echter head lukt het misschien niet op tijd eerder sluiten fortune zal het uitvoeren van gegevens voltooien. Dan zal het werken && echo "Повезло!".

In een van mijn vandaag GNUMakefile er is zoiets fragment:

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

Vertaald in de mens

Het is hier gebruikelijk GNU-merk и slaan op de manier van de compiler met behulp van de optie --version er wordt gevraagd wie hij is, en als de optie niet wordt ondersteund, wordt er een stub ingevoegd "Gebruik een GCC- of CLANG-compatibele compiler".

zoals standaardtekst overal te vinden. Het verscheen lang geleden op deze plek en werkte overal perfect (Linux, Solaris, OSX, FreeBSD, WSL enz.). Maar gisteren binnen altlinux op het platform Elbrus 2000 (E2K) Ik merkte op:

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

Eerlijk gezegd herkende ik mijn oude ‘kennis’ niet meteen. Bovendien is het project al vele malen getest op Elbrus en onder veel verschillende distributies, waaronder Alt. Met verschillende compilers, versies van GNU Make en bash. Daarom wilde ik mijn fout hier niet zien.

Toen ik probeerde het probleem te reproduceren en/of te begrijpen wat er aan de hand was, begonnen er meer vreemde dingen te gebeuren.
Opdrachtregelspelling:

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

Zo nu en dan leverde het extra tekst op, en dan weer niet... Vaak bleef een van de opties behoorlijk lang hangen, maar als je langer prikte, kreeg je altijd beide!

Natuurlijk strace ons alles! En nadat ik een strace-tirade had getypt, maar geen tijd had om op Enter te drukken, herkende ik mijn oude vriend, de heer Heisenbug, en de ontwikkelaars compiler mezelf 25 jaar geleden, Nostalgie… En ik besloot verdrietig te zijn en dit briefje te schrijven 😉

Trouwens, zoals elke zichzelf respecterende Heisenbug, onder strace geeft er de voorkeur aan zich niet voort te planten.

Dus wat is er aan de hand?

  • Nut head heeft het recht (of beter gezegd, wordt zelfs gedwongen) om het gelezen kanaal te sluiten zodra het het gevraagde aantal regels leest.
  • De gegevensgenererende programmaschrijver (in dit geval cc) kan meerdere regels afdrukken en vrij doe dit via meerdere oproepen write().
  • als de lezer heeft tijd om het kanaal aan zijn kant te sluiten vóór het einde van de opname aan de kant van de schrijver, waarna de schrijver een foutmelding krijgt.
  • Schrijver programma gerechtigd beide negeren de kanaalschrijffout en geven deze weer in uw voltooiingscode.
  • Wegens installatie set -o pipefail de pijplijnvoltooiingscode zal niet nul zijn (foutief) als het resultaat van ten minste één element niet nul is, en dan zal het werken || echo "Please use GCC or CLANG compatible compiler".

Er kunnen variaties zijn, afhankelijk van hoe het schrijfprogramma met signalen werkt. Het programma kan bijvoorbeeld abnormaal eindigen (met automatische generatie van een niet-nul/foutbeëindigingsstatus), of write() zal het resultaat retourneren van het schrijven van minder bytes dan gevraagd en ingesteld errno = EPIPE.

Wie is de schuldige?

In het beschreven geval een beetje van alles. Foutafhandeling binnen cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) is niet overbodig. In veel gevallen is het beter om het zekere voor het onzekere te nemen, hoewel dit wel plotselinge gebreken aan het licht brengt in een standaard die is ontworpen voor traditioneel gedrag.

Wat te doen?

Mis:

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

corrigeren:

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

    Hier wordt het vroegtijdig sluiten van een pijpleiding afgehandeld door de opdrachtinterpreter bij het onderhouden van de geneste pijpleiding ("binnen" de haakjes). Dienovereenkomstig, als fortune rapporteert een fout bij het schrijven naar een gesloten kanaal in de status en vervolgens naar de uitgang || echo "Ошибка" het komt nergens, omdat het kanaal al gesloten is.

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

    Hier is het hulpprogramma cat werkt als een demper omdat het de fout negeert EPIPE bij intrekking. Dit is voor nu voldoende conclusie fortune klein (meerdere regels) en past in de kanaalbuffer (van 512 bytes tot ≈64K, in de meeste besturingssystemen ≥4K). Anders kan het probleem terugkeren.

Hoe correct te verwerken EPIPE en andere opnamefouten?

Er is niet één juiste oplossing, maar er zijn eenvoudige aanbevelingen:

  • EPIPE nodig moet worden verwerkt (en weerspiegeld in de afsluitstatus) bij het uitvoeren van gegevens waarvoor integriteit vereist is. Bijvoorbeeld tijdens de werking van archiverings- of back-uphulpprogramma's.
  • EPIPE beter te negeren bij het weergeven van informatie en hulpberichten. Bijvoorbeeld bij het weergeven van informatie over opties --help of --version.
  • Als de code die wordt ontwikkeld eerder in een pijplijn kan worden gebruikt | headdan EPIPE Het is beter om te negeren, anders is het beter om te verwerken en te reflecteren in de exit-status.

Ik wil graag van deze gelegenheid gebruik maken om mijn dank uit te spreken aan de teams MCST и altlinux voor geweldig productief werk. Je vastberadenheid is geweldig!
Ga zo door Camarades, blijf doorgaan bijeenkomsten in het najaar!

Dank berez voor het corrigeren van typefouten en fouten.
KDPV van Georgy A.

Bron: www.habr.com

Voeg een reactie