はじめに
この記事では、macOS の launchd で自動化スクリプトを動かそうとしたときに遭遇する「exit 78」の原因と、実際に僕がやらかした失敗ログをもとにした解決手順を書く。
launchd を使いはじめたばかりの人が一番つまずくのが、「スクリプトは手動で動くのに launchd だと動かない」という症状だ。exit 78 はその典型で、原因がわかってしまえば5分で直る。でもわからないまま半日溶かすこともある。実際に溶かした。
launchd の exit 78 が意味すること
まず前提として、launchd が報告する終了コードはジョブ自身が (略) した値をそのまま返す。
exit 78 は (略) で定義されている (略) に対応している。
(コード例は元記事を参照)
「設定エラーで死んだ」という意味だ。ただし、これはスクリプト側が明示的に (略) を呼んだ場合ではない(それなら原因は明白)。launchd 経由でのみ再現し、手動実行では問題ない——そういうケースに限って、環境の差異が原因になっている。
実際に踏んだパターン3選
僕のケースを時系列で書く。同じ症状の人は参考にしてほしい。
パターン1: PATH が端末と launchd で違う
一番多い。手動実行では (略) が見つかるのに、launchd 経由だと (略) で死ぬ。
ターミナルで (略) を打つと (略) が入っているが、launchd のデフォルト PATH は (略) だけだ。
plist に EnvironmentVariables を書いていないと、スクリプトが依存するコマンドが全部見つからなくなる。
(コード例は元記事を参照)
これを追加して (略) → (略) で直った。
パターン2: WorkingDirectory が存在しない
plist に (略) を指定していて、そのパスが存在しないか typo している場合も exit 78 になる。
(コード例は元記事を参照)
確認コマンドはこれだけ。
(コード例は元記事を参照)
存在確認が通らなければディレクトリ名を見直す。(略) の展開も launchd では効かないので絶対パスで書くこと。
パターン3: スクリプト内で import しているモジュールが入っていない仮想環境
これが一番わかりにくかった。ターミナルから実行するときは (略) がアクティブになっているので動く。launchd から呼ばれるときはアクティブになっていないので、(略) とか (略) が (略) で死ぬ。
解決策は2つある。
(コード例は元記事を参照)
僕は案Aに統一した。シンプルで依存が少ない。
exit コードで原因を素早く絞り込む対応表
| exit コード | 意味 | よくある原因 |
|---|---|---|
| 78 | EX_CONFIG(設定エラー) | PATH・WorkingDirectory・venv 未設定 |
| 127 | command not found | インタープリタのパスが間違い |
| 1 | 一般エラー | スクリプト内で例外発生 |
| 126 | 実行権限なし | chmod +x していない |
| 0 | 正常終了 | — |
exit 78 と 127 はほぼ環境差異の問題。exit 1 になったらスクリプト側のバグを疑う。
デバッグ手順——これをやれば大抵わかる
ログを見る。まずここから。
(コード例は元記事を参照)
あるいは StandardErrorPath を plist に追加してエラーログをファイルに吐かせる。
(コード例は元記事を参照)
ロードして実行された直後に (略) を見ると、何が足りないのかが出てくる。
(略) なら venv 問題。(略) なら PATH か WorkingDirectory 問題。このどちらかで exit 78 の9割は説明できた。
plist チェックリスト
設定を見直すときに確認するリスト。
- [ ] (略) の Python パスが venv 内の絶対パスになっているか
- [ ] (略) に typo がないか、ディレクトリが実在するか
- [ ] (略) に PATH を明示しているか(homebrew を使うなら (略) も)
- [ ] (略) でエラーログを出力しているか
- [ ] plist を変更した後に (略) → (略) しているか
最後の「変更後に reload していない」も地味に多い。変更しても反映されていないのに「直らない」と悩むやつ。
まとめ
exit 78 は「設定が壊れている」というシグナルで、スクリプト自体のバグではないことが多い。launchd はターミナルとは別の環境で動くので、PATH・WorkingDirectory・venv のどれかが欠けていると再現する。
(略) を必ず設定しておけばデバッグ時間が劇的に短くなる。これだけは本当に徹底した方がいい。
今はパイプラインが10本以上 launchd で動いているが、exit 78 で詰まるのはたいてい新しい plist を書いたときの最初の数分だけだ。慣れたら怖くない。✨
(コード例は元記事を参照)