ffmpegのstdin接続を遮断してTTOUシグナルによるRailsハングを修正

rails/rails

ActiveStorageのビデオプレビュー生成時に発生するRailsプロセスのハングアップを、ffmpegのstdinを /dev/null に接続することで解消します。

背景

ffmpegは起動時に tcsetattr を呼び出して制御端末の設定を行い、q キーによる終了などのキー入力を監視します。この挙動が、特定のプロセス管理環境下でRailsを停止させるシグナルを引き起こしていました。

問題は、Railsが自身のプロセスグループを持つ形で起動された場合に顕在化します。overman のように pgroup: true でプロセスを生成するプロセスマネージャを使用している環境では、ffmpegが制御端末に書き込もうとした際にバックグラウンドプロセスグループのメンバーに送られる TTOU(SIGTTOU)シグナル をRailsプロセスが受信します。SIGTTOUのデフォルト動作はプロセスの停止(STOP)であるため、Railsは無期限にハングしてしまいます。

Railsに IO.popen でffmpegを起動する際、stdinをそのまま引き継いでいたことが原因です。ffmpegはstdinが端末であると判断して tcsetattr を呼び出し、バックグラウンドプロセスからの端末制御操作としてSIGTTOUが発火するという連鎖が起きていました。

技術的な変更

activestorage/lib/active_storage/previewer.rbcapture メソッドに対し、IO.popen の呼び出しに in: IO::NULL オプションを追加する1行の変更が加えられました。

変更前:

IO.popen(argv, err: err) { |out| IO.copy_stream(out, to) }

変更後:

IO.popen(argv, in: IO::NULL, err: err) { |out| IO.copy_stream(out, to) }

IO::NULL はRubyの定数で、プラットフォームに応じたnullデバイス(Unixでは /dev/null、Windowsでは NUL)を参照します。stdinをnullデバイスに接続することで、ffmpegはstdinが端末ではないと判断し、tcsetattr による端末設定を試みなくなります。

この修正は Previewer 基底クラスの capture メソッドに適用されているため、VideoPreviewer だけでなく Previewer を継承するすべてのプレビュー処理に影響します。PR内ではffmpegに限らずActiveStorageのPreviewerにstdinを提供する理由がないとの判断から、基底クラスレベルで修正が行われています。なお、IO.popen("ffprobe"...) を使用する箇所については、ffprobeがstdinに対して特殊な操作をしないとの判断から変更対象外とされています。

設計判断

VideoPreviewer に限定せず Previewer 基底クラスで修正する方式 が採用されました。

PRでは -nostdin をffmpegの引数に追加する代替案も言及されています。しかし、その方法はffmpeg固有の対処に留まります。基底クラスで in: IO::NULL を指定する方式はより汎用的で、将来追加されるPreviewerが誤ってstdinを引き継ぐリスクを防ぎます。

ActiveStorageのPreviewerは外部プロセスを起動してその標準出力をファイルにキャプチャする用途に特化しており、stdinへのアクセスを必要とするユースケースが存在しません。この前提に基づき、基底クラスで一律にstdinを遮断することが適切と判断されています。

まとめ

たった1行のオプション追加でありながら、この修正はプロセスグループとシグナルという低レイヤーの挙動に起因するハングアップを根本から解消します。外部プロセスを起動する際にstdinを明示的に制御するという原則が、Previewer 基底クラスへの適用という形で設計に組み込まれた変更です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
f4bdf9ee

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→セクション群(各論)→まとめ(結論)」の構成が明確です。背景、技術的変更、設計判断の各セクションが論理的に配置されており、非常に理解しやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライト(```ruby:ファイルパス)や、ソースPRへのリンクが正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「SIGTTOU」「プロセスグループ」「tcsetattr」といった用語を前提として説明しており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが「総論→各論」の構造を持ち、トピックセンテンスが段落の冒頭に配置されているため、非常に読みやすいです。1段落1トピックの原則も守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは、提供されたDiffの変更内容(`in: IO::NULL` の追加)を正確に反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「TTOU」「tcsetattr」「IO.popen」などの技術用語は、PRの情報と一致しており、文脈上も正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

SIGTTOUシグナルによるハングアップのメカニズムと、`in: IO::NULL`による解決策の説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(問題の背景、代替案、修正範囲など)は、提供されたPRのDescriptionによって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57201)やファイルパス、クラス名などの固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRのタイトル「Fix TTOU hanging Rails when ffmpeg is spawned for video previews」の内容を的確に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PRに記載のない捏造された外部知識(バージョン情報やリリース予定など)は含まれていません。`IO::NULL`に関する補足説明は、技術的に自明な範囲の妥当な解説です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内の時間表現は、現在発生している問題を修正するというPRの内容と一致しており、歪曲はありません。