Skip to content

Daemon: servicio del sistema

源码版本v2026.6.11

Responsabilidad

resolveGatewayService (resolveGatewayService:370-375) es la entrada unificada con la que OpenClaw instala el gateway como «servicio del sistema (daemon) residente»: en macOS es un LaunchAgent (gestionado por launchd), en Linux es una systemd user unit (gestionada por systemd --user), en Windows es una Scheduled Task (tarea registrada con schtasks disparada por login). Los comandos de cada plataforma se envuelven en la misma interfaz GatewayService (GatewayService:75-87): stage/install/uninstall/stop/restart/isLoaded/readCommand/readRuntime, la capa CLI no se preocupa por las diferencias de plataforma.

Lo que hace no es «correr el gateway» — el gateway en sí sigue siendo node openclaw.mjs gateway. La capa Daemon hace escribir ese comando en el manifiesto del servicio del sistema (plist/unit/XML) y dejar que el gestor de servicios del sistema lo levante, monitorice y reinicie automáticamente. Por eso la mayor parte del código del módulo daemon está en el rendering de plantillas plist/unit/XML y en la invocación de los CLI launchctl/systemctl/schtasks; el ciclo de vida real del proceso gateway lo controla el gestor de servicios del sistema.

Motivación de diseño

¿Por qué no directamente nohup openclaw gateway &?

  1. Auto-arranque en boot + reinicio tras crash: KeepAlive=true de launchd, Restart=always de systemd, LogonTrigger de schtasks son mecanismos a nivel sistema que levantan el gateway automáticamente al reiniciar la máquina o si el proceso cae inesperadamente. nohup solo «ignora SIGHUP», en un reinicio de la máquina se pierde.
  2. Configuración unificada + aislamiento de profile: OPENCLAW_PROFILE permite correr varias instancias de gateway en la misma máquina (por ejemplo, dev/prod separadas), y resolveGatewayLaunchAgentLabel (resolveGatewayLaunchAgentLabel:33-39) compone el profile en el service label — ai.openclaw.gateway (default) o ai.openclaw.<profile>; systemd y schtasks añaden el sufijo correspondiente. Cada profile tiene su propio plist/unit/XML, sin interferencias.
  3. future config guard: withFutureConfigGuard (withFutureConfigGuard:336-362) invoca assertFutureConfigActionAllowed antes de cada operación de escritura (stage/install/uninstall/stop/restart); si detecta que la versión actual de OpenClaw es más nueva que la que escribió el servicio (o viceversa, config generada por versión futura), rechaza la reescritura — una red de seguridad para prevenir «un downgrade arranque y corrompa los archivos de servicio del nuevo formato».
  4. Detección de repair: collectGatewayServiceStartRepairIssues (collectGatewayServiceStartRepairIssues:138-172) chequea antes de restart el OPENCLAW_SERVICE_VERSION del servicio cargado, si el program path apunta a un directorio temporal (isTemporaryProgramPath) y si el archivo program todavía existe (isMissingProgramPath). Drift de versión o path inválido exige reinstalar en lugar de fingir un restart exitoso.

Archivos clave

Flujo de datos

resolveGatewayService (resolveGatewayService:370) elige adapter desde el registro de plataforma:

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 envuelve todos los métodos de escritura con assertFutureConfigActionAllowed, una intercepción unificada de «chequea antes de escribir» — evita que una versión vieja de OpenClaw sobrescriba los archivos de servicio generados por una versión nueva y corrompa el formato.

La plantilla plist de launchd (buildLaunchAgentPlist:267) es la más compleja:

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

Cuatro keys clave: RunAtLoad=true (arranca en carga), KeepAlive=true (reinicia ante cualquier salida), ExitTimeOut=20 (tras SIGTERM espera 20s antes de SIGKILL), ThrottleInterval=10 (protección de crash loop builtin de launchd, mínimo 10s entre arranques). Umask=0o077 hace que todos los archivos que el gateway crea sean owner-only por defecto — evita que group/other lean secrets. envXml embebe variables como OPENCLAW_STATE_DIR/OPENCLAW_PROFILE dentro del plist, así no hace falta un env-file separado.

La unit de systemd (buildSystemdUnit:68) usa tres secciones estándar:

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: no reiniciar en error de config, evita loop
  "TimeoutStopSec=30",
  "TimeoutStartSec=30",
  "SuccessExitStatus=0 143",       // 143 = SIGTERM cuenta como salida normal
  "OOMPolicy=continue",            // si un subproceso cae por OOM, el servicio principal sigue
  "KillMode=control-group",        // al reiniciar, mata todos los subprocesos juntos, evita huérfanos ACP worker
  workingDirLine,
  ...environmentFileLines,
  ...envLines,
  "",
  "[Install]",
  "WantedBy=default.target",
  "",
].filter((line) => line !== null).join("\n");

