0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

macOSのlaunchdで「動かない」と3時間格闘した話:cronから移行する際の落とし穴4選

0
Posted at

環境情報

  • 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)を一切読み込みません。そのため、nodenpmのようなコマンド名だけ書いても「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がわかりやすいです。

動作確認の手順

  1. スクリプトを単体で実行して動作確認
  2. plistの構文チェック: plutil -lint
  3. launchctl bootstrapでロード
  4. ログファイルを確認
$ 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) — 日々の知見を発信中

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?