LoginSignup
1
1

More than 5 years have passed since last update.

macOSで複数の似通った常駐プロセスをログイン時に起動させる

Last updated at Posted at 2017-02-05

はじめに

先日、「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つのプロパティリストで複数プロセスを起動することができました。

watch-repositories.sh
#!/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

対象ディレクトリのリストは別ファイルに切り出しておきました。

watch-repositories.list
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/1
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/2
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/3
/ABSOLUTE/PATH/TO/REPOSITORY/PARENT/4

これを起動するプロパティリストは下記のとおりです。最小限の設定しかしていません。

$HOME/Library/LaunchAgents/watch-repositories.plist
<?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.shtrap で「異常終了時に terminal-notifier に通知する」というロジックを組み込むこともできます。その方がプロセスが終了した際にすぐに通知が飛ぶので正しい実装のようにも思いますが、プログラムの責務の分け方が美しくないと感じ、今回はこのようにしています。

Rubygemとしても作成されていますが、brewでインストールすることができます。

$ brew install terminal-notifier

終わりに

かなり簡単な方法ではありますが、複数のプロセスを1つのプロパティリストによって起動することができ、プロセスの起動をチェックすることもできました。既存の仕組みを上手く使えば、あまり難しいことはせずに実装できるという見本になれたのではないかと思っています。

参照

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1