cronからlaunchdへ(より効率的なジョブ管理を目指して)

前回、cronでジョブ(コマンドやスクリプト、単一のプログラムなど)を自動実行するために、crontabからその設定をする方法について調べていた。自分なりにかなり詳細に。

ところが、OSX 10.5ではcronを利用してジョブを自動実行する仕組みは一切、利用されていなかった...。ユーザーがcrontabで設定ファイルを作成しない限り、cronさえ起動していない状態だ。(crontabで設定ファイルを作成すれば、cronも起動するようになる。)cronに替わって、OSX 10.5ではlaunchdが活躍していた。launchdはcron以上に柔軟にジョブを管理する仕組みを持っている。

例えば、スティッキーズをspotlightで検索可能な状態にするために、cronを使ってStickiesDatabaseを1分ごとにコピーしていた。*1

  • 最初は無差別にコピーしていたが、
* * * * * cp ~/Library/StickiesDatabase ~/Documents/StickiesData.aaa
  • その後StickiesDatabaseが更新された時だけ、コピーするように変更した。
* * * * * diff -q ~/Library/StickiesDatabase ~/Documents/StickiesData.aaa || cp ~/Library/StickiesDatabase ~/Documents/StickiesData.aaa

diffによって無駄なコピーを排除し効率的になったが、実は決定的な問題が残っていて、それは1分ごとにしか監視されていないということ。スティッキーズに何か書いて、それを即Spotlight検索しても、内蔵時計の秒針が次に0を指すまで、たった今更新した内容は反映されることはないのだ...。運が良ければ1秒後に反映されるが、最大60秒後、平均30秒は更新されない計算になる。まあ、1分以内の更新なんて、頭の中に残っているので検索するまでもないことなのだが...。このケースでは大した問題ではないが、このようなタイムラグが発生することで、場合によっては何か問題になることもあるかもしれない。
それなら、ジョブを定期実行する間隔を短くすれば良さそうなのだが、cronで直接的に管理できる間隔は1分単位が最短。それよりもっと短い間隔で管理しようとすれば、sleepコマンド等で何らかの工夫をするか、別の手段を考えた方が良さそう。

そんな時、launchdは素晴らしい環境を提供してくれる。

  • launchdなら秒単位の定期実行も問題なく管理できる。
  • さらに、秒単位で指定するまでもなく、ファイルを指定して、それが更新された時だけ実行するという最も理想的なタイミングを指定できるのだ。*2
  • 理想的にStickiesDatabaseが更新されたタイミングでコピーするためには、以下のxmlを ~/Library/LaunchAgents/com.bebekoubou.stickiesdata.sync.plist として保存すればOK。
<?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>com.bebekoubou.stickiesdata.sync</string>
	<key>ProgramArguments</key>
	<array>
		<string>cp</string>
		<string>/Users/zari/Library/StickiesDatabase</string>
		<string>/Users/zari/Documents/StickiesData.aaa</string>
	</array>
	<key>WatchPaths</key>
	<array>
		<string>/Users/zari/Library/StickiesDatabase</string>
	</array>
</dict>
</plist>

見ての通り、その設定ファイルはxmlで記述することになっていた...。cronの設定ファイルは独特の書式なのかもしれないが、人間が見て理解し易く、シンプルに指定できるように工夫されていた。対して、xmlにはあらゆることを表現できる能力があるのかもしれないが、cronの設定ファイルのようにシンプルとは言えない...。
Property List Editorを利用すれば、もう少し見易くなるが、規定されているキーと値の意味はちゃんと理解しておく必要がある...。

Appleは、このような独特の仕様を作るが、その仕様は大抵、素晴らしいチュートリアルとしてまとめられている。但し、英語で...。

英語が苦手な自分には、ちょっと敷居が高く、取っ付き難いのだ。そんな時は、Lingon!これでcronの設定以上に分かり易く、シンプルに管理できるようになった。(はい、ご想像の通り、上記xmlはLingonが生成したものです。)

Lingon

  • Lingonの基本設定を見ていると、launchdはとても柔軟にOSの状況を監視していることがわかる。
  • 基本的な設定だけでも、以下の条件でジョブを効率的に実行することが可能だ。
    • 何が起ころうとも絶えず実行し続ける
    • ロードされた時実行する(起動時、あるいはログイン時)
    • ボリュームがマウントされた時毎回実行する
    • 特定の日時を指定して、繰り返し実行する
    • 毎秒・毎分・毎時を指定して、繰り返し実行する
    • 指定したファイルやフォルダが更新された時実行する
    • 指定したフォルダが空でなかったり、ファイルが追加された時実行する

launchdの底力

実は、launchdはジョブを定期的に自動実行することに限らず、OSが必要とするすべてのジョブを一括して管理する仕組みを提供してくれていた。その状況はアクティビティモニタを起動しても簡単に確認することができた。launchdのプロセスIDは1。プロセスIDが0のkernel_taskと並んで、OSが起動する時、最初に起動されるプログラムになっている。

そして、「すべてのプロセスを階層表示」してみると、そのあと起動されたプロセスはすべてlaunchdの子プロセスという扱いになっている。つまり、launchdはすべてのプロセスの源流、最初の一滴なのであった。だから、ここまで詳細にOSが刻々と変化する状況に合わせて、ジョブを効率的に実行する仕組みを提供してくれていたのだ。

