Daemon:システムサービス
責務
resolveGatewayService(resolveGatewayService:370-375) は OpenClaw が gateway を「システム級常駐サービス (daemon)」としてインストールする統一入口です:macOS では LaunchAgent(launchd 管理)、Linux では systemd user unit(systemd --user 管理)、Windows では Scheduled Task(schtasks 登録のログオントリガタスク)。3 プラットフォームの各コマンドは同じ 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 をサービスラベルに組み込みます——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の 3 セット adapter,各々 1 つのプラットフォーム実装モジュールに対応。withFutureConfigGuard:336-362— stage/install/uninstall/stop/restart を包み,前置assertFutureConfigActionAllowedチェック。resolveGatewayService:370-375—process.platformで adapter を選び,未知プラットフォームはcreateUnsupportedGatewayService(全メソッドがunsupportedを throw)を返す。isTemporaryProgramPath:115-136—/tmp//var/tmp/os.tmpdir()パス検出,サービスが一時ディレクトリを指すと修復要とマーク。collectGatewayServiceStartRepairIssues:138-172—OPENCLAW_SERVICE_VERSIONドリフト + 一時パス + ファイル欠落,3 種の 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]の 3 段,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>
`;4 つの重要キー:RunAtLoad=true(ロード時即起動)、KeepAlive=true(任意の終了で再起動)、ExitTimeOut=20(SIGTERM 後 20s 待ってから SIGKILL)、ThrottleInterval=10(launchd 内蔵の崩壊ループ保護,2 回起動間は最低 10s)。Umask=0o077 で gateway が作成するすべてのファイルをデフォルト owner-only にします——group/other が secrets を読むのを回避します。envXml は OPENCLAW_STATE_DIR/OPENCLAW_PROFILE などの環境変数を plist に埋め込み,追加の env-file を不要にします。
systemd unit(buildSystemdUnit:68)は標準 3 段構造を使います:
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 kill されても主サービスは止まらない
"KillMode=control-group", // 再起動時にすべての子プロセスを一緒に KILL,孤立 ACP worker を残さない
workingDirLine,
...environmentFileLines,
...envLines,
"",
"[Install]",
"WantedBy=default.target",
"",
].filter((line) => line !== null).join("\n");OOMPolicy=continue と KillMode=control-group は 2 つの非デフォルト値——前者は子プロセスの OOM が gateway 全体を巻き込まないことを保証,後者は gateway 再起動時に ACP/agent worker の孤立を残さないことを保証します。StartLimitBurst=5/StartLimitIntervalSec=60 は systemd 内蔵の崩壊ループ保護で,60s 内に連続 5 回崩壊したら再起動を停止します(人手介入が必要)。
Windows schtasks の XML(buildScheduledTaskXml:152)は LogonTrigger(ユーザログオン時にトリガー)、LeastPrivilege(管理者権限なし)、DisallowStartIfOnBatteries=false(ノート PC バッテリモードでも実行)を使います。1 つの隠し罠: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が起動直後の gateway に SIGTERM を送り,本当のリスナー起動を setup の健全性チェックデッドライン越しに押し込んでしまいます。 - launchd spawn throttle:
LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10(LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:8)は launchd のデフォルト値ですが,明示的に plist に書き込むことで崩壊ループ (crash loop) を最低 10s に 1 回に制限し,毎秒引き上げを回避します。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 が 3 種のシステムサービスマネージャ(launchd/systemd/schtasks)に接続する薄いラッパーです:gateway を走らせず,plist/unit/XML を書き + システム CLI でロード/アンロード/再起動を呼ぶだけです。resolveGatewayService が process.platform で adapter を選び,withFutureConfigGuard が書き込み操作をバージョン不一致から保護し,collectGatewayServiceStartRepairIssues が restart 前にバージョンドリフト/パス失効/一時ディレクトリの 3 種問題を検出します。具体的な gateway 起動ロジックは Gateway コア,コンテナ化デプロイ(システムサービスマネージャがない)は Docker と Fly デプロイ,profile 切替とサービス環境変数は openclaw.json を参照。