launchd plist 不生效主因是路径错误或权限不符:用户服务须放 ~/Library/LaunchAgents/(644权限),系统服务须放 /Library/LaunchDaemons/(root:wheel,644),且必须含 Label 和 ProgramArguments;Catalina 起需用 bootstrap 替代 load。

为什么 launchd plist 文件总不生效?先检查路径和权限
MacOS 的自启动服务必须放在特定目录下,且权限严格。放错位置或权限不对,launchctl load 会静默失败,甚至 launchctl list 都看不到你的服务。
-
~/Library/LaunchAgents/:用户登录后启动(推荐多数场景),归属当前用户,权限应为644(即-rw-r--r--) -
/Library/LaunchDaemons/:系统级启动(需 root 权限),服务在登录前就运行,文件属主必须是root:wheel,权限也必须是644 - 切勿放在
/System/Library/LaunchDaemons/—— SIP 保护,写入会失败 - 文件名建议用反向域名格式,如
com.example.myserver.plist;不能含空格或特殊字符
plist 文件里哪些键是必须的?别漏掉 Label 和 ProgramArguments
一个最小可用的 launchd plist 至少要定义 Label(唯一标识)和 ProgramArguments(实际执行命令)。用 Program 是过时写法,已不被推荐;用 RunAtLoad 控制是否开机/登录即启,但不等于“立即运行”——它只影响首次加载时机。
-
Label必须与文件名一致(不含.plist),否则launchctl会报错Could not find domain for -
ProgramArguments是数组,首项是可执行文件路径(绝对路径!),后续是参数。例如:["/usr/bin/python3", "/opt/myapp/main.py"]—— 不能写成字符串"/usr/bin/python3 /opt/myapp/main.py" - 如果脚本依赖环境变量(如
PYTHONPATH或PATH),必须显式通过EnvironmentVariables字典设置,launchd不继承 shell 环境 - 调试时加
StandardOutPath和StandardErrorPath,指向可写的日志文件,否则错误全丢弃
加载后服务没运行?用 launchctl bootstrap 替代旧式 load
macOS Catalina(10.15)起,launchctl load/unload 被废弃,强制使用层级化 bootstrap/unbootstrap。继续用 load 可能提示 Operation not permitted,尤其在 LaunchDaemons 场景下。
- 用户级服务:运行
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myserver.plist - 系统级服务:需
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.myserver.plist - 验证是否加载成功:
launchctl print gui/$(id -u) | grep com.example.myserver(用户)或launchctl print system | grep com.example.myserver(系统) - 手动触发一次运行:
launchctl kickstart -k "gui/$(id -u)/com.example.myserver",比等自动触发更快定位问题
进程退出后不自动重启?KeepAlive 的默认行为容易误解
KeepAlive 默认值是 false,设为 true 并不表示“崩溃后无限拉起”,而是“只要进程退出就立刻重启”。这在调试阶段可能造成高频 fork,撑满进程表;生产环境常需配合条件控制。
- 无条件保活:
<key>KeepAlive</key><true/> - 仅当退出码为 0 时才重启:
<key>KeepAlive</key><dict><key>SuccessfulExit</key><true/></dict> - 更安全的做法是加
ThrottleInterval(单位秒),比如30,避免 1 秒内反复崩溃重启 - 注意:
StartInterval和KeepAlive互斥;若同时存在,KeepAlive优先生效
真正难搞的是服务本身后台化逻辑 —— 比如 Python 脚本没用 daemonize 或双 fork,launchd 会认为它“已结束”,从而反复拉起。这类问题不会报错,只能靠日志和 ps aux | grep 观察实际进程状态。

















