launchdで定期的にスクリプトを実行

定期的にスクリプトを実行する場合、Mac OS Xでは`crontab`より`launchd`を使うことが推奨されている。
`launchd`を用いてMac OS Xで**定期的にスクリプトを実行する方法**を記述。

# 特徴

- 設定が２種類ある：
    - **エージェント**はユーザーがログイン中に実行できるプログラム。
    - **デーモン**はシステム共通で、誰もログインしていなくても実行できるプログラム。
- 前回の実行が終わらないと次の実行は始まらない。また、スリープ状態、シャットダウン状態では実行されない。[^1]
- 標準で**CPU時間**、**メモリ**、**ファイル**等の使用の制限[^3]が設けられている。必要に応じて設定ファイルで上限を上げないと、シグナルで届いてしまう。

設定ファイルを置く場所は次の通り。

| 場所 | 用途 |
|:----|:-----|
|`~/Library/LaunchAgents/<Label>.plist`|ユーザーごと設定できる**エージェント**|
|`/Library/LaunchAgents/<Label>.plist`|管理者用の**エージェント**|
|`/Library/LaunchDaemons/<Label>.plist`|システム共通の**デーモン**（`root`がオーナー）|

**Label** はユニークな識別名。ファイルは`plist`形式[^2]。

#単純なサンプル

一定間隔で実行と、特定の時間に実行する方法がある。

## 10秒間隔でスクリプトを実行

次を満たすサンプルを作る。

- 実行するのは一般ユーザー所有のスクリプト：`test.rb`。
- 10秒ごとに実行する**エージェント**として設定する。
- STDOUT/STDERR出力をファイルに保存（`test.out`/`test.err`）。

識別名（Label）は`test`にするので設定ファイルは`test.plist`。
一般ユーザー所有のエージェントスクリプトなので`~/Library/LaunchAgents/`に置く。

```xml:設定ファイル：~/Library/LaunchAgents/test.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>test</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/bin/ruby</string>
		<string>-Ku</string>
		<string>/Users/mofu/script/test.rb</string>
	</array>
	<key>StartInterval</key>
	<integer>10</integer>
	<key>StandardOutPath</key>
	<string>/Users/mofu/script/test.out</string>
	<key>StandardErrorPath</key>
	<string>/Users/mofu/script/test.err</string>
</dict>
</plist>
```

次のコマンドで登録すると、実行されるようになる。

```sh
$ launchctl load ~/Library/LaunchAgents/test.plist
```

これ以降、システムを再起動してもこのユーザーでログインすれば設定が維持され、実行が再開する。
登録解除するには次のコマンドを使う。

```sh
$ launchctl unload ~/Library/LaunchAgents/test.plist
```
これ以降、実行されなくなる。

## 毎日、0時0分にスクリプトを実行

次を満たすサンプルを作る。

- 実行するのは一般ユーザー所有のスクリプト：`test2.rb`。
- 毎日、0時間0分に実行する**エージェント**として設定する。
- STDOUT/STDERR出力をファイルに保存（`test2.out`/`test2.err`）。

識別名は`test2`とするので設定ファイルは`test2.plist`。
一般ユーザー所有のエージェントスクリプトなので`~/Library/LaunchAgents/`に置く。

```xml:設定ファイル：~/Library/LaunchAgents/test2.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>test2</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/bin/ruby</string>
		<string>-Ku</string>
		<string>/Users/mofu/script/test2.rb</string>
	</array>
	<key>StartCalendarInterval</key>
	<dict>
		<key>Hour</key>
		<integer>0</integer>
		<key>Minute</key>
		<integer>0</integer>
	</dict>
	<key>StandardOutPath</key>
	<string>/Users/mofu/script/test2.out</string>
	<key>StandardErrorPath</key>
	<string>/Users/mofu/script/test2.err</string>
</dict>
</plist>
```

次のコマンドで登録。

```sh
$ launchctl load ~/Library/LaunchAgents/test2.plist
```

#用途ごとサンプル

## iOSアプリのデイリービルドデーモン

次を満たすサンプルを作る。

- スクリプト`build.sh`をシステム共通で（ログインしていない状態でも）実行できるようにする。
- すなわち**デーモン**として設定する。実行するタイミングは毎日0時間0分。
- デーモンとして設定するので`root`所有の設定だが、スクリプトの実行ユーザーは`mofu`とする。
- 各種制限に引っかからないよう、上限を設定する。
  - 処理時間：1,200秒（標準は20秒である）。
  - CPU時間：1,200秒。
  - 最大メモリ：2,000,000,000バイト。
  - 最大ファイルサイズ：2,000,000,000バイト。
