---
name: comfyui_ensure_server
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def comfyui_ensure_server(*, port: int = 8188, lowvram: bool | None = None, health_timeout: int = 60, comfyui_dir: str = '~/ComfyUI', unit_name: str = 'comfyui', runner=None) -> dict"
description: "Garantiza que ComfyUI corre como servicio systemd-user resiliente y sano. Genera/instala el unit systemd-user comfyui.service (ExecStart con el venv de ComfyUI + main.py --port, anadiendo --lowvram si lowvram=True o autodetectando GPUs <= 8 GB; Restart=always — NO on-failure; WantedBy=default.target), hace daemon-reload + enable + start, y comprueba la salud via GET /system_stats (2xx) con timeout. Idempotente: si el servicio ya esta gestionado por systemd, activo y respondiendo, no toca nada. Migracion limpia: si ComfyUI ya corre a mano (puerto ocupado por un proceso main.py que systemd NO gestiona), lo para con SIGTERM (nunca SIGKILL) y lo levanta via systemd. Solo stdlib (subprocess, urllib, os, signal, time, re). No lanza excepciones: devuelve un dict de estado."
tags: [comfyui, systemd, service, server, resilient, ml, healthcheck, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["os", "re", "signal", "subprocess", "time", "urllib.request"]
params:
  - name: port
    desc: "puerto HTTP del backend ComfyUI; tambien el que escribe en el unit (--port) y el que sondea el health check (default 8188)"
  - name: lowvram
    desc: "True/False fuerza/omite el flag --lowvram en ExecStart; None autodetecta por VRAM (GPUs con <= 8200 MiB -> True). Recomendado True en GPUs de 8 GB para modelos grandes (Flux, video)"
  - name: health_timeout
    desc: "segundos maximos sondeando GET /system_stats tras arrancar el servicio antes de declararlo no-sano (default 60)"
  - name: comfyui_dir
    desc: "raiz de la instalacion de ComfyUI; debe contener .venv/bin/python y main.py (default ~/ComfyUI, se expande y normaliza a absoluto)"
  - name: unit_name
    desc: "nombre del unit systemd-user (sin .service); el archivo va a ~/.config/systemd/user/<unit_name>.service (default 'comfyui')"
  - name: runner
    desc: "callable(cmd: list) -> CompletedProcess inyectable para tests; default ejecuta subprocess.run capturando salida"
output: "dict con ok (bool: servicio activo y sano), active (ActiveState del unit: active|inactive|failed), port, health (bool: /system_stats respondio 2xx), error (str|None), lowvram (bool aplicado), unit_path (ruta del .service escrito), migrated (bool: paro un ComfyUI a mano para migrar a systemd), reloaded (bool: hubo daemon-reload), idempotent (bool: ya estaba activo+sano y no se toco nada)"
tested: true
tests:
  - "_detect_lowvram aplica el umbral de 8 GB (8192/8200 -> True, 8201/24564/None -> False)"
  - "_render_unit incluye Restart=always, WantedBy=default.target y nunca on-failure; anade --lowvram solo cuando corresponde"
  - "error claro si falta el venv python en comfyui_dir"
  - "idempotente: si is-active=active y /system_stats sano, no llama a start"
  - "arranque fresco: escribe el unit, daemon-reload + enable + start y espera salud"
  - "lowvram=False omite el flag --lowvram en el unit escrito"
test_file_path: "python/functions/infra/comfyui_ensure_server_test.py"
file_path: "python/functions/infra/comfyui_ensure_server.py"
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from infra.comfyui_ensure_server import comfyui_ensure_server

# Deja ComfyUI corriendo como servicio systemd-user, sano y con --lowvram
# autodetectado en GPUs de 8 GB. Idempotente: relanzarla no rompe nada.
res = comfyui_ensure_server(port=8188, lowvram=True)
print(res)
# {'ok': True, 'active': 'active', 'port': 8188, 'health': True, 'error': None,
#  'lowvram': True, 'unit_path': '/home/enmanuel/.config/systemd/user/comfyui.service',
#  'migrated': True, 'reloaded': True, 'idempotent': False}
```

CLI directa (despacha por el venv del registry):

```bash
python/.venv/bin/python3 python/functions/infra/comfyui_ensure_server.py --port=8188 --lowvram
```

El usuario lo gestiona despues con systemd-user normal:

```bash
systemctl --user status comfyui      # estado + ultimos logs
systemctl --user restart comfyui     # reiniciar (la salud vuelve verde sola)
systemctl --user stop comfyui        # parar
systemctl --user disable --now comfyui   # revertir: para y deshabilita el arranque automatico
journalctl --user -u comfyui -n 50   # diagnosticar fallos de arranque
```

## Cuando usarla

Usala cuando necesites que ComfyUI este garantizado arriba y sano antes de
encolar workflows (txt2img, video, 3D), o para convertir el ComfyUI que hoy se
relanza a mano en un servicio que arranca solo al boot y se reinicia si cae
(gap del roadmap 0064). Es el primer paso del grupo `comfyui`: dejar el backend
disponible; despues vienen `comfyui_build_*_workflow` + `comfyui_submit_workflow`.

## Gotchas

- **systemd-user requiere linger** para sobrevivir al cierre de sesion / arrancar al boot: `loginctl enable-linger $USER`. Sin linger el unit solo vive mientras hay sesion activa. Si `enable` falla por esto, el dict lo dice en `error`.
- **Migracion limpia con SIGTERM, nunca SIGKILL**: si ComfyUI ya corre a mano ocupando el puerto, la funcion lo para con SIGTERM y espera a que libere el bind (hasta ~25 s) antes de arrancar el servicio. Si el puerto lo ocupa un proceso que NO es ComfyUI (cmdline sin `main.py`), NO lo toca y devuelve `error` — no arranca para no duplicar el bind.
- **Cambiar los flags del unit (p.ej. lowvram) NO reinicia un servicio ya sano**: la funcion reescribe el `.service` y hace daemon-reload, pero si el servicio ya esta active+healthy no lo reinicia para no interrumpir. Para aplicar flags nuevos: `systemctl --user restart comfyui`.
- **Carga la GPU al arrancar**: levantar ComfyUI reserva VRAM. En una GPU de 8 GB compartida, evita lanzarlo mientras otra tarea pesada usa la GPU.
- **Restart=always (no on-failure)**: un `systemctl --user stop` limpio es exit success; con `on-failure` el servicio reviviria solo tras crash. Para pararlo de verdad usa `stop` (no `restart`) o `disable --now`.
- El health check es `GET http://127.0.0.1:<port>/system_stats` y espera 2xx; solo loopback.
