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

## はじめに

先日、「[macOSでBashスクリプトを常駐プロセスとしてログイン時に起動させる](http://qiita.com/artifactsauce/items/b58866da22be0a2ebc6a)」という記事を書いたのですが、その後、ちょっとしたミスに気がついてしまいました。件のページで紹介しているのは、あるディレクトリ配下を監視して、そこに作られたGitリポジトリのメールアドレスを変更するというものだったのですが、対象となるディレクトリは1つであるという前提が間違っており、実際は4つもあることが判明しました。

1つの常駐プロセスを起動するために1つのプロパティリストを必要とし、1つの常駐プロセスが1つのディレクトリを監視するなら、4つのディレクトリのために4つのプロパティリストが必要になります。もちろん4つ作成すればよいのですが、ほとんど同じ目的をもったほとんど同じような内容のプロパティリストを作成し、管理するというのは、なかなかプライドの傷つけられる状態です。また、対象ディレクトリが増える度にプロパティリストをコピペして改変するという行為を想像するだけで自分が無能過ぎて死にたくなります。そこで今回は、1つのプロパティリストで複数の常駐プロセスを起動する方法について検討します。

## 方針

### 同じコマンドだが引数だけが違う

1つのプロパティリストで複数のプロセスを起動するのであれば、複数の子プロセスを起動する親プロセスを起動するように設定すればよいでしょう。

今回の目的は、「[ディレクトリを定期的に監視してGitリポジトリに間違ったEmailをコミットしない](http://qiita.com/artifactsauce/items/10141d6473bbb0e64cb1)」で作成したスクリプトを複数プロセス起動することです。このスクリプトは下記のように実行します。

```console
$ watch-git-email.sh /PATH/TO/DIRECTORY/ROOT expect-email@example.com &
```

今回、監視対象ディレクトリが4つに増えたという仕様なので、この第1引数以外は全く同じコマンドを4回実行することになります。ループで特定の引数だけを変化させれば簡単に実行できそうです。

### 停止するときは起動した全てのプロセスを停止したい

親プロセスを停止したいときには、起動した全ての子プロセスを停止したいはずです。1つ1つ子プロセスを停止すると考えただけで気が狂いそうになりますから、親プロセスを停止する際に子プロセスも全て停止します。

### 起動したプロセスが落ちたら通知したい

起動したプロセスが常に起動し続けることを保証することはできません。できれば停止したプロセスを再起動してほしいのですが、そこまではしなくとも、死んだプロセスを知らせるくらいはしてほしいものです。プロセスが複数になると、全てのプロセスの起動を目で確認するわけにもいかないので、きちんと起動したこと、そして起動し続けていることを自動的にチェックします。

## 実装

そんなこんなで、下記のとおり、複数のプロセスを起動するスクリプトを作成することで、1つのプロパティリストで複数プロセスを起動することができました。

```bash: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
```

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

```text: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
```

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

```xml:$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>
```

## 解説

### 実行スクリプト

#### 親プロセスの起動

親プロセスを起動する方法が[以前の記事](http://qiita.com/artifactsauce/items/10141d6473bbb0e64cb1)で書いたものと全く同じです。 今回はプロセスの死活をチェックする（後述）ためにこの値を１時間に設定していますが、違いはそれだけです。

```bash
while true; do
  sleep $MONITORING_INTERVAL
done
```

#### 子プロセスの起動

子プロセスの起動は、外部ファイルに保存したリポジトリのPATHのリストを標準入力から読み込み、 `while read` で1行ずつ読みとり、読み取った値を引数に与えてバックグラウンドでコマンドを実行するという方法をとっています。文にするとちょっと長いですが、下記の通り簡単です。

```bash
while read; do
  $COMMAND $REPLY $CORRECT_EMAIL &
done < $REPOSITORY_LIST
```

#### 子プロセスの停止

##### `pkill(1)`, `pgrep(1)`

子プロセスの停止には `pkill` コマンドを使っています。 `pkill` は、プロセステーブルを読み込んで、キーワードにマッチするプロセスすべてにシグナルを送る機能を有しています。もちろん、この場合に送りたいシグナルは `KILL` です。

```bash
pkill -KILL -f /PATH/TO/COMMAND
```

これで起動した子プロセスをすべて停止させることができます。

デフォルトではマッチング対象がプロセス名だけなのですが、 `-f` オプションをつけることで引数全体にもマッチング範囲が広がります。ただ、ここで言うプロセス名とは部分的な文字列のみを指しているようでして、PATHを含んだプログラム名と確実にマッチさせたい場合は、とりあえずこのオプションをつけてを与えておいた方が無難かと思います。

##### `trap(1)`

停止する方法はわかったのですが、親プロセスを停止するときにこのコマンドを実行するのはどうすれば良いのでしょうか？答えは `trap` です。

`trap` はプログラムが何らかのシグナルをキャッチした際に実行するコマンドを管理するbashのビルトインコマンドです。プログラムが停止／終了する際にはシステムから何らかのシグナル受け取ります（と考えてください）。この受け取ったシグナルにしたがってコマンドを実行したい際に、下記のように `trap` で指示しておきます。

```bash
trap "/PATH/TO/COMMAND ARG1 ARG2" KILL
```

指定できるシグナルは `kill -l` で調べることができます。しかし前述のスクリプトに記載されている `EXIT` は含まれていません。これは疑似シグナルと呼ばれ、いくつかのシグナルを束ねたものだと考えると良いんじゃないかと思います。 `EXIT` はプログラムが終了したことを示す疑似シグナルです。終了した理由を問いません。

この `trap` コマンドがよく使われる場面としては、一時ファイルの事後処理などがあります。プログラム中で一時ファイルが必要となる場面は多いですが、そのファイルを残しておきたくありません。処理の最後に一時ファイルを削除する処理を入れていたとしても、処理の途中にエラーで停止してしまった場合には削除されません。 `trap` を使えばそのような場合にも確実にファイルを削除できます。

#### 子プロセスの起動チェック

子プロセスを起動したら、起動した子プロセスが正常に起動し続けているかをチェックしたいものです。ここでは非常に簡単なプロセスの起動チェックをしてみたいと思います。

プロセスの起動チェックを行っているのは下記の部分です。

```bash
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を送るという処理を示しています。これを１時間に１回実行しています。停止しているプロセスを検知できたなら再起動させればよいという考えもありますが、今回のような目的では、プロセスが停止していてもクリティカルではないので、停止したままにさせています。

##### `terminal-notifier`

[`terminal-notifier`](https://github.com/julienXX/terminal-notifier) はコマンドラインからMacの通知センターに通知を送るコマンドです。

実装方法の一つとして、起動される子プロセスである `watch-git-email.sh` に `trap` で「異常終了時に `terminal-notifier` に通知する」というロジックを組み込むこともできます。その方がプロセスが終了した際にすぐに通知が飛ぶので正しい実装のようにも思いますが、プログラムの責務の分け方が美しくないと感じ、今回はこのようにしています。

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

```console
$ brew install terminal-notifier
```

# 終わりに

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

# 参照

- [シェルスクリプトでのtrapの使い方 - 理系学生日記](http://kiririmode.hatenablog.jp/entry/20160730/1469867810)
- [Man page of PGREP](https://linuxjm.osdn.jp/html/procps/man1/pgrep.1.html)
- [Macのターミナルで時間のかかるコマンドが終わったら通知を受け取る方法 - Qiita](http://qiita.com/endam/items/6b2dd23ac7a6a85f7f18)