Skip to content

Daemon: service système

源码版本v2026.6.11

Responsabilités

resolveGatewayService (resolveGatewayService:370-375) est l'entrée unifiée d'OpenClaw pour installer la gateway en « service système résident (daemon) »: LaunchAgent sur macOS (géré par launchd), unit systemd user sur Linux (géré par systemd --user), Scheduled Task sur Windows (tâche enregistrée via schtasks au login). Les commandes des trois plateformes sont encapsulées dans une même interface GatewayService (GatewayService:75-87): stage/install/uninstall/stop/restart/isLoaded/readCommand/readRuntime; la couche CLI ne se soucie pas des différences de plateforme.

Ce qu'elle gère n'est pas « faire tourner la gateway » — la gateway elle-même reste node openclaw.mjs gateway. La couche Daemon fait l'écriture de cette commande dans le manifeste du service système (plist/unit/XML), puis laisse le gestionnaire de services système la lancer, surveiller, redémarrer automatiquement selon ses règles. Donc le module daemon comporte beaucoup de code pour le rendu de templates plist/unit/XML et les appels CLI launchctl/systemctl/schtasks; le cycle de vie réel du process gateway est contrôlé par le gestionnaire de services système.

Motivation de conception

Pourquoi ne pas simplement nohup openclaw gateway &?

  1. Démarrage auto + restart sur crash: KeepAlive=true de launchd, Restart=always de systemd, LogonTrigger de schtasks sont des mécanismes système; au redémarrage de la machine ou à la sortie inattendue du process OpenClaw, ils relancent automatiquement. nohup ne fait qu'« ignorer SIGHUP », inutile au redémarrage machine.
  2. Config unifiée + isolation profile: OPENCLAW_PROFILE permet à une même machine de faire tourner plusieurs gateways (par exemple dev/prod séparés), resolveGatewayLaunchAgentLabel (resolveGatewayLaunchAgentLabel:33-39) encode le profile dans le label du service — ai.openclaw.gateway (default) ou ai.openclaw.<profile>, systemd et schtasks ajoutent aussi le suffixe. Chaque profile a son propre plist/unit/XML, sans interférence.
  3. future config guard: withFutureConfigGuard (withFutureConfigGuard:336-362) appelle assertFutureConfigActionAllowed avant chaque opération d'écriture stage/install/uninstall/stop/restart; si la version OpenClaw courante est plus récente que celle qui a écrit la config du service (ou inversement, la config a été générée par une version future), refuse la réécriture — filet de sécurité contre « un démarrage dégradé qui écrase un fichier de service au format neuf ».
  4. Détection repair: collectGatewayServiceStartRepairIssues (collectGatewayServiceStartRepairIssues:138-172) avant restart vérifie le OPENCLAW_SERVICE_VERSION du service chargé, si le program path pointe vers un répertoire temporaire (isTemporaryProgramPath), si le fichier program existe encore (isMissingProgramPath). Une dérive de version ou un chemin périmé exige une réinstallation, plutôt que de prétendre un restart réussi.

Fichiers clés

Flux de données

resolveGatewayService (resolveGatewayService:370) sélectionne l'adapter via le registry plateforme:

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 impl */ },
  win32:  { label: "Scheduled Task", /* ...schtasks impl */ },
};

export function resolveGatewayService(): GatewayService {
  if (isSupportedGatewayServicePlatform(process.platform)) {
    return withFutureConfigGuard(GATEWAY_SERVICE_REGISTRY[process.platform]);
  }
  return createUnsupportedGatewayService();
}

withFutureConfigGuard enveloppe toutes les méthodes d'écriture d'un assertFutureConfigActionAllowed — interception unifiée « vérifier avant d'écrire » pour éviter qu'une vieille version OpenClaw ne réécrive un fichier de service au format généré par une version plus récente.

Le template plist de launchd (buildLaunchAgentPlist:267) est le plus complexe:

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

Quatre clés critiques: RunAtLoad=true (démarre immédiatement au chargement), KeepAlive=true (relance à toute sortie), ExitTimeOut=20 (attend 20s après SIGTERM avant SIGKILL), ThrottleInterval=10 (protection launchd contre crash loop, au moins 10s entre deux démarrages). Umask=0o077 fait que tous les fichiers créés par la gateway sont owner-only par défaut — pour éviter que group/other ne lisent les secrets. envXml embarque OPENCLAW_STATE_DIR/OPENCLAW_PROFILE etc. dans le plist, sans fichier env séparé.

L'unit systemd (buildSystemdUnit:68) suit la structure standard trois sections:

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: ne pas restart sur erreur de config, anti-boucle
  "TimeoutStopSec=30",
  "TimeoutStartSec=30",
  "SuccessExitStatus=0 143",       // 143 = terminaison par SIGTERM compte comme sortie normale
  "OOMPolicy=continue",            // sous-process OOM tué ne stoppe pas le service principal
  "KillMode=control-group",        // au restart, KILL tous les sous-process, anti orphelins ACP worker
  workingDirLine,
  ...environmentFileLines,
  ...envLines,
  "",
  "[Install]",
  "WantedBy=default.target",
  "",
].filter((line) => line !== null).join("\n");

