macOS更新後に常駐ジョブが保留された時、別の起動条件で救う
今回やったこと
macOSの更新後、launchdに登録されたジョブが「読み込み済み」に見える一方で、実際には一度も起動しない状態が発生した。この事例では、状態を「登録されたか」だけで判断せず、実行中か、起動保留になっていないかまで調べる監視と、別の起動条件から保留中のジョブを再度起こす仕組みを用意した。
scripts/launchd_rescue.py の記録によると、macOS 26.6.2への更新後、RunAtLoad、KeepAlive、StartIntervalを起動条件とする22本のジョブが約33時間、pended nondemand spawn のまま動かなかった。コードを配る処理や監視処理も同じ条件に依存しており、止まった状態を知らせる側まで一緒に止まっていた。一方、StartCalendarIntervalで起動するジョブは予定時刻に発火していた。
この差が設計の手掛かりになった。保留されるジョブと同じ起動条件に救済処理を置くと、救済処理も同じ理由で止まる。そこで、カレンダー起動だけを使う救済ジョブを別に設けた。救助役を、救助対象と異なる起動経路に置くことで、同じ故障に巻き込まれる可能性を下げる。
「載っている」と「動いている」を分けて観測する
従来の確認では launchctl list の結果が使われていた。しかし、今回の調査では、表示上の終了コードが0でも一度も動いていないことがあり、PIDがない状態も定期ジョブの正常時と同じ見た目になると説明されている。登録状態だけでは、実行待ちなのか、起動保留なのかを見分けられない。
追加された共通処理 scripts/lib/launchd_state.py は launchctl print の状態を解析する。pended nondemand spawn は、launchdが自発起動を保留している状態として扱う。また、ログインドメインの on-demand count や Setup Assistant の状態も調べ、単なる未実行と、ログイン環境の初期設定に関係する保留を区別する。
| 観測 | そこから分かること |
|---|---|
| 登録一覧にジョブがある | launchdに登録されている |
| PIDがない | 現時点でプロセスが見えていない。定期ジョブでは正常時にも起きる |
pended nondemand spawn がある | 自発起動が保留されている |
| ドメイン情報も確認する | 保留に関係するログイン環境の状態を調べられる |
状態を一つの表示だけで結論づけず、ジョブ側とログインドメイン側の情報を組み合わせる。監視の入力を増やすのは複雑さも増すが、「読み込み済みだから正常」という誤判定を避けるには必要だった。
救済処理は範囲を絞って周期実行する
救済スクリプトは、保留を見つけた全ジョブを無条件に起動するものではない。常駐ジョブでPIDがない場合や、一度も起動していないRunAtLoadジョブなど、条件を決めて対象を選ぶ。周期ジョブは設定ファイルに理由付きで登録されたものに限り、公開・デプロイ系は予定外に実行されないよう起こさず通知する。意図しない時間に処理を再実行すると、復旧ではなく別の障害を作るためだ。
さらに、手動のkickstartで一度動かしても、StartIntervalの周期自体が復旧せず、次の周期に再び保留された実測が記録されている。そのため、単発の救済ではなく、保留が続く間は別のカレンダー起動から周期ごとに確認し、必要なジョブだけ再度起こす設計にした。救済側は15分ごとに動くよう scripts/launchd/com.ntmedia.launchd-rescue.plist に設定されている。
まずコード配送を起こす順番にも意味がある。止まった機体へ手作業で git pull するのではなく、通常の安全装置を持つ配送処理を起こす。救済スクリプト自身はその回に古いコードで動く場合があるが、次の周期から更新済みのコードを使える。
導入時に確認する点
実装や設定を変更する場合は、少なくとも次の二点を別々に確かめる。ひとつは保留状態を正しく検出できること。もうひとつは救済対象外の公開・デプロイ処理を起こさないことだ。scripts/test_launchd_state.py と scripts/test_launchd_rescue.py には、状態解析や対象選定に関する確認が置かれている。--dry-run は実際には起こさず、何が対象になるかだけを表示する。
この構成で解決できるのは、カレンダー起動の救済処理が実行でき、対象ジョブの状態を取得できる場合の起動保留である。ログイン環境そのものが使えずカレンダー起動も動かない場合や、別の原因でジョブが失敗している場合まで自動で直すものではない。監視は「載っている」から「動いている」へ観測を広げ、救済は異なる起動条件に置き、再実行対象を絞る。今回の仕組みはこの三段で、起動保留の検知漏れと救済側の巻き添えを抑えている。
更新履歴
- 2026-10-04: 初稿作成。参照:
scripts/launchd_rescue.py、scripts/lib/launchd_state.py、scripts/launchd/com.ntmedia.launchd-rescue.plist、scripts/test_launchd_state.py、scripts/test_launchd_rescue.py。
訂正履歴
- なし。