【macOS】2つ以上のプログラムを同時に実行し、Ctrl-Cで一括終了させるシェルスクリプトを作る紆余曲折
元々私は下のようなシェルスクリプトを作って使っていた。
#!/usr/bin/env bash
fvm dart run build_runner watch --delete-conflicting-outputs &
pid1=$!
fvm dart run slang watch &
pid2=$!
trap 'kill $pid1 $pid2; wait $pid1 $pid2 2>/dev/null; exit' SIGINT SIGTERM
wait $pid1 $pid2
これは、2つのコード生成プログラムを同時に実行させ、特定のファイルが変更されたらコードが再生成されるようにするものだ。そして、Ctrl-Cが入力されるかSIGTERMを受け取ると両方のプログラムを終了させる。
実装が汚いのは一旦おいておいて、ある日ターミナルでこれを実行してみるとあることに気づいた。シェルスクリプトをCtrl-Cで終了させた後、対象のファイルを書き換えてみると ログが出てくる。
調べた結果、スクリプトを終了させたあともdartのプロセスが動き続けていることが分かった。私はFVM(Flutterバージョンマネージャー)を利用しているが、FVMでdartを仲介させた場合、FVMのプロセスがdartのプロセスを立ち上げ、そのdartのプロセスにSIGINTが渡され終了すると親であるFVMも終了する、という流れになっている。しかし、上のスクリプトにおいて、Ctrl-Cした時にkillされるPIDはFVMのものであり、FVMが終了してもdartが終了しない。これは困った。
結論としては、以下のようなスクリプトになった。
#!/usr/bin/env bash
# Watch for changes in code generation sources (build_runner, slang) and rebuild them
# This script is intended to be run in the background while developing
# Watch for changes in build_runner sources
fvm dart run build_runner watch --delete-conflicting-outputs &
# Watch for changes in slang sources
fvm dart run slang watch &
# Trap ctrl-c and interrupt signals to stop the background processes
function cleanup() {
echo -e "\nStopping watchers..."
pkill -g $$
wait
exit 0
}
trap cleanup SIGINT SIGTERM
# Keep the script alive to handle the signals
wait
まずはChatGPTに聞いてみたところ、以下のようなコードが返ってきた。
#!/bin/bash
# Function to kill all background jobs on script exit or Ctrl+C
cleanup() {
echo "Stopping all background processes..."
pkill -P $$ # Kill all child processes of this script
wait # Wait for all processes to terminate
exit 0
}
# Trap SIGINT (Ctrl+C) and call cleanup
trap cleanup SIGINT
# Run multiple processes in background
sleep 100 &
sleep 200 &
sleep 300 &
# Keep the script alive to handle Ctrl+C
wait
肝はこの部分。
pkill -P $$ # Kill all child processes of this script
pkill
コマンドは、-P PPID
とすると、指定したPIDを親プロセスとするプロセス(シェルスクリプトの場合すべてのジョブ)にシグナルを送信する。$$
は現在のプロセスのPIDが入っている。このコードを先程のコードに当てはめると、FVMのプロセスがkillされることになる。これでは先程言ったように、dartが終了しない問題が解決できない。
以後、それを含めてChatGPTと格闘したが、動くコードは返ってこなかった。後になって思えば、おそらく動作環境がmacOSであることを事前に伝えていなかったことが原因だと思う。
結局、古来の方法でネットで資料を探して自力で解決した。
pkill -g $$
-g PGID
オプションは、同じプロセスグループID(PGID)に属するプロセス(自身を除く)をフィルターする。シェルスクリプト実行時、PGIDは自身のPIDであるため、全てのジョブと、ジョブのプロセスが開始したプロセスを一括でkillできる。結局AIに惑わされてこれに気づかず2時間消費してしまった・・・
kill -- -PGID
として、負のPIDをkillコマンドに渡すことでPGIDに属するプロセスをすべてkillすることもできるが、スクリプトのプロセス自身もkillされてしまい余計な出力が発生してしまう。
zsh: terminated ./script.sh.