OOMPolicy=continue et KillMode=control-group sont deux valeurs non-default — le premier garantit qu'un sous-process OOM ne fait pas tomber toute la gateway, le second garantit qu'au restart gateway pas d'orphelins ACP/agent worker. StartLimitBurst=5/StartLimitIntervalSec=60 est la protection systemd contre crash loop, après 5 crashes consécutifs en 60s, systemd arrête de relancer (intervention humaine requise).

Le XML schtasks Windows (buildScheduledTaskXml:152) utilise LogonTrigger (déclenchement au login utilisateur), LeastPrivilege (pas admin), DisallowStartIfOnBatteries=false (tourne aussi sur batterie). Piège caché: schtasks /XML requiert UTF-16 LE BOM (writeTaskXmlTempFile:194); l'encodage utf16le de Node n'ajoute pas automatiquement de BOM, le code concatène explicitement Buffer.from([0xff, 0xfe]) + body.

Limites et modes d'échec

  • Plateformes non supportées dégradées: createUnsupportedGatewayService (createUnsupportedGatewayService:275-292) fait que toutes les méthodes d'écriture throw createUnsupportedGatewayServiceError(), readCommand retourne null, readRuntime retourne { status: "unknown", detail: ... }. FreeBSD/OpenBSD etc. dont process.platform n'est pas dans la table obtiennent ce service dégradé.
  • Détection de chemin temporaire: TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"] (TEMP_PROGRAM_ROOTS:115). Si le program path d'un service chargé pointe vers l'un de ces répertoires, collectGatewayServiceStartRepairIssues lève un issue — le répertoire temporaire peut être nettoyé par le système, et le service perd soudainement son exécutable. Cet état exige une réinstall plutôt qu'un simple restart.
  • Protection dérive version: serviceVersion !== VERSION compte aussi comme issue (OPENCLAW_SERVICE_VERSION check:146-150). OPENCLAW_SERVICE_VERSION est écrite dans l'environnement du plist/unit/XML à l'install du service, relue au runtime et comparée à la version OpenClaw courante — une incohérence indique que le service a été installé par une ancienne version, qui peut référencer un chemin binaire supprimé ou une vieille layout de working directory.
  • launchd sans kickstart -k: installLaunchAgent (avoid kickstart -k:1018-1020) commente explicitement que bootstrap fait déjà RunAtLoad, ne pas appeler kickstart -k — sur macOS VM lent, kickstart -k SIGTERM la gateway qui vient de démarrer, repoussant le démarrage du vrai listener au-delà du deadline de health check du setup.
  • launchd spawn throttle: LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10 (LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:8) est la valeur par défaut launchd, écrite explicitement dans le plist pour qu'un crash loop soit espacé d'au moins 10s, plutôt que d'être relancé chaque seconde. LAUNCH_AGENT_UMASK_DECIMAL = 0o077 est rendu explicitement en décimal 63 — le champ integer du plist launchd n'accepte pas 0o077.
  • systemd ExitStatus 78: RestartPreventExitStatus=78 (RestartPreventExitStatus:80) est EX_CONFIG — quand la gateway quitte avec le code 78 pour erreur de config, systemd ne restart pas. Cela évite la boucle « config cassée, systemd restart crash à l'infini ».
  • systemd linger: readSystemdUserLingerStatus (readSystemdUserLingerStatus:28) lit loginctl show-user <user> -p Linger. En déploiement headless (sans session SSH active), l'instance systemd user ne démarre pas et la gateway ne se lance pas; il faut loginctl enable-linger <user> — le module daemon lit l'état sans modifier, laisse l'admin décider.
  • schtasks UTF-16 BOM: writeTaskXmlTempFile (writeTaskXmlTempFile:194) doit écrire le BOM FFFE, sinon Task Scheduler MMC refuse l'import dans certaines locales. Détail de compat outil Windows, commenté explicitement dans le code.
  • schtasks upgrade Change + XML: updateExistingScheduledTask (updateExistingScheduledTask:1042) fait d'abord /Change pour modifier /TR (task command), puis /Create /F /XML pour écraser tout le XML — cela garantit qu'au passage de version une task installée par ancienne version puisse hériter des nouveaux champs comme <DisallowStartIfOnBatteries>false</...> (#59299). Les deux étapes sont best-effort, /Change échoué conserve les vieux réglages, ne perd pas la task.
  • cap_drop et no-new-privileges en conteneur: le mode daemon physique n'offre pas ce niveau d'isolation — cf. Docker et Fly déploiement pour la défense en profondeur des conteneurs.

Résumé

Le module Daemon est une fine enveloppe d'OpenClaw sur les trois gestionnaires de services système (launchd/systemd/schtasks): il ne fait pas tourner la gateway, il ne fait qu'écrire plist/unit/XML + appeler les CLI système pour charger/décharger/restart. resolveGatewayService choisit l'adapter selon process.platform, withFutureConfigGuard protège les écritures contre les décalages de version, collectGatewayServiceStartRepairIssues détecte avant restart trois types de problèmes: dérive version, chemin périmé, répertoire temporaire. La logique de démarrage de la gateway elle-même dans cœur de la Gateway, le déploiement conteneurisé (sans gestionnaire de services système) dans déploiement Docker et Fly, le switch profile et les variables d'environnement du service dans openclaw.json.