miya8060
← back to blog

NOTE · 2026-05-07

夜間 cron が 3 日連続発火していなかった: LaunchAgent + pmset wake で sleep 中の窓を開ける

毎晩 23:30 に動かしているはずの LaunchAgent が 3 日連続で一度も走っていなかった。macOS の LaunchAgent はスリープ中に発火窓が来ると skip し、起きた後も補完しない。pmset repeat wakeorpoweron で発火直前に dark wake させて窓を開けておく。

macoslaunchdcronautomation

このサイトの 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:00wake または 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 printruns=0pmset -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 にもトラブルシューティング節として反映。)