睡前想让 Mac Mini 自动把屏幕关了。听起来就一行 crontab 的事,结果踩了三个坑,最后换成了 LaunchDaemon。
需求很简单
Mac Mini 作为 always-on 的服务器跑着,平时 displaysleep 设成 0(永不休眠),因为远程操作时不希望屏幕乱睡。但晚上没人看,屏幕亮一夜纯属浪费。
需求:每天 00:00 自动关闭显示器,其他时间不管。
第一反应:crontab
SSH 上去,一行搞定:
echo '0 0 * * * /usr/bin/pmset displaysleepnow' | crontab -pmset displaysleepnow 是 macOS 提供的命令,立即让显示器进入睡眠,但不影响系统本身——机器照常跑,SSH 照常连。
看起来完美。但实际上这个方案根本跑不通。
坑一:pmset 需要 root 权限
pmset displaysleepnow 不是随便哪个用户都能调的,它需要 root 权限。用户级的 crontab 执行这条命令,直接被拒:
Operation not permitted要让 crontab 能跑,要么配 sudoers 的 NOPASSWD 规则,要么用 sudo crontab -e 编辑 root 的 crontab。两种都不优雅。
坑二:macOS 的 cron 需要 Full Disk Access
从 macOS Mojave 开始,苹果给 cron 加了权限限制。/usr/sbin/cron 必须在 系统设置 → 隐私与安全 → 完全磁盘访问权限 里被授权,否则 cron job 会静默失败——不报错,就是不执行。
这个坑尤其恶心,因为你完全不知道它没跑,日志里也没什么有用信息。
坑三:Mac 睡着了 cron 不会补执行
如果 00:00 的时候 Mac 恰好处于睡眠状态(虽然我的 Mini 设了 sleep 0,但万一呢),cron 不会在唤醒后补跑错过的任务。错过就是错过了。
正解:LaunchDaemon
苹果从 2005 年就开始推 launchd 替代 cron,到现在 cron 基本算是”还能用但没人管”的状态。LaunchDaemon 才是 macOS 上定时任务的正道:
- 以 root 身份运行,不需要 sudo 配置
- 支持错过补执行,如果触发时机器在睡觉,醒来后会补上
- 不需要 Full Disk Access 这种额外权限配置
- Apple 官方推荐,和系统集成更好
创建 plist 文件 /Library/LaunchDaemons/com.local.displaysleepnow.plist:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>Label</key> <string>com.local.displaysleepnow</string> <key>ProgramArguments</key> <array> <string>/usr/bin/pmset</string> <string>displaysleepnow</string> </array> <key>StartCalendarInterval</key> <dict> <key>Hour</key> <integer>0</integer> <key>Minute</key> <integer>0</integer> </dict></dict></plist>几个关键点:
Label是这个 daemon 的唯一标识,随便起,但要有辨识度ProgramArguments用数组形式传命令和参数,不要写成一整个字符串StartCalendarInterval指定触发时间,格式类似 crontab 但用 dict 表示。只写Hour和Minute意味着每天都触发
安装和加载:
sudo cp com.local.displaysleepnow.plist /Library/LaunchDaemons/sudo chown root:wheel /Library/LaunchDaemons/com.local.displaysleepnow.plistsudo chmod 644 /Library/LaunchDaemons/com.local.displaysleepnow.plistsudo launchctl load /Library/LaunchDaemons/com.local.displaysleepnow.plist权限必须是 root:wheel + 644,否则 launchctl 会拒绝加载,报 “Path had bad ownership/permissions” 之类的错。
验证文件就位:
ls -la /Library/LaunchDaemons/com.local.displaysleepnow.plist-rw-r--r-- 1 root wheel 562 Apr 1 13:56 com.local.displaysleepnow.plistLaunchDaemon vs LaunchAgent
macOS 的 launchd 有两种配置:
| 类型 | 位置 | 运行身份 | 适用场景 |
|---|---|---|---|
| LaunchDaemon | /Library/LaunchDaemons/ | root | 系统级任务,需要高权限 |
| LaunchAgent | ~/Library/LaunchAgents/ | 当前用户 | 用户级任务,不需要 sudo |
pmset displaysleepnow 需要 root,所以必须用 LaunchDaemon。如果你的定时任务不需要特殊权限(比如跑个备份脚本),LaunchAgent 就够了。
displaysleepnow vs sleepnow
别搞混这两个:
pmset displaysleepnow—— 只关显示器,系统正常运行,SSH/远程连接不受影响pmset sleepnow—— 整台机器睡眠,除非开了 Wake on LAN,否则远程连不上
对于当服务器用的 Mac Mini,肯定选前者。
如果想卸载
sudo launchctl unload /Library/LaunchDaemons/com.local.displaysleepnow.plistsudo rm /Library/LaunchDaemons/com.local.displaysleepnow.plist先 unload 再删文件,顺序不能反。
小结
一个”每天零点关屏”的需求,crontab 方案看着一行搞定,实际上权限、FDA 授权、补执行三个坑等着你。macOS 上做定时任务,直接上 LaunchDaemon/LaunchAgent,省心。