導入
cron を使ってたんだけど、macOS に移行してから挙動が怪しくなった。スリープ中に実行時間を過ぎると、起動後もスルーされる。毎日朝7時に SNS 投稿スクリプトを走らせたいのに、MacBook が起きるのを待たずに「今日はもういいや」ってなる。
launchd に乗り換えたのは去年の秋ごろで、最初の .plist ファイルを書くのに丸1日溶かした。今は30本以上のスクリプトを launchd で管理してて、SNS投稿・KDP生成・ブログ記事生成まで全部自動で回ってる。その経験をそのまま書く。
launchd と cron の違い、実際のところ
cron に慣れてると、launchd は最初に面食らう。* * * * * で書けたものを、XML で書かなきゃいけない。
でも慣れると launchd の方がはるかに信頼できた。スリープ復帰後に「missed」した実行を拾ってくれるし、stdout/stderr を別ファイルに吐けるし、失敗したら自動再起動もできる。
僕のパイプラインでいうと、毎日 07:00 / 12:30 / 20:30 の3回、BSky と X と note に投稿を走らせてる。cron 時代は深夜に充電しながら放置するとMacBookがスリープして7時の投稿が飛んでた。launchd に変えてから一度もそういう事故は起きてない。
.plist ファイルの基本構造
~/Library/LaunchAgents/ に .plist を置くのが基本。ここに置いたものは、ログインユーザー権限で動く。
[コード略 — 詳細は元記事参照]
Label はユニークな逆ドメイン形式で書くのが慣例。ファイル名もこれと合わせる(com.taito.sns-post.plist)。
詰まったポイント3つ
python3 のパスが通らない
一番最初に踏んだ。ターミナルで which python3 すると /usr/bin/python3 が返ってくるのに、launchd から実行すると「command not found」が出た。
原因は環境変数 PATH が launchd 環境では別物だから。上の例みたいに EnvironmentVariables セクションに明示的に書く必要がある。
homebrew 経由の Python を使ってるなら /opt/homebrew/bin/python3 を指定する。絶対パスで書いたほうが確実。
[コード略 — 詳細は元記事参照]
仮想環境の pip モジュールが読み込まれない
スクリプトがローカルの仮想環境(.venv)のパッケージを使ってる場合、launchd は .venv の存在を知らないのでインポートエラーになる。
解決策は2つあって、僕は「仮想環境の Python を直接指定する」方法を使ってる。
[コード略 — 詳細は元記事参照]
もう一つは shell スクリプトをラッパーにして source .venv/bin/activate してから呼ぶ方法だけど、ログが分散するのが嫌でやめた。
作業ディレクトリが違う
スクリプト内で相対パスを使ってると、launchd から実行したときに FileNotFoundError が出る。launchd のデフォルト作業ディレクトリは / だから。
[コード略 — 詳細は元記事参照]
これを追加するか、スクリプト側で絶対パス指定に統一するか。僕は両方やってる。
読み込みと確認コマンド
.plist を書いたら launchctl で登録する。
[コード略 — 詳細は元記事参照]
launchctl list で出てくる数値の見方。
[コード略 — 詳細は元記事参照]
PID が - で終了コードが 0 なら正常に完了してる。PID に数値が出てれば今実行中。終了コードが -1 や 2 とかになってたらエラー——ログを確認しに行く。
複数スクリプトを管理するときの整理
今は30本以上あるので、命名規則を決めてる。
[コード略 — 詳細は元記事参照]
常駐型(KeepAlive)と定時型(StartCalendarInterval)を混在させても問題ない。常駐型は KeepAlive を true にするだけ。
[コード略 — 詳細は元記事参照]
プロセスが落ちたら launchd が自動で再起動してくれる。Discord Bot はこれで動かしてて、クラッシュしても数秒以内に復活する。✨
ログ設計の話
ログを雑に設計すると、何が起きてるか追えなくなる。僕の設計は:
stdout→04_Config/logs/{name}.log(正常出力)stderr→04_Config/logs/{name}_err.log(エラー)
スクリプト側でも Python の logging モジュールを使って、実行日時・処理件数・エラー内容を必ず出力してる。launchd は実行のたびにログを追記してくれるけど、スクリプト側で ---- のセパレーターを入れて区切りを明示してる。
[コード略 — 詳細は元記事参照]
まとめ
cron より設定が面倒なのは本当で、最初の1本を動かすまでが一番しんどい。でも動き出したら安定してて、スリープ復帰後の取りこぼしもなく、ログも綺麗に管理できる。
「手で実行したら動くのに launchd から動かない」という問題のほぼ全部は、環境変数かパスかディレクトリの問題。この3点を先に確認する癖をつけると解決が早い。
今は朝7時に起きると SNS への投稿が全部済んでて、KDP の原稿も生成されてる。ここまで来るのに半年かかったけど、launchd の設定でつまずいた時間が一番長かった気がする。誰かの1時間節約になれば。
[コード略 — 詳細は元記事参照]