- 環境変数を設定：CocoaPodsの`pod install`のため`LC_ALL`を`en_US.UTF-8`に。
- ワーキングディレクトリを`/Users/mofu/script`に設定。
- STDOUT出力をファイルに保存（`build.out`）。

識別名は`build`とするので設定ファイルは`build.plist`。
システム共通のデーモンスクリプトなので`/Library/LaunchDaemons/`に置き、`root`を所有者にする。

```xml:設定ファイル：/Library/LaunchDaemons/build.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>build</string>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/bash</string>
		<string>/Users/mofu/script/build.sh</string>
	</array>
	<key>StartCalendarInterval</key>
	<dict>
		<key>Hour</key>
		<integer>0</integer>
		<key>Minute</key>
		<integer>0</integer>
	</dict>
	<key>StandardOutPath</key>
	<string>/Users/mofu/script/build.out</string>
	<key>WorkingDirectory</key>
	<string>/Users/mofu/script</string>
	<key>EnvironmentVariables</key>
	<dict>
		<key>LC_ALL</key>
		<string>en_US.UTF-8</string>
	</dict>
	<key>UserName</key>
	<string>mofu</string>
	<key>SessionCreate</key>
	<true />
	<key>ExitTimeOut</key>
	<integer>1200</integer>
	<key>SoftResourceLimits</key>
	<dict>
		<key>CPU</key>
		<integer>1200</integer>
		<key>FileSize</key>
		<integer>2000000000</integer>
		<key>Data</key>
		<integer>2000000000</integer>
	</dict>
	<key>HardResourceLimits</key>
	<dict>
		<key>CPU</key>
		<integer>1200</integer>
		<key>FileSize</key>
		<integer>2000000000</integer>
		<key>Data</key>
		<integer>2000000000</integer>
	</dict>
</dict>
</plist>
```

次のコマンドで設定ファイルのオーナーを`root`にし、登録する。

```sh
$ sudo chown root /Library/LaunchDaemons/build.plist
$ sudo launchctl load /Library/LaunchDaemons/build.plist
```

