夜間封存執行到一半中斷,隔天只留下逾時紀錄;重新連上遠端桌面後,機器本身卻又能正常操作。這類問題不能直接歸咎於網路。對無人值守的雲端 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 管理。檢查時至少要確認以下項目:
- 服務標籤固定,例如
local.ci.runner,不要在每次登入後重複啟動。 - 工作目錄使用絕對路徑,不能依賴從終端機啟動時的目前目錄。
- 明確設定
PATH、工具鏈選擇與快取目錄,不要繼承互動式 shell 設定。 - 將標準輸出與標準錯誤輸出寫入不同的日誌檔案,並設定可控的輪替策略。
- 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 只影響顯示輸出,系統是否持續執行仍取決於 sleep 設定與目前的電源斷言,兩者必須分別檢查。
應永久停用睡眠,還是只在建置時使用 caffeinate?
持續執行 CI 的獨享節點可在保存原始設定後停用交流供電下的系統睡眠;低頻工作則適合用 caffeinate 限定防休眠時間。
遠端工作階段中斷後建置退出,應先檢查什麼?
先確認 Runner 是否由 launchd 獨立託管,再比對退出碼、電源記錄與重新啟動記錄。依附互動式終端的程序不會因修改睡眠設定而自動成為常駐服務。
XcodeVM 雲端 Mac
選擇適合目前工作負載的獨享實體主機
核對記憶體、儲存空間、節點與計費週期後,即可進入下單設定。所有節點全年正常運作,實際可用性以控制台即時回傳的結果為準。