Ένα ακόμα Heisenbug πέρασε τον κροκόδειλο

Ένα ακόμα Heisenbug πέρασε τον κροκόδειλο

$> set -o pipefail

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

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

Εδώ fortune πρόγραμμα υπό όρους χωρίς exit(rand()).

Μπορείς να εξηγήσεις? τι φταίει εδώ?

Λυρικοϊστορική παρέκβαση

Γνώρισα για πρώτη φορά αυτό το Heisenbug πριν από ένα τέταρτο του αιώνα. Στη συνέχεια, για την πύλη στο FaxNET ήταν απαραίτητο να δημιουργηθούν πολλά βοηθητικά προγράμματα μέσω σωλήνες "παίζοντας πούλια" στο FreeBSD. Όπως ήταν αναμενόμενο, θεωρούσα τον εαυτό μου προχωρημένο και αρκετά έμπειρο προγραμματιστή. Ως εκ τούτου, σκόπευα να κάνω τα πάντα όσο το δυνατόν πιο προσεκτικά και προσεκτικά, δίνοντας ιδιαίτερη προσοχή στον χειρισμό σφαλμάτων...

Η προηγούμενη εμπειρία μου στην αντιμετώπιση σφαλμάτων στο sendmail και στο uucp/uupc συνέβαλε στην επιμέλειά μου στον «ενδελεχή χειρισμό σφαλμάτων». Δεν έχει νόημα να βουτήξω στις λεπτομέρειες αυτής της ιστορίας, αλλά πάλεψα με αυτό το Heisenbug για δύο εβδομάδες για 10-14 ώρες. Ως εκ τούτου, το θυμήθηκαν, και χθες αυτός ο παλιός γνώριμος σταμάτησε να επισκεφθεί ξανά.

TL;DR Απάντηση

Χρησιμότητα head κουτί κλείστε το κανάλι από fortune сразу μόλις διαβάσει την πρώτη γραμμή. Αν fortune εξάγει περισσότερες από μία γραμμές και μετά την αντίστοιχη κλήση write() θα επιστρέψει ένα σφάλμα ή θα αναφέρει ότι εξάγονται λιγότερα byte από αυτά που ζητήθηκαν. Με τη σειρά του, γραμμένο με προσεκτικό χειρισμό σφαλμάτων 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')"'

Μεταφρασμένο σε ανθρώπινο

Είναι συνηθισμένο εδώ για Δημιουργία GNU и βίαιο χτύπημα με τον τρόπο μεταγλώττισης χρησιμοποιώντας την επιλογή --version ρωτά ποιος είναι και εάν η επιλογή δεν υποστηρίζεται, τότε εισάγεται ένα στέλεχος "Παρακαλώ χρησιμοποιήστε μεταγλωττιστή συμβατό με GCC ή CLANG".

σαν λέβητα μπορεί να βρεθεί οπουδήποτε. Εμφανίστηκε σε αυτό το μέρος πριν από πολύ καιρό και λειτούργησε τέλεια παντού (Linux, Solaris, OSX, FreeBSD, WSL και τα λοιπά.). Αλλά χθες στο altlinux στην πλατφόρμα Elbrus 2000 (E2K) Παρατήρησα:

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

Ειλικρινά, δεν αναγνώρισα αμέσως τον παλιό μου «γνωστό». Επιπλέον, το έργο έχει ήδη δοκιμαστεί πολλές φορές στο Elbrus και σε πολλές διαφορετικές διανομές, συμπεριλαμβανομένου του Alt. Με διάφορους μεταγλωττιστές, εκδόσεις του 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 tirade, αλλά δεν είχα χρόνο να πατήσω Enter, αναγνώρισα τον παλιό μου φίλο κ. Heisenbug και τους προγραμματιστές μεταγλωττιστής τον εαυτό μου πριν από 25 χρόνια, Νοσταλγία… Και αποφάσισα να στεναχωρηθώ και να γράψω αυτό το σημείωμα 😉

Παρεμπιπτόντως, όπως κάθε άλλος που σέβεται τον εαυτό του Heisenbug, κάτω από strace προτιμά να μην αναπαράγει.