cronもlaunchdの子プロセス

  • OSX 10.5ではcronさえ実行されていない状態だが、crontabで設定すれば、すぐにcronが起動して、設定した通りに定期的に自動実行してくれる。
  • 必要に応じて、いつでも利用できる状態なのであった...。その仕組みは、launchdの以下の設定ファイルにある。
  • /usr/lib/cron/tabsを監視して、そこにファイルが追加されたら、/usr/sbin/cronを実行する設定になっていた。

毎日、毎週、毎月の設定

  • cronでお決まりで設定する定期実行も、以下のように設定されていた。
    • com.apple.periodic-daily
    • com.apple.periodic-weekly
    • com.apple.periodic-monthly
  • 上記設定により、以下のフォルダ内においたファイルが定期的に実行される。
    • /private/etc/periodic/daily/
    • /private/etc/periodic/weekly/
    • /private/etc/periodic/monthly/

daemonとagentの違い

launchdが実行するジョブの形態はdeamonとagentの二つに分かれている。

  • deamonは、OS起動時にrootユーザーが実行する。(.plist内で実行ユーザー、グループを指定すれば、変更できる)
  • agentは、ログイン時にログインユーザーが実行する。(変更できない)
  • deamonは、決してGUIを持つことはない。(ログイン前から起動しているプロセスなので)
  • agentは、GUIを持つことも可能。

使い分け

launchdeamonやlaunchagentの.plistを配置する場所について、Appleは次の5箇所を示している。

  • ログイン時にUSER_NAMEのユーザーのみに提供するサービス(USER_NAME以外のユーザーには提供されない)
    • /Users/USER_NAME/Library/LaunchAgent(ログイン時に起動される)
      • 例:フォルダアクションスクリプトのフォルダ監視サービスの起動
      • 例:SafariMobileMeの同期サービスの起動
  • すべてのユーザーに提供するサービス(どのユーザーが利用中でも提供されることになる)
    • /Library/LaunchAgent(ログイン時に起動される)
    • /Library/LaunchDeamon(OS起動時に起動される)
  • OSXとして提供する基本サービス
    • /System/Library/LaunchAgent(ログイン時に起動される)
      • 例:Spotlightの起動
    • /System/Library/LaunchDeamon(OS起動時に起動される)
      • 例:AirPort(AirMac)サービスの起動

注意すること

  • ファイル名にスペースが含まれる場合、ダブルクォートで囲ってエスケープなしで書く。(バックスラッシュによるエスケープではダメ)
/Applications/Spotlight\ helper\ for\ Stickies/OpenStickies.app/Contents/MacOS/StickiesDataSync.sh #NG
"/Applications/Spotlight helper for Stickies/OpenStickies.app/Contents/MacOS/StickiesDataSync.sh" #OK
cp ~/Library/StickiesDatabase ~/Documents/StickiesData.aaa #NG
cp $HOME/Library/StickiesDatabase $HOME/Documents/StickiesData.aaa #NG
cp /Users/zari/Library/StickiesDatabase ~/Users/zari/Documents/StickiesData.aaa #OK

.plistのロードなど

  • .plistは、deamonであればOS起動時に、agentであればログイン時にlaunchdが読み取って、設定した通りにジョブを管理してくれることになる。
  • 上記以外のタイミングで、.plistの変更や停止をlaunchdに伝えたい時は、launchctlコマンドで行う。
# 設定ファイルのロード(launchdへ登録する)
launchctl load ~/Library/LaunchAgents/com.bebekoubou.stickiesdata.sync.plist

# 設定ファイルのアンロード(launchdから削除する)
launchctl unload ~/Library/LaunchAgents/com.bebekoubou.stickiesdata.sync.plist

StickiesDatabaseをコピーする時に残った疑問

  • 以上のことを理解して、launchdの設定ファイルはagentとして、/Users/USER_NAME/Library/LaunchAgentに配置した。(LingonではMY AGENTS)
  • xml内ではコマンドや監視ファイルの指定に ~ や $HOME は利用できないので、ログインするユーザーごとに設定する必要があった。
    • What
cp /Users/zari/Library/StickiesDatabase /Users/zari/Documents/StickiesData.aaa
    • When
      • Run it if this file is modified:
/Users/zari/Library/StickiesDatabase
  • 上記のやり方では、ユーザーごとに /Users/USER_NAME/Library/LaunchAgentに.plist のインストール作業を実施する必要がある。
  • もし ~ あるいは $HOME が利用できれば、理想的には以下のようにして、/Library/LaunchAgentに.plistをインストールして、すべてのユーザー共通の設定にしたい。
    • What
cp ~/Library/StickiesDatabase ~/Documents/StickiesData.aaa
    • When
      • Run it if this file is modified:
~/Library/StickiesDatabase
  • 果たして、上記のような都合の良い設定は出来るのだろうか?(~ あるいは $HOME を代用する書き方があるのだろうか?)
  • もし出来ないとすれば、管理者ユーザーがアプリケーションをインストールした時に、すべてのユーザーにLaunchAgentの.plistをインストールする手段はあるのだろうか?
  • 予想:全ユーザー共通のシステム環境設定としてインストールして、ログイン時に.plistを書き込み、ロードする。(そんなことできるのかな?)

*1:StickiesDatabaseは更新される度に毎回削除されているようなので、ハードリンクは利用できない状況であった。

*2:結局はlaunchdが内部で秒単位で監視する作業をしているのだと思うが、秒単位でコマンドを実行して監視するよりは遥かに効率的だし、利用者から見てイベント駆動型の指定が出来るので、動作の意味や目的がとても理解し易い。