Apple 官方文档明确区分了系统上下文的 LaunchDaemon 与用户会话中的 LaunchAgent;因此,需要 Keychain、代码签名或 iOS Simulator 的 CI Agent,不应简单塞进 LaunchDaemon。本周建议先采用“系统恢复层 + 专用用户构建层”:LaunchDaemon 负责主机健康检查、网络与恢复辅助,LaunchAgent 负责真正的用户态构建,并用真实签名流水线验证恢复结果。(developer.apple.com)
适用对象与判断边界
这篇文章适合负责 Mac 构建节点无人值守运行的 IT 负责人,尤其是正在处理“Mac 已重启、SSH 可达,但流水线仍离线”的团队。
如果团队只运行不依赖用户目录、Keychain 或图形会话的主机级探针,LaunchDaemon 可以独立承担任务;如果涉及签名、模拟器或用户凭证,则应继续读完运行上下文和恢复验收部分。
运行上下文
LaunchDaemon、全局 LaunchAgent 与用户级 LaunchAgent
在 macOS 中,“开机启动”“用户登录后启动”和“能够执行生产构建”不是同一个状态。LaunchDaemon 属于系统上下文,通常在系统启动阶段由 launchd 管理;LaunchAgent 则属于用户会话,在相应用户登录后运行。Apple 对这两类服务的生命周期和身份边界有明确说明。(developer.apple.com)
| 模式 | 启动条件 | 运行身份 | 适合职责 | 主要限制 |
|---|---|---|---|---|
| LaunchDaemon | 系统启动或按需触发 | 系统账号或指定服务账号 | 主机探活、端口检查、恢复辅助、无用户依赖的监控 | 默认没有交互式用户会话,不能假设用户 Keychain 或模拟器可用 |
| 全局 LaunchAgent | 指定用户登录后 | 登录用户 | 团队统一安装的 CI Agent、用户态构建服务 | 用户退出后停止;必须验证登录链路 |
| 用户级 LaunchAgent | 指定用户登录后 | 专用 CI 账号 | 签名、模拟器、用户目录、构建缓存 | 配置依赖该账号,账号策略和会话必须可恢复 |
Apple 官方资料还指出,用户登录时会启动该用户的 launchd,并加载系统级和用户目录中的 LaunchAgent;用户退出时,相关 Agent 会收到终止信号。换句话说,LaunchAgent 的“自动拉起”通常是登录事件之后的自动拉起,而不是磁盘刚解锁时就已经可用。(developer.apple.com)
因此,验收时必须把状态拆开记录:
- 主机已经完成启动;
- 启动磁盘已经解锁;
- 专用构建账号已经建立用户会话;
- CI Agent 已注册并在线;
- Keychain 已可访问;
- 真实构建已经完成签名或模拟器测试。
控制台显示在线,只能证明其中一部分。它不能替代生产构建成功证据。
iOS CI 的运行归属
对于 iOS CI,默认选择不是把所有任务都交给一种服务,而是按依赖拆分。
某主流 CI Agent 的官方 macOS 安装文档明确将用户模式 LaunchAgent 作为支持模式,并指出该模式运行在已认证用户下,能够使用用户 Keychain 和图形会话;同一文档同时说明,系统级 LaunchDaemon 没有用户会话,不能直接替代这种部署方式。(docs.gitlab.com)
这条结论不能机械推广到所有 CI 工具。不同 Agent 的安装方式、权限需求和官方支持范围必须单独核查,但它足以说明一个常见边界:如果流水线必须读取签名身份、调用 iOS Simulator 或访问用户目录,用户级 LaunchAgent 通常更符合运行条件。
用户态依赖
Keychain 访问边界
问题通常不在 launchd 没有启动进程,而在于进程启动时所处的安全上下文不同。
官方技术说明指出,在用户上下文之外运行的 launchd daemon 必须面向文件型 Keychain;数据保护 Keychain 只能由用户上下文中的程序访问。也就是说,将依赖登录用户 Keychain 的签名流程直接改成 root LaunchDaemon,可能导致身份查询失败、临时 Keychain 不可用,或构建过程在签名阶段中断。(developer.apple.com)
代码签名还涉及证书、私钥、访问控制和签名身份是否被当前进程识别。官方代码签名文档建议通过 security find-identity -p codesigning -v 检查当前环境可见的签名身份,而不是只检查证书文件是否存在。(developer.apple.com)
建议在专用 CI 用户会话中执行以下检查:
whoami
security find-identity -p codesigning -v
security list-keychains
xcrun simctl list devices
预期结果不应只是命令返回成功,而应包括:
whoami输出专用 CI 账号;- 签名身份列表中存在流水线需要的有效身份;
- Keychain 搜索路径指向受控位置;
- Simulator 设备列表能够正常返回;
- 后续真实构建能够完成签名或测试。
iOS Simulator、窗口服务与用户目录
模拟器并不是一个单纯的后台二进制程序。它可能依赖用户目录、图形会话、设备运行时、缓存路径和当前用户拥有的服务状态。因此,SSH 能登录并不等于模拟器已经具备可运行条件。
对于采用 LaunchAgent 的 CI Agent,配置文件应放在专用账号的用户目录中,并让 Agent 使用固定的 HOME、工作区和日志目录。下面的片段只保留用于说明运行身份和输出位置的最小字段,实际字段必须以所用 Agent 的官方安装文档为准:
<?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.example.ci-agent</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/ci-agent</string>
<string>run</string>
</array>
<key>StandardOutPath</key>
<string>/Users/ci/Library/Logs/ci-agent.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/ci/Library/Logs/ci-agent.err.log</string>
</dict>
</plist>
不要把 UserName、HOME、Keychain 路径和工作区路径混在多个脚本中隐式传递。配置越分散,重启后越难判断是 Agent 没启动、账号不对,还是构建环境变量丢失。
无人值守恢复链路
FileVault 与无人值守恢复
FileVault 会把“磁盘解锁”和“用户登录”连在一起,但企业验收不能因此假设所有策略都能无人值守。官方资料说明,在部分配置下,成功完成 FileVault 解锁后会自动进入对应用户;同时,自动登录选项在开启 FileVault 时可能不可用或被组织策略禁止。(support.apple.com)
企业需要先明确恢复模型:
- 允许受控的专用 CI 账号登录:适合需要用户 Keychain、Simulator 和签名的节点,但必须评估凭证暴露风险。
- 人工或远程完成 FileVault 解锁:安全边界更清晰,但不属于完全无人值守,需要把人工介入时间计入 SLA。
- 使用设备管理和恢复密钥策略:适合有集中管理能力的团队,必须验证恢复密钥托管、权限审批和审计记录。
- 利用系统版本支持的远程解锁能力:官方安全文档说明,Apple Silicon 且运行 macOS 26 或更高版本的设备,在启用 Remote Login 并具备网络连接时,可以在重启后通过 SSH 解锁 FileVault;这项能力仍需结合企业安全策略和实际版本进行验证。(support.apple.com)
因此,FileVault 的验收不能只问“能不能自动重启”。真正要问的是:重启后谁能够解锁磁盘、谁能够建立用户会话、凭证是否进入可用状态,以及这些动作是否留下审计记录。
重启恢复场景
建议至少执行以下 5 类测试,每类测试都保存时间、主机状态、Agent 状态、Keychain 检查结果和真实构建编号:
- [ ] 冷启动:从关机状态启动,确认主机可达、磁盘解锁、用户会话和 CI Agent 依次恢复。
- [ ] 计划重启:通过受控命令重启,确认 Agent 不依赖人工打开终端或图形应用。
- [ ] 意外断电:模拟异常掉电后恢复,确认工作区不会因锁文件或缓存损坏而阻塞下一次构建。
- [ ] 用户退出:让专用 CI 账号退出,确认系统能够识别 Agent 离线,而不是继续报告虚假的在线状态。
- [ ] FileVault 解锁:分别验证有人工介入和无人工介入两种策略,记录哪一个环节阻断了生产流水线。
- [ ] 真实签名构建:不要只运行健康检查,要执行包含签名和必要模拟器步骤的流水线。
- [ ] 远程重启回滚:测试失败后,确认 IT 能够远程停止错误 Agent、恢复上一版配置并重新注册节点。
⚠️ 经验上,KeepAlive 只能表达“进程应保持运行”或“退出后尝试重新启动”的管理意图,不能证明账号正确、Keychain 可用或构建逻辑成功。持续崩溃的 Agent 可能被反复拉起,却仍然无法接单;因此必须把退出状态和真实构建结果分开记录。相关启动行为和异常处理边界应以官方 launchd 文档为准。(developer.apple.com)
权限与可观测性
双层启动架构
生产环境建议按职责分层,而不是让一个 root 进程承担所有任务。
系统恢复层使用 LaunchDaemon,负责:
- 检查主机是否在线、磁盘空间和基础网络是否可用;
- 检查用户态 CI Agent 是否存在、是否长时间未更新状态;
- 记录进程退出码、重启次数和配置版本;
- 触发受控恢复动作,但不读取发布证书和私钥;
- 在无法恢复时发出告警并标记节点不可调度。
用户构建层使用专用账号的 LaunchAgent,负责:
- 访问用户目录和工作区;
- 读取受控 Keychain;
- 执行代码签名;
- 调用 Simulator 和 Xcode 工具链;
- 向 CI 控制面注册、接单和回传日志。
Apple 的服务设计文档也给出了类似的职责分离思路:如果一个系统同时提供用户无关服务和用户专属服务,可以分别使用 daemon 与 agent,让系统层处理独立能力,让用户层处理会话相关能力。(developer.apple.com)
最小权限指标
权限决策不要只写“使用非 root 账号”,而要记录每类账号实际能访问的范围:
| 运行账号 | 可访问内容 | 适合任务 | 否决条件 |
|---|---|---|---|
| root 或系统账号 | 主机级目录、系统服务和网络配置 | 健康检查、恢复辅助 | 直接读取发布 Keychain、执行完整签名流水线 |
| 专用 CI 账号 | 专用工作区、用户 Keychain、模拟器和构建缓存 | 生产构建、签名、测试 | 与开发者个人账号共用凭证或管理员权限 |
| 共享管理员账号 | 范围通常过大,审计边界不清 | 临时迁移或人工维护 | 长期无人值守生产构建 |
每次升级系统、Agent 或配置文件,都应先在隔离节点复测。至少保存以下信息:
# 查看当前用户域中的服务状态
launchctl print "gui/$(id -u)/com.example.ci-agent"
# 查看进程与退出状态
ps -axo user,pid,ppid,state,etime,command | grep -i ci-agent
# 检查日志最近内容
tail -n 80 "$HOME/Library/Logs/ci-agent.err.log"
日志中应能区分 4 类故障:进程崩溃、用户会话缺失、凭证不可用、任务路由失败。只记录“服务在线”或“端口可访问”,无法支持采购验收和故障追责。
方案准入与节点选择
在结尾前,可以用下面两张表判断现有主机是否值得整改。第一张表关注启动模式,第二张表关注供应和容量要求;两者不能互相替代。
| 方案 | 适用条件 | 关键证据 | 直接否决项 |
|---|---|---|---|
| 仅 LaunchAgent | 构建完全依赖用户会话,且已有可靠登录与解锁流程 | 用户会话建立、Keychain 可用、真实签名成功 | 重启后无法建立会话,或没有远程恢复路径 |
| 仅 LaunchDaemon | 任务不依赖用户 Keychain、Simulator 和图形服务 | 系统启动后探针、网络和任务执行成功 | 构建需要用户态签名或模拟器 |
| 双层架构 | 生产节点需要无人值守,同时存在用户态构建依赖 | 主机恢复、Agent 注册、凭证隔离、真实构建、远程重启 | 系统层与用户层共用高权限凭证,或没有备用恢复方式 |
| 验收指标 | 通过标准 | 不通过时的处理 |
|---|---|---|
| 主机可达 | 重启后 SSH 或管理通道可用 | 检查网络、磁盘解锁和远程管理策略 |
| 用户会话 | 专用 CI 账号进入预期会话 | 回退到登录策略和 FileVault 解锁链路 |
| Agent 注册 | Agent 使用正确账号在线并能接单 | 核查 LaunchAgent 域、配置路径和账号归属 |
| Keychain | 签名身份可见,临时 Keychain 可读写 | 禁止把流程直接迁移到 root LaunchDaemon |
| 真实流水线 | 签名、测试和产物回传均成功 | 节点不得进入生产调度 |
| 远程恢复 | IT 能完成重启、停止错误进程和回滚配置 | 增加系统恢复层或改用可独立恢复的节点 |
| 备用容量 | 主节点不可用时有明确切换路径 | 重新评估节点数量、租赁周期和容量冗余 |
如果现有 Mac 只能做到“重启后 SSH 在线”,却无法独立完成用户会话、Keychain 恢复和真实签名构建,那么问题不是再增加一个 KeepAlive 参数就能解决,而是节点的恢复链路没有达到生产准入标准。
对于长期重负载、必须接入本地硬件或需要完全掌握物理设备的团队,自购 Mac 仍然可能更合适;但如果目标是临时增加 CI 容量、快速补充备用节点,或者需要远程重启、完整 root 权限和可独立验证的 Mac 环境,传统自购方案往往还要承担采购周期、硬件维护、故障替换和闲置折旧。此时,使用 NodeMini 的远程 Mac 租赁方案,可以先按实际流水线验证用户会话、签名和恢复流程,再决定是否扩大长期容量;相关的 Mac 远程算力租赁方案 可作为节点采购评估的一部分。
本周可以先复制上面的验收清单,在一台非核心节点上完成冷启动、计划重启、FileVault 解锁和真实签名构建。如果现有节点无法提供独立用户会话、远程重启与备用容量,再把 企业 Mac 构建节点方案 纳入供应评估,而不是继续用“主机在线”替代“流水线已恢复”。