Λοιπόν τι συμβαίνει?

  • Χρησιμότητα head έχει το δικαίωμα (ή μάλλον, αναγκάζεται) να κλείσει το κανάλι που διαβάζεται μόλις διαβάσει τον αριθμό των γραμμών που ζητήθηκε.
  • Το πρόγραμμα-εγγραφής που δημιουργεί δεδομένα (σε αυτή την περίπτωση cc) κουτί εκτύπωση πολλαπλών γραμμών και Ελεύθερος το κάνετε αυτό μέσω πολλαπλών κλήσεων write().
  • αν ο αναγνώστης θα έχει χρόνο να κλείσει το κανάλι από την πλευρά του πριν από το τέλος της ηχογράφησης από την πλευρά του συγγραφέα, τότε ο συγγραφέας θα λάβει ένα σφάλμα.
  • Πρόγραμμα συγγραφέα με τίτλο Και οι δύο αγνοούν το σφάλμα εγγραφής καναλιού και το αντικατοπτρίζουν στον κωδικό ολοκλήρωσής σας.
  • Λόγω εγκατάστασης set -o pipefail ο κωδικός ολοκλήρωσης του αγωγού θα είναι μη μηδενικός (λανθασμένος) εάν το αποτέλεσμα είναι μη μηδενικό από τουλάχιστον ένα στοιχείο και, στη συνέχεια, θα λειτουργήσει || echo "Please use GCC or CLANG compatible compiler".

Μπορεί να υπάρχουν παραλλαγές ανάλογα με τον τρόπο λειτουργίας του προγράμματος εγγραφής με τα σήματα. Για παράδειγμα, το πρόγραμμα μπορεί να τερματιστεί ασυνήθιστα (με αυτόματη δημιουργία κατάστασης τερματισμού μη μηδενικού/λάθους) ή write() θα επιστρέψει το αποτέλεσμα της εγγραφής λιγότερων byte από αυτά που ζητήθηκαν και ορίστηκαν errno = EPIPE.

Ποιος φταίει;

Στην περιγραφόμενη περίπτωση λίγο από όλα. Σφάλμα χειρισμού cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) δεν είναι περιττός. Σε πολλές περιπτώσεις, είναι προτιμότερο να είστε προσεκτικοί, αν και αυτό αποκαλύπτει ξαφνικά ελαττώματα σε μια πλάκα λέβητα που έχει σχεδιαστεί για παραδοσιακή συμπεριφορά.

Τι να κάνω;

Λανθασμένος:

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

Σωστά:

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

    Εδώ, το πρόωρο κλείσιμο ενός σωλήνα θα διεκπεραιωθεί από τον διερμηνέα εντολών κατά τη συντήρηση του ένθετου αγωγού ("εντός" των παρενθέσεων). Αντίστοιχα, εάν fortune θα αναφέρει ένα σφάλμα εγγράφως σε ένα κλειστό κανάλι στην κατάσταση και μετά στην έξοδο || echo "Ошибка" δεν θα φτάσει πουθενά, αφού το κανάλι είναι ήδη κλειστό.

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

    Εδώ είναι το βοηθητικό πρόγραμμα cat λειτουργεί ως αποσβεστήρας επειδή αγνοεί το σφάλμα EPIPE κατά την απόσυρση. Αυτό αρκεί για τώρα το συμπέρασμα fortune μικρό (πολλές γραμμές) και χωράει στην προσωρινή μνήμη καναλιού (από 512 byte έως ≈64K, στα περισσότερα λειτουργικά συστήματα ≥4K). Διαφορετικά το πρόβλημα μπορεί να επανέλθει.

Πώς να επεξεργαστείτε σωστά EPIPE και άλλα σφάλματα εγγραφής;

Δεν υπάρχει ενιαία σωστή λύση, αλλά υπάρχουν απλές συστάσεις:

  • EPIPE απαιτείται πρέπει να υποβληθεί σε επεξεργασία (και αντικατοπτρίζεται στην κατάσταση εξόδου) κατά την έξοδο δεδομένων που απαιτούν ακεραιότητα. Για παράδειγμα, κατά τη λειτουργία αρχειοθέτησης ή βοηθητικών προγραμμάτων δημιουργίας αντιγράφων ασφαλείας.
  • EPIPE καλύτερα να αγνοήσετε κατά την εμφάνιση πληροφοριών και βοηθητικών μηνυμάτων. Για παράδειγμα, όταν εμφανίζονται πληροφορίες για επιλογές --help ή --version.
  • Εάν ο κώδικας που αναπτύσσεται μπορεί να χρησιμοποιηθεί σε μια διοχέτευση πριν | head, Στη συνέχεια EPIPE Είναι καλύτερα να αγνοήσετε, διαφορετικά είναι καλύτερο να επεξεργαστείτε και να αναλογιστείτε στην κατάσταση εξόδου.

Με αυτή την ευκαιρία θα ήθελα να εκφράσω τις ευχαριστίες μου στις ομάδες MCST и altlinux για εξαιρετική παραγωγική δουλειά. Η αποφασιστικότητά σας είναι εκπληκτική!
Συνεχίστε έτσι Καμαράδες, επάνω συναντήσεις το φθινόπωρο!

σας ευχαριστώ berez για διόρθωση τυπογραφικών σφαλμάτων και λαθών.
KDPV από Γεώργιος Α.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο