NOTE · 2026-05-07
夜間 cron が 3 日連続発火していなかった: LaunchAgent + pmset wake で sleep 中の窓を開ける
毎晩 23:30 に動かしているはずの LaunchAgent が 3 日連続で一度も走っていなかった。macOS の LaunchAgent はスリープ中に発火窓が来ると skip し、起きた後も補完しない。pmset repeat wakeorpoweron で発火直前に dark wake させて窓を開けておく。
このサイトの blog draft は、別 repo の wiki vault (claude-obsidian) で毎晩 23:30 に LaunchAgent が走り、当日のログから下書き MDX を生成する構成になっている。3 日前にこの cron を仕込んで以来、blog-drafts/ には何も出てこなかった。今朝確認してようやく原因がわかったので残しておく。
何が起きていたか
infra/dev.miya8060.blog-draft.plist を ~/Library/LaunchAgents/ に置いて launchctl load -w で登録、StartCalendarInterval で 23:30 daily に発火するように書いてある。3 日経ったので blog-drafts/ を見たら空のまま。
launchctl print gui/(id -u)/dev.miya8060.blog-draft | grep -E "runs|last exit"
# runs = 0
# last exit code = (never exited)
runs = 0 は plist が正しく登録されていて発火条件待ちであるが 一度も走っていない ことを意味する。/tmp/blog-draft.{out,err}.log も不在 (走れば必ず作られる)。Python script のバグでもない、permission の問題でもない、そもそも launchd が呼んでいない。
なぜ走らなかったのか
pmset -g log | grep -E "Sleep|Wake" を当該日付で見ると、23:30 前後は全部 sleep 中。MacBook を寝る前に閉じて電源を抜いていたので当然なのだが、私は LaunchAgent が「次に起きたタイミングで補完発火する」と勘違いしていた。
実際の挙動はこう:
- LaunchAgent の
StartCalendarIntervalは 発火窓が sleep 中だと skip する - skip された run は 後から補完されない (cron と違う点)
runsカウンタは「実際に走った回数」なので 0 のまま
仕様を読み返したら "If the computer is asleep when the StartCalendarInterval fires, the job is not run" と書いてあった。読んだはずなのに、cron の感覚で「次の wake で走るやろ」と思い込んでいた。
直し: pmset で発火直前に wake させる
LaunchAgent 側はそのままで、システム側に「発火 5 分前に起きてくれ」と頼む。
sudo pmset repeat wakeorpoweron MTWRFSU 23:25:00
意味:
MTWRFSU= 月〜日 (毎日)23:25:00に wake または power on- 走り終われば macOS が再びスリープに戻す
確認:
pmset -g sched
# Repeating power events:
# wakepoweron at 11:25PM every day
取り消し:
sudo pmset repeat cancel
wakepoweron は dark wake なので画面は点かない、ファンも回さない、数十秒で再スリープに戻る。LaunchAgent の 23:30 発火窓を捕まえるだけの目的にはちょうど良い。
注意点 4 つ
pmset repeat は schedule 1 個しか登録できない。別用途 (バックアップ起こしなど) で既に使っているなら衝突する。pmset -g sched で先に確認。複数欲しければ別レイヤ (常時稼働 server に逃がす、caffeinate で sleep を抑制する) を考える。
MacBook の蓋閉じ + 電源未接続では wake しない。AC 電源接続中の clamshell mode のみ dark wake する。電源を抜いて持ち出す運用なら LaunchAgent ではどうにもならず、別アーキを検討。
sudo 必須。スクリプト化するなら sudoers に pmset を登録する必要がある。私は ! sudo pmset ... で手動実行で済ませた (毎晩は不要、一度設定すれば repeat する)。
dark wake 中の処理時間制限。dark wake は数十秒で sleep に戻るので、cron task は短時間で終わる前提。今回の blog draft 生成は数百 ms で終わるので問題なし、長時間 task は別 pattern を検討。
「cron が一度も走っていない」を疑う時の確認順
将来の自分のために手順を残しておく。
launchctl list | grep <label> # 登録されているか
launchctl print gui/(id -u)/<label> | grep -E "runs|last exit" # 発火履歴
cat /tmp/<label>.err.log # 走った場合の stderr
pmset -g sched # wake schedule の有無
pmset -g log | grep -E "Sleep|Wake" | grep <date> # 当該時間帯の sleep/wake 履歴
runs = 0 / last exit code = (never exited) が出たら、まず sleep skip を疑う。permission や script のバグを疑い始めると深い穴に入る。
なぜここで効くか
このサイトは claude-obsidian で wiki に流したログから毎晩 draft を生成して、翌朝 polish して publish する pipeline の出力先になっている。draft が出てこないと publish する元ネタが消える、つまり pipeline 全体が止まる。LaunchAgent の発火が見えないところで沈黙していると、止まったことに気づくのも遅れる。runs カウンタを最初に見る癖をつけて、発火そのものが起きているかを真っ先に確認する。
(Origin: 5/7 朝に「3 日分の draft が出ていない」と気づき、launchctl print で runs=0、pmset -g sched で wake schedule なしを確認、5/4 plist 登録から 5/4-5/6 全 skip と判定。pmset repeat wakeorpoweron MTWRFSU 23:25:00 登録で 5/7 以降の発火を担保。docs/blog-pipeline.md にもトラブルシューティング節として反映。)