launchdのStartCalendarIntervalで曜日を指定して実行する方法

公開日:
目次

cronから移したジョブをlaunchdで動かそうとして、「平日だけ朝9時に実行」をどう書くかで詰まりました。cronなら 0 9 * * 1-5 で済むのに、plistにそのまま範囲を書く手段がありません。最終的にはWeekdayキーを使う書き方に落ち着いたので、自分用のまとめとして整理します[1]

StartCalendarIntervalの基本

StartCalendarInterval は、起動するタイミングを「カレンダー上の特定の時刻」で指定する設定項目です。値は辞書(dict)で、Minute / Hour / Day / Weekday / Month の5つの項目を持てます。指定しなかった項目は「すべて」を意味します。たとえば Hour=9 だけ書けば、毎日9時台の0分・1分・…と動くわけではなく、毎日9時0分に1回だけ動きます。書かなかった Minute は0として扱われるからです(ここは少し直感に反するので注意)。

最小の例として「毎日9時0分に動かす」設定はこうなります。

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>9</integer>
    <key>Minute</key>
    <integer>0</integer>
</dict>

cronで言えば 0 9 * * * に相当します。

Weekdayの値で気をつけたい点

曜日を指定するのが Weekday キーです。値は整数で、0が日曜、1が月曜、…、6が土曜です。

注意したいのが、7も日曜として扱われることです。

これはBSD系cronの仕様を引き継いだもので、知らないと「6が日曜だと思って書いたら土曜に動いた」のような事故になります。月曜から金曜は1〜5、土曜は6、日曜は0または7、と覚えるのが安全です。

Weekday単独で書いた場合

Weekday=1 だけを書くと、月曜の0時0分に1回動きます。Minute を省略しているのに「毎分動く」とはならない点は、上の Hour=9 と同じです。書かれていない時刻系のキーは0として補われます。

<key>StartCalendarInterval</key>
<dict>
    <key>Weekday</key>
    <integer>1</integer>
</dict>

これで「毎週月曜の0時0分」になります。

平日だけ動かしたいとき

cronのように 1-5 のような範囲指定はplistには存在しません。「月曜から金曜の9時に動かす」を表現したいときは、StartCalendarInterval を辞書の配列にして、曜日ごとに1エントリずつ書きます[2]

<key>StartCalendarInterval</key>
<array>
    <dict>
        <key>Weekday</key>
        <integer>1</integer>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <dict>
        <key>Weekday</key>
        <integer>2</integer>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <dict>
        <key>Weekday</key>
        <integer>3</integer>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <dict>
        <key>Weekday</key>
        <integer>4</integer>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <dict>
        <key>Weekday</key>
        <integer>5</integer>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
</array>

冗長に見えますが、これがlaunchd公式の書き方です。配列の各要素が独立した起動条件として扱われ、そのいずれかにマッチしたら起動します。cronの 1-5 を5行に展開したと考えると分かりやすいかもしれません。

cronと比べたときの違い

平日指定の書き方を見ると、cronのほうが簡潔に感じます。実際、純粋なスケジューリングの記法としてはcronのほうが短く書けます。一方でlaunchdには別の利点があります。

一つは、Mac起動時に 時刻を見逃していた場合の挙動 が違うことです。cronはマシンが起動していなかった時刻のジョブを取りこぼしますが、launchdは「次回起動時に補って実行する」挙動が選べます(StartCalendarInterval でも、設定時刻を過ぎてからMacを起動すれば実行されます)。ノート型のMacのように電源断や休止が頻繁に挟まる環境では、こちらのほうが取りこぼしを抑えられます。

もう一つは、ログ管理やリソース制限などの他の項目を同じplistに書けることです。StandardOutPath / StandardErrorPath で出力先を分けたり、Nice で優先度を下げたり、cronでは別途仕掛けが必要な制御を、定期実行の設定と同じファイルで完結できます。

動作確認のしかた

書いた ~/Library/LaunchAgents/com.example.weekday.plist を反映するには launchctl bootstrap gui/$(id -u) を使います。読み込みに成功しているかは launchctl list | grep com.example.weekday で確認できます。次回起動予定の時刻を知るには launchctl print gui/$(id -u)/com.example.weekday の出力にある next run の行を見ます。

設定の意図どおりに次の起動時刻が決まっていれば、配列の書き方が正しく解釈されています。もし時刻が想定とズレていたら、Weekday=0/7 の混同や Minute の省略補完あたりを疑うと当たりやすいです。

まとめ

平日だけ動かす処理を書くたびに「またこの長いplistを書くのか」と思うのですが、いまのところ正攻法はこれだけです。/etc/cron.d/ から書き出した設定をlaunchd用に変換する小さな自作ツールを持っておくと、毎回手で書き写す手間が減ります。曜日指定が複雑になってきたら、PythonかRubyで雛形を生成する処理を書いてしまうのも現実的な選択肢になります。

脚注
  1. launchd.plist(5) Manual Page ↩︎

  2. MacOS launchd plist StartInterval and StartCalendarInterval examples - alvinalexander.com ↩︎