macOS 的 cron 和 launchd
今天在终端收到一封 cron 发来的邮件,说某个脚本 “Operation not permitted”。查下去是两个问题合在一起,顺手也把 cron 和 launchd 在 macOS 上的差别重新过了一遍。
先看今天这件事#
我有三个用 cron 跑的定时任务:每小时 15 分把知识库备份到 git,每小时 20 分刷新知识库的向量索引,每天早 9 点跑一次健康检查。
有一天开始,前两个任务悄无声息失败。打开日志才看见:
/bin/bash: .../brain-git-backup.sh: Operation not permitted
/bin/bash: gbrain: command not found
第一个是权限拦截——脚本文件在 ~/Library/CloudStorage/Dropbox/ 里,macOS 的隐私系统(TCC)不让 cron 碰 iCloud、Dropbox 这种云端同步目录下的文件。
第二个是 PATH 问题。cron 跑的 bash 是个干净的非交互 shell,.zshrc 里配置的 PATH 不会加载,gbrain 找不到。
cron 和 launchd 是什么#
两者都是"定时工具"——让系统在指定时间跑一段命令。
cron 是 Unix 时代的老工具,几十年历史,Linux、Mac 都有,写一行配置就能用。
launchd 是苹果自己做的调度系统,2005 年随 macOS 10.4 推出。现在苹果系统里所有后台服务,包括 cron 本身,都归 launchd 管。
cron 在 macOS 上的状态#
从 macOS 10.4 起,cron 被标为 “deprecated”。系统里它依然可用,但实际是由 launchd 拉起的一个兼容进程(/System/Library/LaunchDaemons/com.vix.cron.plist)。之后新加的一些系统能力,接到 launchd 而没有接到 cron 上,比如 TCC 权限的细粒度授权,和 log 命令里的子系统筛选。
每个任务单独授权#
cron 只有一个进程 /usr/sbin/cron,所有定时任务都跑在它底下。给 cron 开"完全磁盘访问"权限,等于一次性给所有 cron 任务都开了。要么全开,要么全关。
launchd 不一样。每个任务是一个独立的 “agent”,放在 ~/Library/LaunchAgents/ 下的一个 plist 文件:
~/Library/LaunchAgents/com.luca.brain-backup.plist
~/Library/LaunchAgents/com.luca.brain-sync.plist
~/Library/LaunchAgents/com.luca.brain-health.plist
系统把每个 agent 当成独立程序,可以单独授权、单独关、单独看日志。
plist 大概长什么样#
就是一个 XML 文件。比如"每小时 15 分跑一次 brain-git-backup.sh":
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.luca.brain-backup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/lucawu/Dropbox/Github/Luca/script/brain-git-backup.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>15</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/lucawu/.brain-git-backup.log</string>
<key>StandardErrorPath</key>
<string>/Users/lucawu/.brain-git-backup.log</string>
</dict>
</plist>
写好丢进 ~/Library/LaunchAgents/,跑一下 launchctl load <plist 路径>,就开始工作了。
launchd 还有几个 cron 做不到的事#
- 睡眠期间错过的任务能补跑。cron 直接跳过,launchd 电脑醒来会补一次
- 进程挂了能自动重启,加一行
KeepAlive就行 - 可以监听文件变化触发。比如某个文件夹一有新文件就跑脚本,不一定按时间
- 日志接到系统 log 命令,用
log show --predicate 'subsystem == "com.luca.brain-backup"'能按服务筛
那今天我迁走了吗#
没有。三个任务用 cron 也能跑好,只要把 PATH 显式写到 crontab 顶部,再给 /usr/sbin/cron 加一次完全磁盘访问权限,两个问题都解决。
迁 launchd 是为将来准备的——等到任务多了想分开管、想让睡眠错过的任务补跑、想让某个任务挂掉自动重启,那时候再迁不迟。