云端 Mac CI 睡眠治理:避免无人值守构建中断

云端 Mac CI 睡眠治理:避免无人值守构建中断

夜间归档跑到一半,第二天只留下超时记录;远程桌面重新连上后,机器本身又能正常操作。这类问题不能直接归因于网络。对无人值守的云端 Mac CI,首先要分清系统睡眠、显示器睡眠、远程会话退出和 Runner 进程退出。四种状态表面相似,修复位置完全不同。

先确认中断发生在哪一层

开始修改配置前,先记录失败任务的最后输出、退出码和主机启动时间。若日志在某条构建命令中间突然停止,而系统启动时间没有变化,应继续检查睡眠与进程生命周期;若启动时间发生变化,则要按重启事件排查,不能用防休眠参数掩盖。

现象 优先检查 常见结论
画面熄灭,SSH 与构建正常 displaysleep 仅显示输出休眠
SSH 和任务同时失联 sleep、电源日志 系统可能进入睡眠
关闭终端后任务退出 Runner 启动方式 进程依附交互式会话
主机启动时间改变 重启记录、任务日志 发生过系统重启

先采集一份不改动系统的基线:

mkdir -p "$HOME/ci-audit"
date > "$HOME/ci-audit/power-baseline.txt"
pmset -g custom >> "$HOME/ci-audit/power-baseline.txt"
pmset -g assertions >> "$HOME/ci-audit/power-baseline.txt"
pmset -g sched >> "$HOME/ci-audit/power-baseline.txt"
sysctl -n kern.boottime >> "$HOME/ci-audit/power-baseline.txt"

pmset -g custom 展示不同供电场景的配置,pmset -g assertions 展示当前阻止睡眠的进程,pmset -g sched 用于发现已存在的定时事件。不要只截取一行 sleep 0 就下结论,配置档与当前实际断言需要一起看。

区分显示休眠与系统睡眠

Mac mini 没有电池,主要关注交流供电配置。显示器休眠不会停止编译,也没有必要为了 CI 永久点亮虚拟显示输出。真正影响无人值守任务的是系统睡眠,以及 Runner 是否持续持有有效进程。

专用于持续构建的独享节点,可在保存原始输出后关闭交流供电下的系统睡眠:

sudo pmset -c sleep 0 disksleep 0 powernap 0
pmset -g custom

这里没有修改 displaysleep,因为屏幕是否熄灭与后台构建无关。执行后应重新读取配置,而不是把命令返回成功当成验收结果。若机器还承担人工桌面任务,应先评估团队的使用时段;低频 CI 更适合按任务申请防休眠,不必永久改变系统策略。

电源设置解决的是机器能否保持运行,不能让一个依附终端的临时进程自动变成后台服务。

用 caffeinate 包住单次构建

caffeinate 可以在子进程存活期间创建电源断言。相比全局关闭睡眠,它的影响范围更清晰,特别适合定时归档、长时间测试和一次性依赖构建。

创建统一包装脚本:

sudo install -d -m 0755 /usr/local/bin
cat <<'EOF' | sudo tee /usr/local/bin/ci-awake >/dev/null
#!/bin/zsh
set -euo pipefail
if (( $# == 0 )); then
  exit 64
fi
exec /usr/bin/caffeinate -ims "$@"
EOF
sudo chmod 0755 /usr/local/bin/ci-awake

Runner 中不要再拼接一层后台符号,直接让包装脚本接管目标命令:

/usr/local/bin/ci-awake /usr/bin/xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  build

-i 阻止空闲系统睡眠,-m 阻止磁盘空闲,-s 在交流供电时阻止系统睡眠。目标命令结束后,断言会随包装进程释放。可在构建期间运行 pmset -g assertions,确认输出中存在对应断言。

让 Runner 脱离远程会话

如果关闭 SSH 或图形会话后构建仍会退出,重点不再是 pmset,而是 Runner 的父进程。手工在终端启动的进程可能收到会话结束信号,也可能因工作目录、环境变量或临时钥匙串上下文消失而失败。

持续运行的 Runner 应交给 launchd 托管。检查时至少确认以下项目:

  1. 服务标签固定,例如 local.ci.runner,不要每次登录后重复启动。
  2. 工作目录使用绝对路径,不能依赖终端启动时的当前目录。
  3. PATH、工具链选择和缓存目录显式设置,不继承交互式 shell 配置。
  4. 标准输出与错误输出写入不同日志文件,并配置可控的轮转策略。
  5. Runner 收到退出信号时能结束当前子进程,避免遗留占用构建目录的孤儿进程。

用户级服务可这样确认:

launchctl print "gui/$(id -u)/local.ci.runner"
pgrep -afil 'runner|xcodebuild'

若服务需要在无人登录时运行,应使用系统级 LaunchDaemon,并通过 launchctl print system/local.ci.runner 检查。不要同时保留用户级与系统级实例,否则两个 Runner 可能争用同一工作目录。

用证据完成上线验收

配置完成后,安排一次超过原中断时长的测试任务。测试期间主动关闭远程桌面和 SSH 客户端,但不要终止 Runner。重新连接后核对任务退出码、产物时间、服务进程和电源断言。

遇到再次中断时,立即采集:

date
pmset -g assertions
pmset -g log | tail -n 120
sysctl -n kern.boottime
last reboot | head

最终验收应满足四点:显示器休眠不影响任务;关闭远程会话后 Runner 仍存在;构建期间能看到 caffeinate 断言;任务结束后断言自动释放。若其中任一项失败,就回到对应层处理,不要连续叠加更多电源参数。

对 XcodeVM 上的独享云端 Mac,稳定的无人值守 CI 依赖两条边界:系统电源策略保证节点保持运行,launchd 与任务包装保证构建进程不依附人工会话。把两者分开配置和取证,才能让下一次中断留下可定位的原因,而不是一条模糊的超时记录。

常见问题

关闭显示器休眠是否等于阻止 CI 主机睡眠?

不等于。displaysleep 只影响显示输出,CI 是否继续运行取决于系统 sleep 设置和当前电源断言,应分别通过 pmset -g custom 与 pmset -g assertions 检查。

应该永久关闭睡眠,还是只在构建时使用 caffeinate?

专用于持续 CI 的独享节点可在确认原始配置后关闭交流供电下的系统睡眠;低频任务更适合用 caffeinate 包装单次构建,把防休眠范围限制在任务生命周期内。

远程桌面断开后构建仍然退出,应该先查什么?

先确认 Runner 是否由 launchd 独立托管,再检查退出码、pmset 电源日志和重启记录。若任务依附交互式终端或图形会话,单纯修改睡眠设置无法解决退出问题。

XcodeVM 云端 Mac

选择适合当前工作负载的独享物理机

核对内存、存储、节点与计费周期后进入下单配置。所有节点全年正常运行,实际可用性以控制台实时返回为准。

选择方案并租用