※iOSビルドスクリプトについては[こちら](http://qiita.com/rsahara/items/024d9907af2c051f3d44)に記述。

## 平日のみ、毎日10時にスクリプトを実行

次を満たすサンプルを作る。

- スクリプト`weekdays.sh`をデーモンとして実行する。
- 実行するタイミングは平日のみ、毎日10時。

```xml:設定ファイル：/Library/LaunchDaemons/weekdays.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>weekdays</string>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/bash</string>
		<string>/Users/mofu/script/weekdays.sh</string>
	</array>
	<key>StartCalendarInterval</key>
	<array>
		<dict>
			<key>Weekday</key>
			<integer>1</integer>
			<key>Hour</key>
			<integer>10</integer>
			<key>Minute</key>
			<integer>0</integer>
		</dict>
		<dict>
			<key>Weekday</key>
			<integer>2</integer>
			<key>Hour</key>
			<integer>10</integer>
			<key>Minute</key>
			<integer>0</integer>
		</dict>
		<dict>
			<key>Weekday</key>
			<integer>3</integer>
			<key>Hour</key>
			<integer>10</integer>
			<key>Minute</key>
			<integer>0</integer>
		</dict>
		<dict>
			<key>Weekday</key>
			<integer>4</integer>
			<key>Hour</key>
			<integer>10</integer>
			<key>Minute</key>
			<integer>0</integer>
		</dict>
		<dict>
			<key>Weekday</key>
			<integer>5</integer>
			<key>Hour</key>
			<integer>10</integer>
			<key>Minute</key>
			<integer>0</integer>
		</dict>
	</array>
</dict>
</plist>
```

次のコマンドで設定ファイルのオーナーを`root`にし、登録する。

```sh
$ sudo chown root /Library/LaunchDaemons/weekdays.plist
$ sudo launchctl load /Library/LaunchDaemons/weekdays.plist
```

#設定項目の一覧

定期的にスクリプトを実行するために使う設定。

## 識別名【必須】

ジョブを識別するためのユニークな文字列で構成された**識別名**を`Label`に指定する。
ファイル名にもこの文字列を使う。

```xml:Label
<key>Label</key>
<string>mofu.build.daily</string>
```
## 何を実行するか【必須】

`ProgramArguments`に、実行する**プログラム**と**パラメータ**を指定する。

```xml:ProgramArguments
<key>ProgramArguments</key>
<array>
	<string>/bin/bash</string>
	<string>/Users/mofu/script/build.sh</string>
</array>
```

## いつ実行するか

一定間隔なら`StartInterval`で**秒数**を指定する。

```xml:StartInterval
<key>StartInterval</key>
<integer>10</integer>
```

特定の時間に実行するなら`StartCalendarInterval`に時間を指定する。

 - `Minute`：**分**
 - `Hour`：**時間**
 - `Day`：**日**
 - `Weekday`：**曜日**（0と7は日曜日）
 - `Month`：**月**

```xml:StartCalendarInterval
<key>StartCalendarInterval</key>
<dict>
	<key>Hour</key>
	<integer>0</integer>
	<key>Minute</key>
	<integer>0</integer>
</dict>
```

複数の時間を指定することもできる。

```xml:StartCalendarInterval、配列
<key>StartCalendarInterval</key>
<array>
	<dict>
		<key>Hour</key>
		<integer>10</integer>
		<key>Minute</key>
		<integer>0</integer>
	</dict>
	<dict>
   		<key>Hour</key>
		<integer>19</integer>
		<key>Minute</key>
		<integer>0</integer>
	</dict>
</array>
```


## どの環境で実行するか

`WorkingDirectory`に**ワーキングディレクトリ**を指定できる。

```xml:WorkingDirectory
<key>WorkingDirectory</key>
<string>/Users/mofu/script</string>
```

`EnvironmentVariables`に、**環境変数**を指定できる。

```xml:EnvironmentVariables
<key>EnvironmentVariables</key>
<dict>
	<key>LC_ALL</key>
	<string>en_US.UTF-8</string>
</dict>
```

`UserName`に、スクリプトを実行する**ユーザー**を指定できる。

```xml:UserName
<key>UserName</key>
<string>mofu</string>
```

そのユーザーのセッションを作成した上で実行したい場合は`SessionCreate`を指定できる。（デーモンでもログインキーチェインが使える状態になる。）

```xml:SessionCreate
<key>UserName</key>
<string>mofu</string>
<key>SessionCreate</key>
<true />
```

## 処理時間の制限

`ExitTimeOut`に処理の**制限時間**を**秒数**で設定できる。標準は20秒。
この時間が経過するとプロセスに`SIGKILL`シグナルが送られる。

```xml:ExitTimeOut
<key>ExitTimeOut</key>
<integer>1200</integer>
```

## リソースの制限

`SoftResourceLimits`、`HardResourceLimits`に、ソフト制限、ハード制限を設定できる。
ソフト制限を超えるとプロセスにシグナルが送られるがハート制限未満までなら実行を続けられる。
主な設定項目[^5]：

 - `CPU`：各プロセスに使用される最大**CPU時間**（秒）。
 - `Data`：使用できる最大**メモリ**（バイト）。
 - `FileSize`：作成できる**ファイルの最大サイズ**（バイト）。

```xml:SoftResourceLimits、HardResourceLimits
<key>SoftResourceLimits</key>
<dict>
	<key>CPU</key>
	<integer>1200</integer>
	<key>FileSize</key>
	<integer>2000000000</integer>
	<key>Data</key>
	<integer>2000000000</integer>
</dict>
<key>HardResourceLimits</key>
<dict>
	<key>CPU</key>
	<integer>1200</integer>
	<key>FileSize</key>
	<integer>2000000000</integer>
	<key>Data</key>
	<integer>2000000000</integer>
</dict>
```

#参照
- [Scheduling Timed Jobs](https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html)
- [Creating Launch Daemons and Agents](https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html)
- [MAN: launchd(8)](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/launchd.8.html)
- [MAN: launchd.plist(5)](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/launchd.plist.5.html)
- [A launchd Tutorial](http://launchd.info/)
- [SessionCreateプロパティの使用 - StackOverflow](http://stackoverflow.com/a/9482707/4518606)
- [Technical Note TN2083 Daemons and Agents（SessionCreateプロパティが載っている）](https://developer.apple.com/library/content/technotes/tn2083/_index.html)


[^1]: スリープの場合、スリープ中に実行するタイミングが来ていた場合、スリープから復帰したときに**直ち**に実行される。
[^2]: Property List、XML形式。テキストエディタで編集可能。
[^3]: 処理時間、CPU使用時間、メモリ、スタック、同時に開くファイル数、ファイルサイズ等。
[^5]: 必要に応じて設定できる他のキー：`Core`、`MemoryLock`、`NumberOfFiles`、`NumberOfProcesses`、`ResidentSetSize`、`Stack`。