Skip to content

Daemon:系统服务

源码版本v2026.6.11

职责

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 &?

  1. 开机自启 + 崩溃重启:launchdKeepAlive=true、systemd 的 Restart=always、schtasks 的 LogonTrigger 都是系统级机制,机器重启或 OpenClaw 进程意外退出时自动拉起。nohup 只能"忽略 SIGHUP",机器重启就废。
  2. 配置统一 + profile 隔离:OPENCLAW_PROFILE 可以让同一台机器跑多套 gateway(比如开发/生产分开),resolveGatewayLaunchAgentLabel(resolveGatewayLaunchAgentLabel:33-39)把 profile 编进 service label——ai.openclaw.gateway(default)或 ai.openclaw.<profile>,systemd 和 schtasks 也对应加后缀。每个 profile 独立 plist/unit/XML,互不干扰。
  3. future config guard:withFutureConfigGuard(withFutureConfigGuard:336-362)在 stage/install/uninstall/stop/restart 每个写操作前调 assertFutureConfigActionAllowed,如果检测到当前 OpenClaw 版本比服务配置写入时新(或反之,配置由未来版本生成),就拒绝改写——这是防止"降级启动把新格式的服务文件写坏"的安全网。
  4. repair 检测:collectGatewayServiceStartRepairIssues(collectGatewayServiceStartRepairIssues:138-172)在 restart 前检查已加载服务的 OPENCLAW_SERVICE_VERSION、program path 是否指向临时目录(isTemporaryProgramPath)、program 文件是否还存在(isMissingProgramPath)。版本漂移或路径失效会要求重装,而不是假装 restart 成功。

关键文件

数据流

resolveGatewayService(resolveGatewayService:370)用平台注册表选 adapter:

typescript
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)是最复杂的一个:

typescript
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。envXmlOPENCLAW_STATE_DIR/OPENCLAW_PROFILE 等环境变量嵌进 plist,这样不需要额外的 env-file。

systemd unit(buildSystemdUnit:68)用三段标准结构:

typescript
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=continueKillMode=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)必须写 FFFE BOM,否则 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 装载/卸载/重启。resolveGatewayServiceprocess.platform 选 adapter,withFutureConfigGuard 保护写操作不被版本错位破坏,collectGatewayServiceStartRepairIssues 在 restart 前检测版本漂移/路径失效/临时目录三类问题。具体的 gateway 启动逻辑看 Gateway 核心,容器化部署(没有系统服务管理器)看 Docker 与 Fly 部署,profile 切换和服务环境变量见 openclaw.json