Zenn
Closed3

【macOS】2つ以上のプログラムを同時に実行し、Ctrl-Cで一括終了させるシェルスクリプトを作る紆余曲折

cqcq

元々私は下のようなシェルスクリプトを作って使っていた。

#!/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で終了させた後、対象のファイルを書き換えてみると ログが出てくる

cqcq

調べた結果、スクリプトを終了させたあとも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

cqcq

まずは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.
このスクラップは2ヶ月前にクローズされました
ログインするとコメントできます