系统学习 故障排查学习路径:从方法论到系统、网络、数据库与 K8s

macos-launchd-failed

macOS 的 launchd 代理或守护进程无法启动、反复退出时的定位与修复

现象

登录后某个后台服务没有运行,launchctl list 里对应任务显示 PID 为 -、Status 为非零;手动 launchctl bootstrap 时报 Bootstrap failed: 5: Input/output error、119: Service is disabled 或 service already loaded。有的任务能启动但几秒后退出,launchctl print 中 runs 不断增加,系统日志出现 Service exited with abnormal code 或 Throttling respawn: Will start in 10 seconds。需要图形会话的代理还可能报找不到窗口服务器,错误码常见 -600。

这类问题通常不是 launchd 本身坏了,而是 plist、可执行文件、权限、domain 或运行环境不满足要求。先确认任务属于哪个 domain、当前是否被禁用、进程最后一次退出码是什么,再决定修配置还是修程序。

快速定位

先按 Label 找到任务并读取完整状态:

launchctl list | grep -i com.example
launchctl print gui/$(id -u)/com.example.agent
launchctl print system/com.example.daemon
launchctl print-disabled gui/$(id -u)
launchctl print-disabled system

print 输出里的 state、path、program、arguments、last exit code、runs、KeepAlive 和 environment 是判断依据。PID 为 - 只表示当前没运行;last exit code 才是进程退出时留下的线索。还要检查 plist 语法、所有者和权限:

plutil -lint ~/Library/LaunchAgents/com.example.agent.plist
plutil -p ~/Library/LaunchAgents/com.example.agent.plist
ls -l ~/Library/LaunchAgents/com.example.agent.plist
ls -l /Library/LaunchDaemons/com.example.daemon.plist
ls -l /usr/local/bin/example
codesign --verify --strict --verbose=2 /usr/local/bin/example

如果 print 找不到服务,先用 launchctl print-disabled 查禁用覆盖,再确认 plist 是否真的位于当前 domain 对应的目录。错误日志用统一日志系统读取:

/usr/bin/log show --last 30m --style compact \
  --predicate 'process == "launchd" AND eventMessage CONTAINS[c] "com.example"'
/usr/bin/log show --last 30m --style compact --predicate 'process == "example"'

在 zsh 中 log 可能被同名内建命令占用,直接写 log show 会报参数错误,使用 /usr/bin/log 可以避免歧义。launchctl blame gui/$(id -u)/com.example.agent 会说明任务为什么正在运行,例如由 KeepAlive 或按需事件触发,可与 print 里的 runs 对照。

常见根因

  1. plist 语法错误、Label 与文件名或调用目标不一致,或同一个 Label 被加载了两次。
  2. ProgramArguments 第一项路径不存在、不可执行,脚本缺少可执行位或 shebang 指向不存在的解释器;路径里写了 ~、$HOME 等 launchd 不会展开的内容。
  3. 文件或目录权限不对:LaunchDaemon 需要 root:wheel,LaunchAgent 归登录用户,且目录不能被组或其他用户写;权限过宽会被 launchd 拒绝加载。
  4. 任务已被 launchctl disable、plist 里 Disabled 为真,或系统设置中的登录项被关闭,表现为 119: Service is disabled。
  5. 旧的 job 仍留在 domain 中,再次 bootstrap 报 already loaded;或者先 load 后 bootstrap 造成两套接口混用。
  6. GUI 任务被放进 system domain,或通过 SSH 在没有图形登录会话时加载 gui/$UID,任务拿不到窗口服务器。
  7. KeepAlive 为真但程序启动即崩溃,launchd 每十秒左右重新拉起,形成重启循环,真正的错误在程序日志里。
  8. launchd 不读取 shell 配置文件,PATH、HOME、语言环境和代理变量与终端里不同;脚本依赖 source ~/.zshrc 时最容易失败。
  9. StandardOutPath、StandardErrorPath 或 WorkingDirectory 指向不存在的目录,launchd 不会替你创建目录,任务直接退出。
  10. 需要访问受保护目录、摄像头、麦克风或网络的代理缺少隐私授权,或被沙箱、代码签名策略拦截。

