ワニを越えたもう一頭のハイゼンバグ

ワニを越えたもう一頭のハイゼンバグ

$> set -o pipefail

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

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

それは fortune 条件なしのプログラム exit(rand()).

説明できますか? ここで何が間違っているのか?

叙情的歴史的余談

私がこのハイゼンバグを初めて知ったのは今から四半世紀前です。 次に、FaxNET のゲートウェイ用に、次の方法でいくつかのユーティリティを作成する必要がありました。 パイプ FreeBSD での「チェッカーの再生」。 予想どおり、私は自分自身を高度でかなり経験豊富なプログラマーであると考えていました。 そのため、エラー処理には特に注意しながら、できるだけ慎重に慎重に作業を行ったつもりでしたが…。

以前に sendmail と uucp/uupc のバグに対処した経験により、「徹底したエラー処理」に対する私の熱心さがさらに高まりました。 その話の詳細を掘り下げる意味はありませんが、私はこのハイゼンバグと10週間、14〜XNUMX時間格闘しました。 それで、それを思い出して、昨日、その旧知の人がまた立ち寄ってくれました。

TL;DR 回答

ユーティリティ head ~からのチャンネルを閉じる fortune 一度に 彼が最初の行を読むとすぐに。 もし fortune 複数行を出力し、その後、対応する呼び出しを出力します。 write() エラーを返すか、要求されたバイト数よりも少ないバイト数が出力されたことを報告します。 また、エラー処理を慎重に行って書かれています 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メイク и bash オプションを使用したコンパイラ方式で --version 彼が誰であるかを尋ねられ、オプションがサポートされていない場合は、スタブが挿入されます。 「GCC または CLANG 互換のコンパイラを使用してください」.

以下のような ボイラープレート どこでも見つけることができます。 それはずっと前にこの場所に登場し、どこでも完璧に動作しました (Linux、Solaris、OSX、FreeBSD、 WSL 等。)。 しかし昨日は altlinux プラットフォーム上で エルブルス 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")'"

時々、余分なテキストが生成されたり、生成されなかったりします... 多くの場合、オプションの XNUMX つがかなり長い間表示されますが、より長く突き続けると、常に両方が表示されます。

もちろん、 strace 私たちのすべて! そして、激しい攻撃を入力しましたが、Enter キーを押す時間がなかったので、古い友人のハイゼンバグ氏と開発者に気づきました。 コンパイラ 25年前の私も、 懐かしさ… そして私は悲しくなってこのメモを書くことにしました😉

ちなみに、自尊心のある人たちと同じように、 ハイゼンバグ、 下 strace 再現しないことを好みます。

どうしたの?

  • ユーティリティ head は、要求された行数を読み取るとすぐに、読み取られているチャネルを閉じる権利を持っています (または、強制さえされています)。
  • データを生成するプログラム作成者 (この場合は cc) 複数行を印刷し、 無料 複数の呼び出しを通じてこれを行う write().
  • もし ライター側での記録が終了する前にリーダー側でチャンネルを閉じる時間があり、ライター側でエラーが発生します。
  • ライタープログラム 権利がある どちらもチャネル書き込みエラーを無視し、完了コードに反映します。
  • 設置の都合上 set -o pipefail 少なくとも XNUMX つの要素の結果がゼロ以外の場合、パイプライン完了コードはゼロ以外 (エラー) になり、その後は機能します。 || echo "Please use GCC or CLANG compatible compiler".

ライター プログラムが信号をどのように処理するかによって異なる場合があります。 たとえば、プログラムが異常終了する (ゼロ以外の/エラー終了ステータスが自動的に生成される)、または write() 要求および設定されたバイト数よりも少ないバイト数を書き込んだ結果を返します。 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 バイトから約 64K、ほとんどの OS ≧ 4K)。 そうしないと、問題が再発する可能性があります。

正しい処理方法 EPIPE その他の録音エラーはありますか?

唯一の正しい解決策はありませんが、簡単な推奨事項があります。

  • EPIPE 必須 処理する必要があります 整合性が必要なデータを出力するとき (および終了ステータスに反映されます)。 たとえば、アーカイバやバックアップ ユーティリティの操作中などです。
  • EPIPE 無視したほうがいい 情報や補助メッセージを表示するとき。 例えばオプション情報を表示する場合 --help または --version.
  • 開発中のコードが事前にパイプラインで使用できるかどうか | headその後 EPIPE 無視する方が良いですが、そうでない場合は処理して終了ステータスに反映する方が良いです。

この場を借りてチームの皆様に感謝の意を表したいと思います MCST и altlinux 素晴らしい生産的な仕事のために。 あなたの決断力は素晴らしいです!
それを続けてください、仲間たち、続けてください 秋の会合!

感謝 ベレス タイプミスやエラーを修正するため。
KDPVから ジョージー A.

出所: habr.com

コメントを追加します