環境情報
- macOS: Ventura 13.4(Intel Mac)
- Node.js: v18.16.0(/usr/local/bin/node)
- 実行対象: 毎日9時に起動する日次集計スクリプト(Node.js製)
はじめに
Linuxサーバーでcronに慣れ親しんだエンジニアが、macOSで定期実行を組もうとすると、ほぼ確実にlaunchdで躓きます。私もその一人でした。
先週、日次のデータ集計スクリプトを毎朝9時に実行する仕組みを作成したのですが、plistファイルを書いてlaunchctl loadしても何も起きない。ログすら出ない。3時間の格闘の末に原因が判明しました。本記事では、その際に遭遇した4つの落とし穴と、最終的に動作したテンプレートを共有します。
launchdとは
launchdはmacOSの起動・常駐・定期実行を統括するデーモンです。cronとの主な違いは以下の通りです。
| 項目 | cron | launchd |
|---|---|---|
| 設定ファイル | crontab | plist(XML) |
| 設定反映 | crontab -eで即時 | launchctlで再読み込みが必要 |
| 環境変数 | シェル依存 | 最小限(PATHが通らない) |
| 起動条件 | 時刻のみ | 時刻・イベント・ログイン等 |
| 失敗時のログ | cronのメール等 | 標準出力・エラー出力は破棄される |
落とし穴1: PATHが通っていない
症状
plistは正しく書いたはずなのに、スクリプトが実行されずエラーも出ない。
原因
launchdはシェルのプロファイル(.zshrcや.bash_profile)を一切読み込みません。そのため、nodeやnpmのようなコマンド名だけ書いても「command not found」になります。しかし、標準エラー出力の設定がないと、このエラーすら見えません。
対策
実行するコマンドはフルパスで指定します。まずwhichでパスを確認しましょう。
$ which node
/usr/local/bin/node
plistには以下のように書きます。
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/yourname/scripts/daily_report.js</string>
</array>
落とし穴2: WorkingDirectoryの未指定
症状
スクリプト自体は起動するが、fs.readFileSync('./config.json')のような相対パス参照でエラーになる。
原因
launchdはスクリプトの場所をカレントディレクトリにしません。デフォルトでは/がカレントディレクトリになるため、相対パスがすべてずれてしまいます。
対策
plistにWorkingDirectoryを明記します。
<key>WorkingDirectory</key>
<string>/Users/yourname/scripts</string>
落とし穴3: StandardOutPath / StandardErrorPath未設定
症状
何が起きているのか一切ログが残らず、デバッグの糸口すら見つからない。
原因
launchdで実行されたプロセスの標準出力と標準エラー出力は、どこにも出力されません。設定しない限り、エラーは闇に消えます。
対策
必ずログパスを設定します。ログディレクトリは事前に作成しておきましょう。
<key>StandardOutPath</key>
<string>/usr/local/var/log/daily_report.log</string>
<key>StandardErrorPath</key>
<string>/usr/local/var/log/daily_report_error.log</string>
この設定だけで、問題発生時の調査時間は大幅に短縮できます。
落とし穴4: launchctl bootout忘れ
症状
plistファイルを修正してlaunchctl loadを実行しても、修正が反映されない。
原因
launchctl loadは、すでに登録済みのジョブ定義を上書きしません。古い定義がメモリ上に残り続けます。
対策
一度アンロードしてから再ロードします。現在のmacOSではbootout/bootstrapを使うのが推奨です。
# 古い定義を削除
$ launchctl bootout gui/$(id -u)/com.example.daily-report.plist
# 新しい定義を読み込み
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.daily-report.plist
最終的なテンプレート
ここまでの内容を反映した、最終的な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>com.example.daily-report</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/yourname/scripts/daily_report.js</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/yourname/scripts</string>
<key>StandardOutPath</key>
<string>/usr/local/var/log/daily_report.log</string>
<key>StandardErrorPath</key>
<string>/usr/local/var/log/daily_report_error.log</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>
StartCalendarIntervalはcronの0 9 * * *に相当します。分単位の指定であればStartInterval(秒指定)も使えますが、時刻指定の定期実行にはStartCalendarIntervalがわかりやすいです。
動作確認の手順
- スクリプトを単体で実行して動作確認
- plistの構文チェック:
plutil -lint - launchctl bootstrapでロード
- ログファイルを確認
$ plutil -lint ~/Library/LaunchAgents/com.example.daily-report.plist
OK
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.daily-report.plist
$ launchctl list | grep daily-report
- 0 com.example.daily-report
$ cat /usr/local/var/log/daily_report_error.log
これで、毎日9時にスクリプトが実行され、結果がログに残るようになりました。
FAQ
Q. LaunchAgentとLaunchDaemonのどちらを使うべき?
ログイン状態に依存してよいならLaunchAgent(~/Library/LaunchAgents)、システム起動時に確実に動かしたいならLaunchDaemon(/Library/LaunchDaemons)です。ユーザー権限で動くバッチならLaunchAgentで十分です。
Q. launchctl loadはもう使えない?
現在のmacOSではlaunchctl bootstrap/bootoutが推奨されます。load/unloadはレガシー扱いで、環境によっては警告が出ます。
Q. 実行時刻になっても動かない場合の確認方法は?
まずlaunchctl listでジョブが登録されているか確認し、次にログファイルを確認します。ログが空ならlaunchd自体が起動していない可能性があるため、plistのLabelとファイル名の一致を確認してください。
まとめ
launchdはcronと比べて最初のハードルが高いですが、以下の4点を守れば確実に動作します。
- コマンドはフルパスで指定する
- WorkingDirectoryを明記する
- 標準出力・エラー出力のログパスを設定する
- plist修正後はbootout→bootstrapで再読み込みする
一度動いてしまえば、OSレベルで安定して動き続けるのは大きなメリットです。ぜひこのテンプレートをベースに、自分の環境に合わせてカスタマイズしてみてください。
この記事を書いた人
BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。
GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。
👉 業務自動化サービス — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中