CreaRack-SL

Decisión: Flags de CreateProcess en Windows para el watchdog post-swap (DETACHED_PROCESS vs CREATE_NO_WINDOW)

Contexto

El watchdog (proceso hijo desacoplado) que monitorea el auto-update del Agente debe:

  1. Lanzarse desde el Agente que está a punto de reemplazarse.
  2. Sobrevivir al padre (que hace os._exit(0) inmediatamente).
  3. Ejecutar un script PowerShell que escribe logs y realiza healthchecks (GET /health).

En Windows, subprocess.Popen(creationflags=...) requiere una combinación de flags que garantice:

  • Que el proceso hijo no hereda la consola del padre (debe estar desacoplado).
  • Que el child obtiene handles válidos para stdin/stdout/stderr (PowerShell necesita escribir).

Alternativas consideradas

❌ Opción 1: DETACHED_PROCESS | CREATE_NO_WINDOW (hasta 2.13.2)

creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW

Razonamiento original:

  • DETACHED_PROCESS garantiza desacoplamiento del padre.
  • CREATE_NEW_PROCESS_GROUP crea un grupo de proceso nuevo (no asociado a la sesión del padre).
  • CREATE_NO_WINDOW oculta la ventana de consola.

Problema descubierto (incidentes 03-07 y 05-07-2026):

  • DETACHED_PROCESS y CREATE_NO_WINDOW son modos mutuamente excluyentes en CreateProcessW.
  • Si se especifican ambos, Windows prioriza DETACHED_PROCESS e ignora CREATE_NO_WINDOW.
  • El proceso se crea sin consola válida y sin handles de entrada/salida.
  • PowerShell.exe muere en su host initialization (antes de cargar el script) al intentar validar la consola.
  • Síntoma: PID registrado en logs, pero el script jamás escribe su primera línea.

✅ Opción 2: CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW + stdin/stdout/stderr=DEVNULL (2.13.3+)

creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,

Razonamiento revisado:

  • CREATE_NEW_PROCESS_GROUP (sin DETACHED_PROCESS) crea un grupo nuevo y desacopla del padre correctamente.
  • CREATE_NO_WINDOW abre una consola privada invisible (válida internamente).
  • Proporcionar handles explícitos (DEVNULL) garantiza que PowerShell obtiene I/O válidos.

Ventajas:

  • ✅ PowerShell inicializa correctamente.
  • ✅ Los child processes sobreviven al padre en Windows (comportamiento por defecto).
  • ✅ El script se ejecuta completamente, incluso si el padre hace os._exit(0) al instante.
  • ✅ Verificado empíricamente en laboratorio (reproducción de ambos escenarios).

Por qué no usar explícitamente DETACHED_PROCESS:

  • No es necesario: CREATE_NEW_PROCESS_GROUP sin padre ya desacopla.
  • Causa conflicto semántico con CREATE_NO_WINDOW.
  • Históricamente, DETACHED_PROCESS se usaba en aplicaciones de servicio Windows (no guarded); aquí no aplica.

Decisión

Adoptar Opción 2: Reemplazar DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW por CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW + handles explícitos en DEVNULL.

Reasoning

  1. Correctness: Elimina el conflicto de flags que mataba silenciosamente el watchdog.
  2. Simplicity: Menos flags = menos superficies de ambigüedad en Windows API.
  3. Empirical verification: Probado en laboratorio; el nuevo setup permite que el script corra completamente.
  4. Backwards compatibility: No afecta el resto del código (solo el lanzamiento del watchdog).

Implementation

  • Archivo: terminal/agent/core/updater.py, función _launch_swap_watchdog(), líneas ~249–269.
  • Cambio: 4 líneas de creationflags reemplazadas por 4 líneas + 3 parámetros de handles.
  • Test de regresión: tests/agent/test_agent_updater_trace.py::test_watchdog_launch_sin_detached_process verifica ausencia de DETACHED_PROCESS en el source.

Impacto

En el Agent

  • Versión afectada: 2.13.3+ (el updater que lanza el watchdog con flags nuevos).
  • Alcance: Solo la función _launch_swap_watchdog() (privada, interna).
  • Efectos secundarios: Ninguno; el resto de los subprocesos del Agente no cambian.

En la flota

  • Desde 2.13.3 en adelante, el watchdog funciona de verdad.
  • Swaps previos (hasta 2.13.2) siguen siendo vulnerables a fallos silenciosos si el AV bloquea el arranque.
  • Mitigación: El cambio es prospectivo; el primer swap a 2.13.3 puede fallar silenciosamente (watchdog viejo), pero todos los posteriores usan el vigilante corregido.

Véase también

  • [[incident—20260705—watchdog-auto-update-mudo]]
  • [[feature—terminal—watchdog-recovery-fix-v2-13-3]]
  • [[entity—terminal—service—updater-swap-watchdog]]
  • [[concept—infra—auto-update-resilience]]