Satu lagi Heisenbug melepasi buaya

Satu lagi Heisenbug melepasi buaya

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "ПовСзло!" || echo "Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ"
ПовСзло!

$> fortune | head -1 > /dev/null && echo "ПовСзло!" || echo "Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ"
Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ

ia adalah fortune program bersyarat tanpa exit(rand()).

Boleh anda terangkan? apa yang tidak kena di sini?

Penyimpangan lirik-sejarah

Saya mula berkenalan dengan Heisenbug ini seperempat abad yang lalu. Kemudian untuk pintu masuk dalam FaxNET adalah perlu untuk mencipta beberapa utiliti melalui paip "bermain dam" di bawah FreeBSD. Seperti yang dijangkakan, saya menganggap diri saya seorang pengaturcara yang maju dan cukup berpengalaman. Oleh itu, saya berhasrat untuk melakukan segala-galanya dengan berhati-hati dan berhati-hati yang mungkin, memberi perhatian khusus kepada pengendalian ralat...

Pengalaman saya sebelum ini menangani pepijat dalam sendmail dan uucp/upc menambah ketekunan saya dalam "pengendalian ralat menyeluruh." Tidak ada gunanya menyelami butiran cerita itu, tetapi saya bergelut dengan Heisenbug ini selama dua minggu selama 10-14 jam. Oleh itu, teringat, dan semalam kenalan lama ini singgah melawat lagi.

Jawapan TL;DR

Utiliti head boleh tutup saluran dari fortune сразу sebaik sahaja dia membaca baris pertama. Jika fortune mengeluarkan lebih daripada satu baris, kemudian panggilan yang sepadan write() akan mengembalikan ralat atau laporan bahawa lebih sedikit bait dikeluarkan daripada yang diminta. Sebaliknya, ditulis dengan pengendalian ralat yang teliti fortune mempunyai hak untuk menggambarkan keadaan ini dalam status keluarnya. Kemudian disebabkan pemasangan set -o pipefail akan bekerja || echo "Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ".

Walau bagaimanapun, head mungkin tidak sampai pada masanya tutup sebelum ini fortune akan selesai mengeluarkan data. Kemudian ia akan berfungsi && echo "ПовСзло!".

Dalam salah satu hari saya GNUMakefile terdapat satu serpihan:

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

Diterjemah kepada manusia

Ia adalah perkara biasa di sini untuk GNU Buat ΠΈ menampar dengan cara pengkompil menggunakan pilihan --version ia bertanya siapa dia, dan jika pilihan itu tidak disokong, maka tunas dimasukkan "Sila gunakan pengkompil GCC atau CLANG yang serasi".

seperti boilerplate boleh didapati di mana-mana sahaja. Ia muncul di tempat ini lama dahulu dan berfungsi dengan sempurna di mana-mana (Linux, Solaris, OSX, FreeBSD, WSL dan lain-lain.). Tapi semalam masuk altlinux pada platform Elbrus 2000 (E2K) saya perasan:

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

Terus terang, saya tidak segera mengenali "kenalan" saya. Selain itu, projek itu telah diuji berkali-kali di Elbrus dan di bawah banyak pengedaran yang berbeza, termasuk Alt. Dengan pelbagai penyusun, versi GNU Make dan bash. Oleh itu, saya tidak mahu melihat kesilapan saya di sini.

Apabila cuba menghasilkan semula masalah dan/atau memahami perkara yang sedang berlaku, lebih banyak perkara pelik mula berlaku.
Ejaan baris arahan:

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

Sekali-sekala ia akan menghasilkan teks tambahan, kemudian tidak... Selalunya salah satu pilihan akan bertahan lama, tetapi jika anda mencucuk lebih lama, anda sentiasa mendapat kedua-duanya!

Sudah tentu, strace segala-galanya kami! Dan setelah menaip omelan strace, tetapi tidak mempunyai masa untuk menekan Enter, saya mengenali rakan lama saya Encik Heisenbug, dan pemaju penyusun diri saya 25 tahun yang lalu, Nostalgia… Dan saya memutuskan untuk bersedih dan menulis nota ini πŸ˜‰