修复

先修 plist 和路径:用 plutil -lint 通过后,把解释器、脚本、工作目录全部写成绝对路径,并在 EnvironmentVariables 中显式声明需要的 PATH。输出日志放到已存在的 ~/Library/Logs 或 /Library/Logs 下,先 mkdir -p 建好目录。

再清理旧状态并重新加载。LaunchAgent 用 GUI domain,LaunchDaemon 用 system domain:

launchctl bootout gui/$(id -u)/com.example.agent 2>/dev/null
launchctl enable gui/$(id -u)/com.example.agent
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.agent.plist
launchctl kickstart -k gui/$(id -u)/com.example.agent
sudo launchctl bootout system/com.example.daemon 2>/dev/null
sudo launchctl enable system/com.example.daemon
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.daemon.plist
sudo launchctl kickstart -k system/com.example.daemon

如果任务被禁用,enable 后再 bootstrap;如果报 already loaded,先 bootout 再加载,不要用 kill -9 代替卸载。修改 plist 后只 kickstart -k 不一定重新读取文件,可靠做法是 bootout 后重新 bootstrap。

KeepAlive 服务反复退出时,不要先把 KeepAlive 关掉掩盖问题。用 launchctl print 的 last exit code 和程序日志定位崩溃原因;如果是依赖的网络或磁盘尚未就绪,用 KeepAlive 的 NetworkState 等条件,或在脚本里做带退避的重试。GUI 代理必须由已登录用户在 gui/$UID 加载;远程维护时用 launchctl asuser $(id -u) ... 进入用户上下文,或先建立本地图形登录。

权限问题按目录修正:Daemon 的 plist 和可执行文件归 root:wheel,Agent 归当前用户,目录不要组可写。可执行文件被修改后签名可能失效,开发中的脚本不必签名,但已签名的二进制要重新签名或重新安装。涉及隐私数据时,在“系统设置 > 隐私与安全性”中给对应可执行文件授权,再重启任务验证。

如果任务由应用内的“登录时打开”或后台任务注册接口创建,plist 可能不在用户目录,而由系统的后台任务数据库管理。此时先用 launchctl print gui/$(id -u)/<Label> 找到实际路径,再检查“系统设置 > 通用 > 登录项与扩展”,不要在多个位置重复注册同一个 Label。应用更新后旧注册残留时,先在应用内关闭再重新开启,必要时重新登录。

最后用 launchctl print 确认 state = running、PID 存在、runs 不再异常增长,并观察一个完整的重启或登录周期。保留 plist 备份和修复前后的日志,便于回滚。

相关命令

参考链接

读完自测

合上页面,先回答,再展开对照。

  1. launchctl list 中 PID 为 -、Status 为 78,说明什么?下一步查哪里?
  2. bootstrap 报 119: Service is disabled 或 already loaded,分别怎么处理?
  3. KeepAlive 服务每隔十秒重启一次,应该怎样定位而不是绕过?
答案
  1. PID 为 - 表示任务当前没有运行;Status 78 是进程上次退出留下的状态,通常对应配置错误。下一步用 plutil -lint 检查 plist,launchctl print 看 last exit code 和参数,再核对权限、路径、domain 与禁用状态。
  2. 119 表示任务被禁用,先用 launchctl print-disabled 确认,再 launchctl enable 后重新 bootstrap;already loaded 表示旧 job 还在,先 bootout 对应 service,再 bootstrap,不要混用 load/unload。
  3. 用 launchctl print 看 runs、last exit code 和 KeepAlive,用 /usr/bin/log show 查 launchd 与程序日志,找到程序退出原因;依赖未就绪时增加重试或启动条件,而不是直接关闭 KeepAlive。
微信小程序码:扫码打开命令速查小程序

小程序版

微信扫码,随时查命令