Daemon: Systemdienst
Verantwortung
resolveGatewayService (resolveGatewayService:370-375) ist der einheitliche Eintritt, über den OpenClaw das Gateway als „systemlevel residenten Dienst (daemon)" installiert: auf macOS ein LaunchAgent (von launchd verwaltet), auf Linux eine systemd-user-unit (von systemd --user verwaltet), auf Windows eine Scheduled Task (über schtasks registrierte Logon-Trigger-Aufgabe). Die drei Plattformen haben je einen Befehl, der in dasselbe GatewayService-Interface (GatewayService:75-87) gekapselt wird: stage/install/uninstall/stop/restart/isLoaded/readCommand/readRuntime; die CLI-Schicht muss Plattformunterschiede nicht kennen.
Es behandelt nicht „das Gateway betreiben" — das Gateway bleibt node openclaw.mjs gateway. Die Daemon-Schicht macht diesen Befehl in die Systemdienstmanifeste (plist/unit/XML) schreiben und den Systemdienstmanager nach Regel hochfahren, überwachen und automatisch neustarten lassen. Daher steckt im Daemon-Modul viel Code in plist-/unit-/XML-Templaterendering und launchctl-/systemctl-/schtasks-CLI-Aufrufen; der echte Gateway-Prozesslebenszyklus wird vom Systemdienstmanager kontrolliert.
Designmotivation
Warum nicht einfach nohup openclaw gateway &?
- Boot-Autostart + Crash-Neustart:
KeepAlive=truebei launchd,Restart=alwaysbei systemd,LogonTriggerbei schtasks sind systemlevel Mechanismen, die beim Reboot oder unerwartetem Exit des OpenClaw-Prozesses automatisch hochfahren.nohup„ignoriert nur SIGHUP"; bei Reboot ist es wertlos. - Einheitliche Konfiguration + Profile-Isolation:
OPENCLAW_PROFILEerlaubt derselben Maschine, mehrere Gateway-Sets zu betreiben (z. B. dev/prod getrennt);resolveGatewayLaunchAgentLabel(resolveGatewayLaunchAgentLabel:33-39) backt das Profile in das service-label —ai.openclaw.gateway(default) oderai.openclaw.<profile>; systemd und schtasks tragen entsprechend ein Suffix. Jedes Profile hat eigene plist/unit/XML, ohne sich gegenseitig zu stören. - future-config-guard:
withFutureConfigGuard(withFutureConfigGuard:336-362) ruft vor jedem Schreibvorgang (stage/install/uninstall/stop/restart)assertFutureConfigActionAllowedauf; wird erkannt, dass die aktuelle OpenClaw-Version neuer als die im Dienst eingetragene ist (oder vice versa, die Konfiguration wurde von einer zukünftigen Version erzeugt), wird der Schreib abgewiesen — Sicherheitsnetz gegen „Downgrade-Start beschädigt neue Dienstdateien". - repair-Detektion:
collectGatewayServiceStartRepairIssues(collectGatewayServiceStartRepairIssues:138-172) prüft vorrestartdieOPENCLAW_SERVICE_VERSIONdes geladenen Dienstes, ob der Programmpfad auf ein temporäres Verzeichnis zeigt (isTemporaryProgramPath) und ob die Programmdatei existiert (isMissingProgramPath). Versionsdrift oder Pfadverlust erfordert Neuinstallation statt vorgetäuschtem Restart.
Schlüsseldateien
GatewayService Typ:75-87— Einheitliches Interface mit 8 Methoden.GATEWAY_SERVICE_REGISTRY:294-334— Drei Adapterdarwin/linux/win32, je ein Plattform-Implementierungsmodul.withFutureConfigGuard:336-362— Wickelt stage/install/uninstall/stop/restart; vorabassertFutureConfigActionAllowed-Prüfung.resolveGatewayService:370-375— Wählt Adapter nachprocess.platform; für unbekannte PlattformencreateUnsupportedGatewayService(alle Methoden werfenunsupported).isTemporaryProgramPath:115-136— Erkennt/tmp//var/tmp/os.tmpdir()-Pfade; Dienste, die auf temporäre Verzeichnisse zeigen, werden als reparaturbedürftig markiert.collectGatewayServiceStartRepairIssues:138-172— Drei Klassen von start-repair-issues:OPENCLAW_SERVICE_VERSION-Drift + temporärer Pfad + fehlende Datei.service label Konstanten: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 Namen:33-60—resolveGatewayLaunchAgentLabel/resolveGatewaySystemdServiceName/resolveGatewayWindowsTaskNamemit Profile-Suffix-Regel.installLaunchAgent:1013-1030—writeLaunchAgentPlist+activateLaunchAgent; macOS-Installation.launchd Konstanten:5-13—LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS=10,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=20,LAUNCH_AGENT_UMASK_DECIMAL=0o077(nur Owner).buildLaunchAgentPlist:267-295— plist-Rendering:RunAtLoad=true/KeepAlive=true/ExitTimeOut=20/ThrottleInterval=10/Umask=0o077/StandardOutPath/StandardErrorPath.installSystemdService:1107-1131—writeSystemdUnit+activateSystemdService.buildSystemdUnit:52-101— Drei Abschnitte[Unit]/[Service]/[Install];Restart=always/RestartSec=5/TimeoutStopSec=30/OOMPolicy=continue/KillMode=control-group.readSystemdUserLingerStatus:28-50— Headless-Deployment brauchtloginctl enable-linger; hier wird der Status gelesen.buildScheduledTaskXml:139-192— Windows Task Scheduler XML;LogonTrigger/LeastPrivilege/DisallowStartIfOnBatteries=false.installScheduledTask:1313-1325—writeScheduledTaskScript+activateScheduledTask.writeTaskXmlTempFile:194-203—schtasks /XMLverlangt UTF-16 LE BOM; Nodeutf16le+ handgeschriebenesFFFE-BOM.GATEWAY_DIST_ENTRYPOINT_BASENAMES:5-22—index.js/index.mjs/entry.js/entry.mjs; wählt aus dem dist-Verzeichnis einen verfügbaren Eintritt.
Datenfluss
resolveGatewayService (resolveGatewayService:370) wählt den Adapter über die Plattform-Registry:
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 Implementierung */ },
win32: { label: "Scheduled Task", /* ...schtasks Implementierung */ },
};
export function resolveGatewayService(): GatewayService {
if (isSupportedGatewayServicePlatform(process.platform)) {
return withFutureConfigGuard(GATEWAY_SERVICE_REGISTRY[process.platform]);
}
return createUnsupportedGatewayService();
}withFutureConfigGuard wickelt alle Schreibmethoden in assertFutureConfigActionAllowed ein — ein einheitlicher „erst prüfen, dann schreiben"-Interceptor, der verhindert, eine alte OpenClaw-Version die von einer neuen Version erzeugten Dienstdateien überschreiben zu lassen.
Die plist-Vorlage für launchd (buildLaunchAgentPlist:267) ist die komplexeste:
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>
`;Vier Schlüsselfelder: RunAtLoad=true (beim Laden sofort starten), KeepAlive=true (jeder Exit startet neu), ExitTimeOut=20 (nach SIGTERM 20s warten, dann SIGKILL), ThrottleInterval=10 (launchds Crash-Loop-Schutz: mindestens 10s zwischen zwei Starts). Umask=0o077 macht alle vom Gateway erzeugten Dateien default owner-only — schützt vor group/other-Lesezugriff auf Secrets. envXml backt OPENCLAW_STATE_DIR/OPENCLAW_PROFILE u. a. in die plist ein, sodass keine zusätzliche env-Datei nötig ist.
Die systemd-unit (buildSystemdUnit:68) nutzt die dreiteilige Standardstruktur:
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: bei Konfigurationsfehlern nicht neustarten, Endlosloop vermeiden
"TimeoutStopSec=30",
"TimeoutStartSec=30",
"SuccessExitStatus=0 143", // 143 = SIGTERM als normaler Exit
"OOMPolicy=continue", // Kind-Prozess OOM-töten stoppt den Hauptdienst nicht
"KillMode=control-group", // Beim Neustart alle Kind-Prozesse mit KILL, keine ACP-worker-Ophan
workingDirLine,
...environmentFileLines,
...envLines,
"",
"[Install]",
"WantedBy=default.target",
"",
].filter((line) => line !== null).join("\n");OOMPolicy=continue und KillMode=control-group sind zwei Nicht-Defaults — ersteres stellt sicher, dass ein OOM im Kind-Prozess das Gateway nicht mitreißt; letzteres stellt sicher, dass beim Neustart des Gateways keine ACP-/agent-worker-Ophans bleiben. StartLimitBurst=5/StartLimitIntervalSec=60 ist systemds Crash-Loop-Schutz: nach 5 Crashes innerhalb 60s wird der Neustart gestoppt (menschliches Eingreifen nötig).
Die Windows-schtasks-XML (buildScheduledTaskXml:152) nutzt LogonTrigger (bei Nutzerlogin), LeastPrivilege (ohne Admin), DisallowStartIfOnBatteries=false (auch im Akkumodus laufen). Versteckte Falle: schtasks /XML verlangt UTF-16 LE BOM (writeTaskXmlTempFile:194); Nodes utf16le-Codierung fügt automatisch kein BOM hinzu, der Code baut manuell Buffer.from([0xff, 0xfe]) + body zusammen.
Grenzen und Fehler
- Nicht unterstützte Plattform-Delegation:
createUnsupportedGatewayService(createUnsupportedGatewayService:275-292) lässt alle Schreibmethodenthrow createUnsupportedGatewayServiceError()werfen;readCommandliefert null,readRuntimeliefert{ status: "unknown", detail: ... }. FreeBSD/OpenBSD u. a., derenprocess.platformnicht in der Tabelle steht, bekommen diesen delegierten Service. - Temporärpfad-Detektion:
TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"](TEMP_PROGRAM_ROOTS:115). Zeigt der Programmpfad des geladenen Dienstes auf eines dieser Verzeichnisse, markiertcollectGatewayServiceStartRepairIssuesein issue — temporäre Verzeichnisse können vom System aufgeräumt werden, und der Dienst findet plötzlich die Binärdatei nicht. In diesem Zustand ist Neuinstallation statt einfachem restart nötig. - Versionsdrift-Schutz:
serviceVersion !== VERSIONgilt ebenfalls als issue (OPENCLAW_SERVICE_VERSION Prüfung:146-150).OPENCLAW_SERVICE_VERSIONwird bei der Dienst-Installation als Umgebungsvariable in plist/unit/XML geschrieben und zur Laufzeit zurückgelesen und mit der aktuellen OpenClaw-Version verglichen — inkonsistent bedeutet, der Dienst wurde von einer alten Version installiert und referenziert möglicherweise gelöschte Binärpfade oder altes Arbeitsverzeichnis-Layout. - launchd kein kickstart -k:
installLaunchAgent(avoid kickstart -k:1018-1020) kommentiert explizit, dassbootstrapbereits RunAtLoad macht und nicht zusätzlichkickstart -kgerufen werden soll — auf langsamen macOS-VMs schicktkickstart -kdem gerade gestarteten Gateway SIGTERM und drückt den echten Listener-Start über die Setup-Health-Check-Deadline hinaus. - launchd spawn throttle:
LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10(LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:8) ist der launchd-Default; explizit in die plist geschrieben, damit eine Crash-Loop (crash loop) mindestens 10s pro Iteration braucht, statt jede Sekunde hochzufahren.LAUNCH_AGENT_UMASK_DECIMAL = 0o077wird explizit als dezimal63gerendert — launchds plist-Integer-Feld akzeptiert die Schreibweise0o077nicht. - systemd ExitStatus 78:
RestartPreventExitStatus=78(RestartPreventExitStatus:80) ist EX_CONFIG — bei Konfigurationsfehler beendet sich der Gateway-Prozess mit Exit-Code 78; systemd sieht diesen Status und startet nicht neu. Das verhindert die Endlos-Loop „Konfigurationsdatei kaputt, systemd restartet ständig und crasht ständig". - systemd linger:
readSystemdUserLingerStatus(readSystemdUserLingerStatus:28) liestloginctl show-user <user> -p Linger. In Headless-Deployments (ohne aktive SSH-Session) startet die systemd-user-instance nicht, und das Gateway läuft nicht — dann istloginctl enable-linger <user>nötig; das Daemon-Modul liest den Status, ändert aber nicht zwingend, überlässt das dem Administrator. - schtasks UTF-16 BOM:
writeTaskXmlTempFile(writeTaskXmlTempFile:194) mussFFFE-BOM schreiben, sonst lehnt Task Scheduler MMC in manchen Locales den Import ab. Das ist ein Kompatibilitätsdetail der Windows-Toolchain; der Code enthält einen expliziten Kommentar. - schtasks Upgrade Change + XML:
updateExistingScheduledTask(updateExistingScheduledTask:1042) ändert zuerst über/Changedas/TR(Task-Command) und überschreibt dann über/Create /F /XMLdas gesamte XML — das garantiert, dass ein von einer alten Version installierter Task beim Upgrade neue Felder wie<DisallowStartIfOnBatteries>false</...>übernimmt (#59299). Beide Schritte sind best-effort; schlägt/Changefehl, bleiben die alten Einstellungen erhalten, der Task geht nicht verloren.
Zusammenfassung
Das Daemon-Modul ist eine dünne Hülle von OpenClaw über drei Systemdienstmanagern (launchd/systemd/schtasks): es betreibt das Gateway nicht, sondern schreibt nur plist/unit/XML + ruft System-CLI zum Laden/Entladen/Neustarten. resolveGatewayService wählt Adapter nach process.platform; withFutureConfigGuard schützt Schreiboperationen vor Versionsversatz; collectGatewayServiceStartRepairIssues prüft vor restart die drei Problemklassen Versionsdrift/Pfadverlust/temporäres Verzeichnis. Die konkrete Gateway-Startlogik siehe Gateway-Kern; containerisiertes Deployment (ohne Systemdienstmanager) siehe Docker- und Fly-Deployment; Profile-Wechsel und Dienst-Umgebungsvariablen siehe openclaw.json.