Skip to content

Daemon: Systemdienst ​

源码版本v2026.6.11

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

  1. Boot-Autostart + Crash-Neustart: KeepAlive=true bei launchd, Restart=always bei systemd, LogonTrigger bei schtasks sind systemlevel Mechanismen, die beim Reboot oder unerwartetem Exit des OpenClaw-Prozesses automatisch hochfahren. nohup „ignoriert nur SIGHUP"; bei Reboot ist es wertlos.
  2. Einheitliche Konfiguration + Profile-Isolation: OPENCLAW_PROFILE erlaubt 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) oder ai.openclaw.<profile>; systemd und schtasks tragen entsprechend ein Suffix. Jedes Profile hat eigene plist/unit/XML, ohne sich gegenseitig zu stören.
  3. future-config-guard: withFutureConfigGuard (withFutureConfigGuard:336-362) ruft vor jedem Schreibvorgang (stage/install/uninstall/stop/restart) assertFutureConfigActionAllowed auf; 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".
  4. repair-Detektion: collectGatewayServiceStartRepairIssues (collectGatewayServiceStartRepairIssues:138-172) prüft vor restart die OPENCLAW_SERVICE_VERSION des 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 ​

Datenfluss ​

resolveGatewayService (resolveGatewayService:370) wählt den Adapter über die Plattform-Registry:

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 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:

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>
`;

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:

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: 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 Schreibmethoden throw createUnsupportedGatewayServiceError() werfen; readCommand liefert null, readRuntime liefert { status: "unknown", detail: ... }. FreeBSD/OpenBSD u. a., deren process.platform nicht 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, markiert collectGatewayServiceStartRepairIssues ein 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 !== VERSION gilt ebenfalls als issue (OPENCLAW_SERVICE_VERSION Prüfung:146-150). OPENCLAW_SERVICE_VERSION wird 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, dass bootstrap bereits RunAtLoad macht und nicht zusätzlich kickstart -k gerufen werden soll — auf langsamen macOS-VMs schickt kickstart -k dem 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 = 0o077 wird explizit als dezimal 63 gerendert — launchds plist-Integer-Feld akzeptiert die Schreibweise 0o077 nicht.
  • 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) liest loginctl 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 ist loginctl 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) muss FFFE-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 /Change das /TR (Task-Command) und überschreibt dann über /Create /F /XML das 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 /Change fehl, 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.