Tailwindcss:watchが停止シグナルを子プロセスへ転送

rails/tailwindcss-rails

Tailwindcss:watch タスクがプロセスマネージャからの停止シグナルを子プロセスに転送し、オーファン化を防止します。Foreman が Rails の Rake プロセスに SIGTERM を送ると、Rails 側は正常に終了しますが、内部で spawn された tailwindcss プロセスが残り続けるケースが報告されていました。この PR は、シグナルハンドリングを統一的に扱うユーティリティを導入し、子プロセスのライフサイクルを管理することで問題を解決します。さらに、タスク実行後に元のシグナルハンドラを復元することで、プロセス全体の状態を汚染しない設計となっています。

背景

開発環境で foremanSIGTERM を送信した際、Rails の Rake プロセスは終了するものの、tailwindcss:watch が spawn した Tailwind CSS のウォッチャーが孤児プロセスとして残る問題がありました。Foreman はプロセス全体にシグナルを送るため、子プロセスはシグナルを受け取れず、OS 上で実行し続けてリソースを消費します。この状態は開発者が手動でプロセスを殺す必要が生じ、作業効率を低下させました。PR はこのシグナル転送の欠如を直接的に修正することを目的としています。

技術的な変更

Tailwindcss::ProcessRunner モジュールが新規に追加され、シグナル転送ロジックをカプセル化しました。モジュール内部で FORWARDED_SIGNALS 定数 ( %w[INT TERM] ) を定義し、転送対象シグナルを明示しています。モジュールは spawn_and_wait メソッドを提供し、子プロセスの生成から終了待機、シグナル転送、元ハンドラの復元までを一貫して行います。

spawn_and_wait はまず対象シグナルを trap で捕捉し、受信したシグナルを変数に保持します。子プロセスが生成されたら保存されたシグナルを即座に転送し、Process.wait で子プロセスが終了するまでブロックします。終了後は trap に保存した元ハンドラを再設定し、プロセス全体のシグナル状態を元に戻します。メソッドは転送されたシグナル名(例: "TERM")または子プロセスが自然終了した場合は nil を返します。

module Tailwindcss
  module ProcessRunner
    FORWARDED_SIGNALS = %w[INT TERM].freeze

    class << self
      def spawn_and_wait(env, *command)
        pid = nil
        received_signal = nil
        previous_traps = {}

        forward_signal = ->(signal) do
          Process.kill(signal, pid) if pid
        rescue Errno::ESRCH
          # child already exited
        end

        FORWARDED_SIGNALS.each do |signal|
          previous_traps[signal] = trap(signal) do
            received_signal ||= signal
            forward_signal.call(signal)
          end
        end

        pid = Process.spawn(env, *command)
        forward_signal.call(received_signal) if received_signal
        Process.wait(pid)
        pid = nil
        FORWARDED_SIGNALS.each { |s| trap(s, previous_traps[s]) }
        received_signal
      end
    end
  end
end

lib/tasks/build.raketailwindcss:watch タスクは、従来の system 呼び出しから Tailwindcss::ProcessRunner.spawn_and_wait へ置き換えられました。タスクは戻り値として受信したシグナル名を取得し、verbose オプションが有効な場合にログ出力します。これによりタスクはシグナル処理を自前で実装する必要がなくなり、コードがシンプルかつテスト可能になっています。

namespace :tailwindcss do
  task :watch do |t, args|
    env = Tailwindcss::Commands.command_env(verbose: verbose)
    puts "Running: #{Shellwords.join(command)}" if verbose

    received_signal = Tailwindcss::ProcessRunner.spawn_and_wait(env, *command)
    puts "Received #{received_signal}, exiting tailwindcss:watch" if verbose && received_signal
  end
end

設計判断

シグナル転送機構を専用モジュール Tailwindcss::ProcessRunner に切り出すことで、Rake タスク自体はプロセス実行の呼び出しだけに集中できます。この分離はテスト容易性を高め、ProcessRunner が他のタスクやスクリプトでも再利用できるように設計されたことを示しています。ハンドラ復元ロジックをメソッド内部で完結させたため、タスク実行後にグローバルなシグナル状態が汚染されるリスクを排除しています。設計上のトレードオフとしては、シグナルハンドリングの抽象化に伴う若干のオーバーヘッドが発生しますが、開発環境の安定性向上という利益がそれを上回ります。

まとめ

今回の変更は、tailwindcss:watch がプロセスマネージャからの停止シグナルを正しく子プロセスへ転送し、孤児プロセス化を防止することを実現しました。Tailwindcss::ProcessRunner.spawn_and_wait による一元化されたシグナル管理は、コードの可読性とテスト性を向上させ、将来的な類似タスクへの適用も容易です。開発者は余計な手動クリーンアップなしで安全に開発サイクルを終了できるようになります。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
cd71b31d

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文→背景→技術的な変更→設計判断→まとめの流れが明確で、必須要素がすべて揃っています。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい ````ruby:ファイルパス` 形式ですが、PRリンクが `[PR #621](URL)` となっており、仕様の `[#621](URL)` 形式と完全には一致していません。

対象読者への適合性 ✓ PASS

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

対象はRails開発者向けで、専門用語が適切に使われており、初心者向けの過剰な説明はありません。

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

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

各セクションは総論→各論→結論の構成で、段落はトピックセンテンスで始まり、一段落一トピック、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事のコードブロックは提供されたDiffと内容が合致しており、変更点(process_runner.rb と build.rake の置換)が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

SIGINT、SIGTERM、trap、Process.spawn などの用語が正しく使用されています。

説明の技術的正確性 ✓ PASS

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

技術的な説明は Diff と PR の記述に基づいており、誤りや矛盾はありません。

事実の突合 ✓ PASS

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

記事の全主張は PR のタイトル・説明・Diff で裏付けられており、憶測や根拠のない記述はありません。

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

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

PR 番号 #621 など数値は正確です。その他の数値は使用されていません。

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

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

記事タイトルは PR タイトル「Forward stop signals to tailwindcss watcher」の意味を日本語で正確に表現しています。

外部知識の正確性 ✓ PASS

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

外部のバージョンサポート情報やリリース日程等、PR に記載されていない情報は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現に関する記述はなく、PR 内容と矛盾する表現もありません。