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。