はじめに
先日、「macOSでBashスクリプトを常駐プロセスとしてログイン時に起動させる」という記事を書いたのですが、その後、ちょっとしたミスに気がついてしまいました。件のページで紹介しているのは、あるディレクトリ配下を監視して、そこに作られたGitリポジトリのメールアドレスを変更するというものだったのですが、対象となるディレクトリは1つであるという前提が間違っており、実際は4つもあることが判明しました。
1つの常駐プロセスを起動するために1つのプロパティリストを必要とし、1つの常駐プロセスが1つのディレクトリを監視するなら、4つのディレクトリのために4つのプロパティリストが必要になります。もちろん4つ作成すればよいのですが、ほとんど同じ目的をもったほとんど同じような内容のプロパティリストを作成し、管理するというのは、なかなかプライドの傷つけられる状態です。また、対象ディレクトリが増える度にプロパティリストをコピペして改変するという行為を想像するだけで自分が無能過ぎて死にたくなります。そこで今回は、1つのプロパティリストで複数の常駐プロセスを起動する方法について検討します。
方針
同じコマンドだが引数だけが違う
1つのプロパティリストで複数のプロセスを起動するのであれば、複数の子プロセスを起動する親プロセスを起動するように設定すればよいでしょう。
今回の目的は、「ディレクトリを定期的に監視してGitリポジトリに間違ったEmailをコミットしない」で作成したスクリプトを複数プロセス起動することです。このスクリプトは下記のように実行します。
$ watch-git-email.sh /PATH/TO/DIRECTORY/ROOT expect-email@example.com &
今回、監視対象ディレクトリが4つに増えたという仕様なので、この第1引数以外は全く同じコマンドを4回実行することになります。ループで特定の引数だけを変化させれば簡単に実行できそうです。
停止するときは起動した全てのプロセスを停止したい
親プロセスを停止したいときには、起動した全ての子プロセスを停止したいはずです。1つ1つ子プロセスを停止すると考えただけで気が狂いそうになりますから、親プロセスを停止する際に子プロセスも全て停止します。
起動したプロセスが落ちたら通知したい
起動したプロセスが常に起動し続けることを保証することはできません。できれば停止したプロセスを再起動してほしいのですが、そこまではしなくとも、死んだプロセスを知らせるくらいはしてほしいものです。プロセスが複数になると、全てのプロセスの起動を目で確認するわけにもいかないので、きちんと起動したこと、そして起動し続けていることを自動的にチェックします。
実装
そんなこんなで、下記のとおり、複数のプロセスを起動するスクリプトを作成することで、1つのプロパティリストで複数プロセスを起動することができました。
#!/usr/bin/env bash
set -eu
COMMAND="/PATH/TO/watch-git-email.sh"
REPOSITORY_LIST="/PATH/TO/watch-repositories.list"
CORRECT_EMAIL="correct-email@example.com"
MONITORING_INTERVAL=3600
PATH="/usr/local/bin:$PATH"
trap "pkill -KILL -f $COMMAND" EXIT
while read; do
$COMMAND $REPLY $CORRECT_EMAIL &
done < $REPOSITORY_LIST
while true; do
while read; do
pgrep -fq $REPLY || terminal-notifier -title "Process not founds" -subtitle "$(basename $0)" -message "$REPLY"
done < $REPOSITORY_LIST
sleep $MONITORING_INTERVAL
done
対象ディレクトリのリストは別ファイルに切り出しておきました。
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/1
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/2
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/3
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/4
これを起動するプロパティリストは下記のとおりです。最小限の設定しかしていません。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>watch-repositories</string>
<key>ProgramArguments</key>
<array>
<string>/ABSOLUTE/PATH/TO/watch-repositories.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
解説
実行スクリプト
親プロセスの起動
親プロセスを起動する方法が以前の記事で書いたものと全く同じです。 今回はプロセスの死活をチェックする(後述)ためにこの値を1時間に設定していますが、違いはそれだけです。
while true; do
sleep $MONITORING_INTERVAL
done
子プロセスの起動
子プロセスの起動は、外部ファイルに保存したリポジトリのPATHのリストを標準入力から読み込み、 while read
で1行ずつ読みとり、読み取った値を引数に与えてバックグラウンドでコマンドを実行するという方法をとっています。文にするとちょっと長いですが、下記の通り簡単です。
while read; do
$COMMAND $REPLY $CORRECT_EMAIL &
done < $REPOSITORY_LIST
子プロセスの停止
pkill(1)
, pgrep(1)
子プロセスの停止には pkill
コマンドを使っています。 pkill
は、プロセステーブルを読み込んで、キーワードにマッチするプロセスすべてにシグナルを送る機能を有しています。もちろん、この場合に送りたいシグナルは KILL
です。
pkill -KILL -f /PATH/TO/COMMAND
これで起動した子プロセスをすべて停止させることができます。
デフォルトではマッチング対象がプロセス名だけなのですが、 -f
オプションをつけることで引数全体にもマッチング範囲が広がります。ただ、ここで言うプロセス名とは部分的な文字列のみを指しているようでして、PATHを含んだプログラム名と確実にマッチさせたい場合は、とりあえずこのオプションをつけてを与えておいた方が無難かと思います。
trap(1)
停止する方法はわかったのですが、親プロセスを停止するときにこのコマンドを実行するのはどうすれば良いのでしょうか?答えは trap
です。
trap
はプログラムが何らかのシグナルをキャッチした際に実行するコマンドを管理するbashのビルトインコマンドです。プログラムが停止/終了する際にはシステムから何らかのシグナル受け取ります(と考えてください)。この受け取ったシグナルにしたがってコマンドを実行したい際に、下記のように trap
で指示しておきます。
trap "/PATH/TO/COMMAND ARG1 ARG2" KILL
指定できるシグナルは kill -l
で調べることができます。しかし前述のスクリプトに記載されている EXIT
は含まれていません。これは疑似シグナルと呼ばれ、いくつかのシグナルを束ねたものだと考えると良いんじゃないかと思います。 EXIT
はプログラムが終了したことを示す疑似シグナルです。終了した理由を問いません。
この trap
コマンドがよく使われる場面としては、一時ファイルの事後処理などがあります。プログラム中で一時ファイルが必要となる場面は多いですが、そのファイルを残しておきたくありません。処理の最後に一時ファイルを削除する処理を入れていたとしても、処理の途中にエラーで停止してしまった場合には削除されません。 trap
を使えばそのような場合にも確実にファイルを削除できます。
子プロセスの起動チェック
子プロセスを起動したら、起動した子プロセスが正常に起動し続けているかをチェックしたいものです。ここでは非常に簡単なプロセスの起動チェックをしてみたいと思います。
プロセスの起動チェックを行っているのは下記の部分です。
while read; do
pgrep -fq $REPLY || terminal-notifier -title "Process not founds" -subtitle "$(basename $0)" -message "$REPLY"
done < $REPOSITORY_LIST
pgrep
は先述の pkill
と似たようなコマンドで、実体としては同じです。マッチするプロセスを探してそのプロセスIDを出力します。 -q
オプションをつければ出力せずにステータスだけを返します。
上記では、リポジトリのリストからPATHを一つずつ受取り、PATHを含むプロセスが存在しない場合は terminal-notifier
コマンド(後述)にそのPATHを送るという処理を示しています。これを1時間に1回実行しています。停止しているプロセスを検知できたなら再起動させればよいという考えもありますが、今回のような目的では、プロセスが停止していてもクリティカルではないので、停止したままにさせています。
terminal-notifier
terminal-notifier
はコマンドラインからMacの通知センターに通知を送るコマンドです。
実装方法の一つとして、起動される子プロセスである watch-git-email.sh
に trap
で「異常終了時に terminal-notifier
に通知する」というロジックを組み込むこともできます。その方がプロセスが終了した際にすぐに通知が飛ぶので正しい実装のようにも思いますが、プログラムの責務の分け方が美しくないと感じ、今回はこのようにしています。
Rubygemとしても作成されていますが、brewでインストールすることができます。
$ brew install terminal-notifier
終わりに
かなり簡単な方法ではありますが、複数のプロセスを1つのプロパティリストによって起動することができ、プロセスの起動をチェックすることもできました。既存の仕組みを上手く使えば、あまり難しいことはせずに実装できるという見本になれたのではないかと思っています。