By the way, seperti mana-mana harga diri Heisenbug, di bawah strace lebih suka untuk tidak membiak.

Jadi apa yang berlaku?

  • Utiliti head mempunyai hak (atau lebih tepatnya, malah dipaksa) untuk menutup saluran yang dibaca sebaik sahaja ia membaca bilangan baris yang diminta.
  • Penulis program penjanaan data (dalam kes ini cc) boleh mencetak berbilang baris dan percuma lakukan ini melalui berbilang panggilan write().
  • Jika pembaca akan mempunyai masa untuk menutup saluran di sebelahnya sebelum tamat rakaman di pihak penulis, maka penulis akan menerima ralat.
  • Program penulis berhak kedua-duanya mengabaikan ralat penulisan saluran dan mencerminkannya dalam kod penyiapan anda.
  • Disebabkan pemasangan set -o pipefail kod penyiapan saluran paip akan menjadi bukan sifar (salah) jika hasilnya bukan sifar daripada sekurang-kurangnya satu elemen, dan kemudian ia akan berfungsi || echo "Please use GCC or CLANG compatible compiler".

Mungkin terdapat variasi bergantung pada cara program penulis berfungsi dengan isyarat. Sebagai contoh, program mungkin ditamatkan secara tidak normal (dengan penjanaan automatik status penamatan bukan sifar/ralat), atau write() akan mengembalikan hasil penulisan lebih sedikit bait daripada yang diminta dan ditetapkan errno = EPIPE.

Siapa yang patut disalahkan?

Dalam kes yang diterangkan sedikit daripada segala-galanya. Ralat semasa mengendalikan masuk cc (lcc:1.23.20:Sepβ€”4-2019:e2k-v3-linux) tidak berlebihan. Dalam kebanyakan kes, adalah lebih baik untuk tersilap berhati-hati, walaupun ini mendedahkan kecacatan secara tiba-tiba dalam boilerplate yang direka untuk tingkah laku tradisional.

Apa yang perlu dilakukan?

salah:

fortune | head -1 && echo "ПовСзло, Π½ΠΎ Π²Ρ‹ рискуСтС!" || echo "WTF?"

Betul:

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

    Di sini, penutupan awal paip akan dikendalikan oleh penterjemah arahan apabila menservis saluran paip bersarang ("dalam" kurungan). Sehubungan itu, jika fortune akan melaporkan ralat secara bertulis kepada saluran tertutup dalam status, kemudian output || echo "Ошибка" ia tidak akan sampai ke mana-mana, kerana saluran itu sudah ditutup.

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

    Inilah utilitinya cat bertindak sebagai peredam kerana ia mengabaikan ralat EPIPE apabila ditarik balik. Ini cukup buat kesimpulan sekarang fortune kecil (beberapa baris) dan muat dalam penimbal saluran (daripada 512 bait hingga β‰ˆ64K, dalam kebanyakan OS β‰₯4K). Jika tidak masalah mungkin kembali.

Cara memproses dengan betul EPIPE dan ralat rakaman lain?

Tiada penyelesaian tunggal yang betul, tetapi terdapat cadangan mudah:

  • EPIPE diperlukan mesti diproses (dan ditunjukkan dalam status keluar) apabila mengeluarkan data yang memerlukan integriti. Contohnya, semasa operasi pengarkib atau utiliti sandaran.
  • EPIPE lebih baik abaikan apabila memaparkan maklumat dan mesej tambahan. Contohnya, apabila memaparkan maklumat tentang pilihan --help atau --version.
  • Jika kod yang dibangunkan boleh digunakan dalam saluran paip sebelum ini | head, Kemudian EPIPE Adalah lebih baik untuk mengabaikan, jika tidak, lebih baik memproses dan mencerminkan dalam status keluar.

Saya ingin mengambil kesempatan ini untuk mengucapkan terima kasih kepada pasukan MCST ΠΈ altlinux untuk kerja produktif yang hebat. Keazaman anda sangat mengagumkan!
Keep it up Rakan-rakan, naik mesyuarat pada musim gugur!

Terima kasih berez untuk membetulkan kesilapan dan kesilapan.
KDPV daripada Georgy A.

Sumber: www.habr.com

Tambah komen