Encore un Heisenbug devant le crocodile

Encore un Heisenbug devant le crocodile

$> set -o pipefail

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

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

il est fortune programme conditionnel sans exit(rand()).

Peux-tu expliquer? qu'est-ce qui ne va pas ici?

Digression lyrique-historique

J'ai découvert ce Heisenbug pour la première fois il y a un quart de siècle. Ensuite pour la passerelle dans FaxNET il a fallu créer plusieurs utilitaires via tuyaux "jouer aux dames" sous FreeBSD. Comme prévu, je me considérais comme un programmeur avancé et assez expérimenté. Par conséquent, j'avais l'intention de tout faire avec le plus grand soin et le plus grand soin possible, en accordant une attention particulière à la gestion des erreurs...

Mon expérience précédente dans la gestion des bogues dans sendmail et uucp/uupc a ajouté à ma diligence dans la « gestion approfondie des erreurs ». Cela ne sert à rien de plonger dans les détails de cette histoire, mais j'ai lutté avec ce Heisenbug pendant deux semaines pendant 10 à 14 heures. Par conséquent, on s'en est souvenu, et hier, cette vieille connaissance est passée nous rendre visite à nouveau.

TL;DR Réponse

Utilitaire head может fermer la chaîne de fortune immédiatement dès qu'il lit la première ligne. Si fortune génère plus d'une ligne, puis l'appel correspondant write() renverra une erreur ou signalera que moins d’octets sont générés que demandé. À son tour, écrit avec une gestion minutieuse des erreurs fortune a le droit de refléter cette situation dans son statut de sortie. Puis à cause de l'installation set -o pipefail travaillera || echo "Вы проиграли".

cependant, head je n'arriverai peut-être pas à temps fermer avant fortune terminera la sortie des données. Alors ça marchera && echo "Повезло!".

Dans l'un de mes aujourd'hui GNUMakefile il y a tel un fragment:

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

Traduit en humain

C'est courant ici pour Marque GNU и bash à la manière du compilateur en utilisant l'option --version il demande qui il est, et si l'option n'est pas prise en charge, alors un stub est inséré "Veuillez utiliser un compilateur compatible GCC ou CLANG".

comme passe-partout peut être trouvé n’importe où. Il est apparu ici il y a longtemps et fonctionnait parfaitement partout (Linux, Solaris, OSX, FreeBSD, WSL etc.). Mais hier, à altlinux sur la plate-forme Elbrouz 2000 (E2K) J'ai remarqué:

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

Franchement, je n’ai pas immédiatement reconnu mon ancienne « connaissance ». De plus, le projet a déjà été testé à plusieurs reprises sur Elbrus et sous de nombreuses distributions différentes, dont Alt. Avec différents compilateurs, versions de GNU Make et bash. Par conséquent, je ne voulais pas voir mon erreur ici.

En essayant de reproduire le problème et/ou de comprendre ce qui se passait, des choses plus étranges ont commencé à se produire.
Sort de ligne de commande :

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

De temps en temps, cela produisait du texte supplémentaire, puis non... Souvent, l'une des options restait assez longtemps, mais si vous pouviez plus longtemps, vous obteniez toujours les deux !

Bien sûr, strace notre tout ! Et ainsi, après avoir tapé une tirade strace, mais n'ayant pas le temps d'appuyer sur Entrée, j'ai reconnu mon vieil ami M. Heisenbug, et les développeurs compilateur moi-même il y a 25 ans, Nostalgie… Et j'ai décidé d'être triste et d'écrire cette note 😉

D'ailleurs, comme tout homme qui se respecte Heisenbugen dessous de strace préfère ne pas se reproduire.

Alors que se passe-t-il?

  • Utilitaire head a le droit (ou plutôt est même obligé) de fermer le canal en cours de lecture dès qu'il lit le nombre de lignes demandé.
  • L'auteur du programme générateur de données (dans ce cas cc) может imprimer plusieurs lignes et gratuit faites-le via plusieurs appels write().
  • si le lecteur aura le temps de fermer la chaîne de son côté avant la fin de l'enregistrement du côté de l'écrivain, celui-ci recevra alors une erreur.
  • Programme d'écriture a le droit les deux ignorent l’erreur d’écriture du canal et la reflètent dans votre code d’achèvement.
  • En raison de l'installation set -o pipefail le code d'achèvement du pipeline sera différent de zéro (erroné) si le résultat est non nul d'au moins un élément, et alors cela fonctionnera || echo "Please use GCC or CLANG compatible compiler".

Il peut y avoir des variations en fonction de la façon dont le programme d'écriture fonctionne avec les signaux. Par exemple, le programme peut se terminer anormalement (avec génération automatique d'un statut de fin différent de zéro/erreur), ou write() renverra le résultat de l'écriture de moins d'octets que demandé et défini errno = EPIPE.

Qui est à blâmer?

Dans le cas décrit Un peu de tout. Gestion des erreurs dans cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) n'est pas redondant. Dans de nombreux cas, il est préférable de pécher par excès de prudence, même si cela révèle des défauts soudains dans un modèle conçu pour un comportement traditionnel.

Que faire?

Mauvais:

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

Correctement:

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

    Ici, la fermeture anticipée d'un canal sera gérée par l'interpréteur de commandes lors de la maintenance du pipeline imbriqué ("à l'intérieur" des parenthèses). En conséquence, si fortune signalera une erreur d'écriture sur un canal fermé dans l'état, puis la sortie || echo "Ошибка" cela n’aboutira à rien, puisque la chaîne est déjà fermée.

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

    Voici l'utilitaire cat agit comme un amortisseur car il ignore l'erreur EPIPE lors du retrait. C'est suffisant pour l'instant conclusion fortune petit (plusieurs lignes) et tient dans le tampon du canal (de 512 octets à ≈64K, dans la plupart des systèmes d'exploitation ≥4K). Sinon, le problème pourrait réapparaître.

Comment traiter correctement EPIPE et autres erreurs d'enregistrement ?

Il n’existe pas de solution unique, mais il existe des recommandations simples :

  • EPIPE requis doit être traité (et reflété dans l'état de sortie) lors de la sortie de données qui nécessitent une intégrité. Par exemple, lors du fonctionnement d'archiveurs ou d'utilitaires de sauvegarde.
  • EPIPE mieux vaut ignorer lors de l'affichage d'informations et de messages auxiliaires. Par exemple, lors de l'affichage d'informations sur les options --help ou --version.
  • Si le code en cours de développement peut être utilisé dans un pipeline avant | headpuis EPIPE Il vaut mieux ignorer, sinon il vaut mieux traiter et refléter dans le statut de sortie.

J'en profite pour exprimer ma gratitude aux équipes MCST и altlinux pour un excellent travail productif. Votre détermination est incroyable !
Continuez ainsi Camarades, debout réunions à l'automne!

merci berez pour corriger les fautes de frappe et les erreurs.
KDPV de Georgy A.

Source: habr.com

Ajouter un commentaire