Daemon:系統服務
職責
resolveGatewayService(resolveGatewayService:370-375) 是 OpenClaw 把 gateway 安裝成「系統級常駐服務 (daemon)」的統一入口:macOS 上是 LaunchAgent(launchd 管理)、Linux 上是 systemd user unit(systemd --user 管理)、Windows 上是 Scheduled Task(schtasks 註冊的登錄觸發任務)。三種平台各自的指令封裝成同一個 GatewayService(GatewayService:75-87)介面:stage/install/uninstall/stop/restart/isLoaded/readCommand/readRuntime,CLI 層不關心平台差異。
它處理的不是「執行 gateway」——gateway 本身還是 node openclaw.mjs gateway。Daemon 這層做的是把這條指令寫進系統服務清單 (plist/unit/XML),再讓系統服務管理器按規則拉起、監控、自動重啟。所以 daemon 模組大量程式碼在 plist/unit/XML 模板渲染和 launchctl/systemctl/schtasks CLI 呼叫上,真正的 gateway 進程生命週期由系統服務管理器掌控。
設計動機
為什麼不直接 nohup openclaw gateway &?
- 開機自啟 + 崩潰重啟:
launchd的KeepAlive=true、systemd 的Restart=always、schtasks 的LogonTrigger都是系統級機制,機器重啟或 OpenClaw 進程意外退出時自動拉起。nohup只能「忽略 SIGHUP」,機器重啟就廢。 - 設定統一 + profile 隔離:
OPENCLAW_PROFILE可以讓同一台機器跑多套 gateway(比如開發/生產分開),resolveGatewayLaunchAgentLabel(resolveGatewayLaunchAgentLabel:33-39)把 profile 編進 service label——ai.openclaw.gateway(default)或ai.openclaw.<profile>,systemd 和 schtasks 也對應加後綴。每個 profile 獨立 plist/unit/XML,互不干擾。 - future config guard:
withFutureConfigGuard(withFutureConfigGuard:336-362)在 stage/install/uninstall/stop/restart 每個寫操作前呼叫assertFutureConfigActionAllowed,如果偵測到當前 OpenClaw 版本比服務設定寫入時新(或反之,設定由未來版本產生),就拒絕改寫——這是防止「降級啟動把新格式的服務檔寫壞」的安全網。 - repair 偵測:
collectGatewayServiceStartRepairIssues(collectGatewayServiceStartRepairIssues:138-172)在restart前檢查已載入服務的OPENCLAW_SERVICE_VERSION、program path 是否指向臨時目錄(isTemporaryProgramPath)、program 檔案是否還存在(isMissingProgramPath)。版本漂移或路徑失效會要求重裝,而不是假裝 restart 成功。
關鍵檔案
GatewayService 型別:75-87— 8 個方法的統一介面。GATEWAY_SERVICE_REGISTRY:294-334—darwin/linux/win32三套 adapter,各對應一個平台實作模組。withFutureConfigGuard:336-362— 包裝 stage/install/uninstall/stop/restart,前置assertFutureConfigActionAllowed檢查。resolveGatewayService:370-375— 按process.platform選 adapter,未知平台回傳createUnsupportedGatewayService(所有方法都 throwunsupported)。isTemporaryProgramPath:115-136—/tmp//var/tmp/os.tmpdir()路徑偵測,服務指向臨時目錄會被標成需要修復。collectGatewayServiceStartRepairIssues:138-172—OPENCLAW_SERVICE_VERSION漂移 + 臨時路徑 + 檔案缺失,三類 start repair issue。service label 常數:5-17—GATEWAY_LAUNCH_AGENT_LABEL="ai.openclaw.gateway"、GATEWAY_SYSTEMD_SERVICE_NAME="openclaw-gateway"、GATEWAY_WINDOWS_TASK_NAME="OpenClaw Gateway"、legacyclawdbot-gateway。profile-aware 名稱:33-60—resolveGatewayLaunchAgentLabel/resolveGatewaySystemdServiceName/resolveGatewayWindowsTaskName,profile 後綴規則。installLaunchAgent:1013-1030—writeLaunchAgentPlist+activateLaunchAgent,macOS 裝載。launchd 常數:5-13—LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS=10、LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=20、LAUNCH_AGENT_UMASK_DECIMAL=0o077(owner-only 檔案)。buildLaunchAgentPlist:267-295— plist 渲染:RunAtLoad=true/KeepAlive=true/ExitTimeOut=20/ThrottleInterval=10/Umask=0o077/StandardOutPath/StandardErrorPath。installSystemdService:1107-1131—writeSystemdUnit+activateSystemdService。buildSystemdUnit:52-101—[Unit]/[Service]/[Install]三段,Restart=always/RestartSec=5/TimeoutStopSec=30/OOMPolicy=continue/KillMode=control-group。readSystemdUserLingerStatus:28-50— headless 部署需要loginctl enable-linger,這裡讀狀態。buildScheduledTaskXml:139-192— Windows Task Scheduler XML,LogonTrigger/LeastPrivilege/DisallowStartIfOnBatteries=false。installScheduledTask:1313-1325—writeScheduledTaskScript+activateScheduledTask。writeTaskXmlTempFile:194-203—schtasks /XML要求 UTF-16 LE BOM,Nodeutf16le+ 手寫FFFEBOM。GATEWAY_DIST_ENTRYPOINT_BASENAMES:5-22—index.js/index.mjs/entry.js/entry.mjs,從 dist 目錄裡選可用入口。
資料流
resolveGatewayService(resolveGatewayService:370)用平台登錄表選 adapter:
const GATEWAY_SERVICE_REGISTRY: Record<SupportedGatewayServicePlatform, GatewayService> = {
darwin: {
label: "LaunchAgent",
loadedText: "loaded",
notLoadedText: "not loaded",
stage: ignoreServiceWriteResult(stageLaunchAgent),
install: ignoreServiceWriteResult(installLaunchAgent),
uninstall: uninstallLaunchAgent,
stop: stopLaunchAgent,
restart: restartLaunchAgent,
isLoaded: isLaunchAgentLoaded,
readCommand: readLaunchAgentProgramArguments,
readRuntime: readLaunchAgentRuntime,
},
linux: { label: "systemd user", /* ...systemd 實作 */ },
win32: { label: "Scheduled Task", /* ...schtasks 實作 */ },
};
export function resolveGatewayService(): GatewayService {
if (isSupportedGatewayServicePlatform(process.platform)) {
return withFutureConfigGuard(GATEWAY_SERVICE_REGISTRY[process.platform]);
}
return createUnsupportedGatewayService();
}withFutureConfigGuard 給所有寫方法套一層 assertFutureConfigActionAllowed,這是「先檢查再寫」的統一攔截——避免用舊版本 OpenClaw 去改寫新版本產生的服務檔導致格式損壞。
launchd 的 plist 模板(buildLaunchAgentPlist:267)是最複雜的一個:
return `<?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>${plistEscape(label)}</string>
${commentXml}
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ExitTimeOut</key><integer>${LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS}</integer>
<key>ProcessType</key><string>${LAUNCH_AGENT_PROCESS_TYPE}</string>
<key>ThrottleInterval</key><integer>${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}</integer>
<key>Umask</key><integer>${LAUNCH_AGENT_UMASK_DECIMAL}</integer>
<key>ProgramArguments</key>
<array>${argsXml}
</array>
${workingDirXml}
<key>StandardInPath</key><string>/dev/null</string>
<key>StandardOutPath</key><string>${plistEscape(stdoutPath)}</string>
<key>StandardErrorPath</key><string>${plistEscape(stderrPath)}</string>${envXml}
</dict>
</plist>
`;四個關鍵鍵:RunAtLoad=true(裝載時立即啟動)、KeepAlive=true(任何退出都重啟)、ExitTimeOut=20(SIGTERM 後等 20s 再 SIGKILL)、ThrottleInterval=10(launchd 自帶的崩潰循環保護,兩次啟動間隔至少 10s)。Umask=0o077 讓 gateway 建立的所有檔案預設 owner-only——避免 group/other 讀到 secrets。envXml 把 OPENCLAW_STATE_DIR/OPENCLAW_PROFILE 等環境變數嵌進 plist,這樣不需要額外的 env-file。
systemd unit(buildSystemdUnit:68)用三段標準結構:
return [
"[Unit]",
descriptionLine,
"After=network-online.target",
"Wants=network-online.target",
"StartLimitBurst=5",
"StartLimitIntervalSec=60",
"",
"[Service]",
`ExecStart=${execStart}`,
"Restart=always",
"RestartSec=5",
"RestartPreventExitStatus=78", // EX_CONFIG: 設定錯誤不重啟,避免死循環
"TimeoutStopSec=30",
"TimeoutStartSec=30",
"SuccessExitStatus=0 143", // 143 = SIGTERM 終止算正常退出
"OOMPolicy=continue", // 子進程被 OOM 殺掉時主服務不停
"KillMode=control-group", // 重啟時把所有子進程一併 KILL,避免孤兒 ACP worker
workingDirLine,
...environmentFileLines,
...envLines,
"",
"[Install]",
"WantedBy=default.target",
"",
].filter((line) => line !== null).join("\n");OOMPolicy=continue 和 KillMode=control-group 是兩個非預設值——前者保證子進程 OOM 不會拖垮整個 gateway,後者保證 gateway 重啟時不留 ACP/agent worker 孤兒。StartLimitBurst=5/StartLimitIntervalSec=60 是 systemd 自帶的崩潰循環保護,60s 內連續崩 5 次後停止重啟(需要人工介入)。
Windows schtasks 的 XML(buildScheduledTaskXml:152)用 LogonTrigger(使用者登入時觸發)、LeastPrivilege(不用管理員)、DisallowStartIfOnBatteries=false(筆電電池模式也執行)。一個隱藏坑:schtasks /XML 要求 UTF-16 LE BOM(writeTaskXmlTempFile:194),Node 的 utf16le 編碼不會自動加 BOM,程式碼手動拼 Buffer.from([0xff, 0xfe]) + body。
邊界與失敗
- 不支援平台降級:
createUnsupportedGatewayService(createUnsupportedGatewayService:275-292)的所有寫方法都throw createUnsupportedGatewayServiceError(),readCommand回傳 null,readRuntime回傳{ status: "unknown", detail: ... }。FreeBSD/OpenBSD 等process.platform不在表裡的系統會得到這個降級服務。 - 臨時路徑偵測:
TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"](TEMP_PROGRAM_ROOTS:115)。如果已載入服務的 program path 指向這些目錄,collectGatewayServiceStartRepairIssues會標 issue——臨時目錄可能被系統清理,服務會突然找不到可執行檔。這種狀態需要重裝而不是簡單 restart。 - 版本漂移保護:
serviceVersion !== VERSION也算 issue(OPENCLAW_SERVICE_VERSION 檢查:146-150)。OPENCLAW_SERVICE_VERSION在服務 install 時寫入 plist/unit/XML 的環境變數,執行時再讀出來和當前 OpenClaw 版本對比——不一致說明服務是舊版本裝的,可能引用了已刪除的二進位路徑或舊的工作目錄佈局。 - launchd 不 kickstart -k:
installLaunchAgent(avoid kickstart -k:1018-1020)註解明確說bootstrap已經 RunAtLoad 了,不要再kickstart -k——在慢的 macOS 虛擬機上,kickstart -k會 SIGTERM 剛啟動的 gateway,把真正的監聽器啟動推過 setup 的健康檢查 deadline。 - launchd spawn throttle:
LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10(LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:8)是 launchd 自帶預設值,顯式寫入 plist 讓崩潰循環 (crash loop) 至少 10s 一次,而不是每秒拉起。LAUNCH_AGENT_UMASK_DECIMAL = 0o077顯式渲染成十進位63——launchd 的 plist 整數欄位不接受0o077寫法。 - systemd ExitStatus 78:
RestartPreventExitStatus=78(RestartPreventExitStatus:80)是 EX_CONFIG——設定錯誤時 gateway 進程退出碼 78,systemd 看到這個狀態不重啟。這避免了「設定檔壞了,systemd 一直重啟一直崩」的死循環。 - systemd linger:
readSystemdUserLingerStatus(readSystemdUserLingerStatus:28)讀loginctl show-user <user> -p Linger。headless 部署(沒有活躍 SSH 會話)下,systemd user instance 不啟動 gateway 就跑不起來,這時需要loginctl enable-linger <user>——daemon 模組讀狀態但不強制修改,留給管理員決定。 - schtasks UTF-16 BOM:
writeTaskXmlTempFile(writeTaskXmlTempFile:194)必須寫FFFEBOM,否則 Task Scheduler MMC 在某些 locale 下拒絕匯入。這是 Windows 工具鏈的相容性細節,程式碼裡有顯式註解。 - schtasks 升級 Change + XML:
updateExistingScheduledTask(updateExistingScheduledTask:1042)先/Change改/TR(task command),再/Create /F /XML覆蓋整個 XML——這保證舊版本安裝的 task 在升級時能繼承新的<DisallowStartIfOnBatteries>false</...>等欄位(#59299)。兩步都 best-effort,/Change失敗會保留舊設定,不會把 task 弄丟。
小結
Daemon 模組是 OpenClaw 對接三種系統服務管理器(launchd/systemd/schtasks)的薄包裝:它不跑 gateway,只負責寫 plist/unit/XML + 調系統 CLI 裝載/卸載/重啟。resolveGatewayService 按 process.platform 選 adapter,withFutureConfigGuard 保護寫操作不被版本錯位破壞,collectGatewayServiceStartRepairIssues 在 restart 前偵測版本漂移/路徑失效/臨時目錄三類問題。具體的 gateway 啟動邏輯看 Gateway 核心,容器化部署(沒有系統服務管理器)看 Docker 與 Fly 部署,profile 切換和服務環境變數見 openclaw.json。