OOMPolicy=continue y KillMode=control-group son dos valores no default — el primero garantiza que un OOM de un subproceso no arrastre todo el gateway, el segundo garantiza que al reiniciar el gateway no queden workers ACP/agent huérfanos. StartLimitBurst=5/StartLimitIntervalSec=60 es la protección de crash loop builtin de systemd: si cae 5 veces consecutivas en 60s se detiene el reinicio (requiere intervención manual).

El XML de Windows schtasks (buildScheduledTaskXml:152) usa LogonTrigger (dispara al iniciar sesión), LeastPrivilege (sin admin), DisallowStartIfOnBatteries=false (corre también en portátil con batería). Un pozo oculto: schtasks /XML exige UTF-16 LE BOM (writeTaskXmlTempFile:194), la codificación utf16le de Node no añade BOM automáticamente, el código monta manualmente Buffer.from([0xff, 0xfe]) + body.

Límites y fallos

  • Plataformas no soportadas degradan: todos los métodos de escritura de createUnsupportedGatewayService (createUnsupportedGatewayService:275-292) hacen throw createUnsupportedGatewayServiceError(), readCommand devuelve null, readRuntime devuelve { status: "unknown", detail: ... }. Sistemas con process.platform fuera de la tabla (FreeBSD/OpenBSD, etc.) reciben este servicio degradado.
  • Detección de path temporal: TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"] (TEMP_PROGRAM_ROOTS:115). Si el program path del servicio cargado apunta a uno de estos, collectGatewayServiceStartRepairIssues marca issue — el directorio temporal puede ser limpiado por el sistema, el servicio de repente no encuentra el binario. Este estado exige reinstalar, no un simple restart.
  • Protección contra drift de versión: serviceVersion !== VERSION también es issue (OPENCLAW_SERVICE_VERSION check:146-150). OPENCLAW_SERVICE_VERSION se escribe como variable de entorno en el plist/unit/XML al instalar; en runtime se lee y se compara con la versión actual de OpenClaw — una inconsistencia indica que el servicio lo instaló una versión vieja, que podría referenciar binarios eliminados o un layout viejo de directorios.
  • launchd sin kickstart -k: installLaunchAgent (avoid kickstart -k:1018-1020) comenta explícitamente que bootstrap ya tiene RunAtLoad, no hace falta kickstart -k — en macOS VMs lentos, kickstart -k manda SIGTERM al gateway recién arrancado, empujando el listener real de arranque más allá del deadline del health check.
  • launchd spawn throttle: LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10 (LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:8) es el default builtin de launchd, se escribe explícitamente en el plist para que un crash loop (crash loop) sea como mínimo cada 10s y no cada segundo. LAUNCH_AGENT_UMASK_DECIMAL = 0o077 se renderiza explícitamente como decimal 63 — los campos enteros de plist de launchd no aceptan la sintaxis 0o077.
  • systemd ExitStatus 78: RestartPreventExitStatus=78 (RestartPreventExitStatus:80) es EX_CONFIG — cuando la config está rota, el proceso del gateway sale con 78 y systemd no reinicia. Evita el loop de «config rota, systemd reinicia sin parar y cae».
  • systemd linger: readSystemdUserLingerStatus (readSystemdUserLingerStatus:28) lee loginctl show-user <user> -p Linger. En despliegues headless (sin sesión SSH activa), la instancia systemd user no arranca y el gateway no corre, hace falta loginctl enable-linger <user> — el módulo daemon lee el estado pero no lo modifica, lo deja a decisión del administrador.
  • schtasks UTF-16 BOM: writeTaskXmlTempFile (writeTaskXmlTempFile:194) debe escribir el BOM FFFE, si no, Task Scheduler MMC rechaza la importación en algunos locales. Es un detalle de compatibilidad de la toolchain de Windows, con comentario explícito en el código.
  • schtasks upgrade Change + XML: updateExistingScheduledTask (updateExistingScheduledTask:1042) primero hace /Change para modificar /TR (task command), luego /Create /F /XML para sobrescribir todo el XML — garantiza que una task instalada por una versión vieja al actualizar herede los nuevos campos como <DisallowStartIfOnBatteries>false</...> (#59299). Ambos pasos son best-effort, si /Change falla se conservan los settings viejos y no se pierde la task.

Resumen

El módulo Daemon es el wrapper delgado de OpenClaw frente a tres gestores de servicio del sistema (launchd/systemd/schtasks): no corre el gateway, solo escribe plist/unit/XML + invoca el CLI del sistema para cargar/descargar/reiniciar. resolveGatewayService elige adapter por process.platform, withFutureConfigGuard protege las operaciones de escritura contra desalineación de versiones, collectGatewayServiceStartRepairIssues detecta tres tipos de issues (drift de versión, path inválido, directorio temporal) antes de un restart. La lógica concreta de arranque del gateway en Núcleo del gateway; despliegue en contenedores (sin gestor de servicios del sistema) en Despliegue con Docker y Fly; cambio de profile y variables de entorno del servicio en openclaw.json.