feat(infra): auto-commit con 3 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,326 @@
|
||||
"""Garantiza que ComfyUI corre como servicio systemd-user resiliente y sano.
|
||||
|
||||
Funcion impura: instala/actualiza el unit systemd-user `comfyui.service`, lo
|
||||
habilita y arranca, y comprueba la salud del backend HTTP. 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 y lo levanta via
|
||||
systemd, para que a partir de ese momento se reinicie solo (Restart=always).
|
||||
|
||||
Solo depende de la stdlib (subprocess, urllib, os, signal, time, re). No lanza
|
||||
excepciones: siempre devuelve un dict de estado.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _default_runner(cmd):
|
||||
"""Ejecuta un comando capturando salida. Inyectable para tests."""
|
||||
return subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
|
||||
def _detect_lowvram(vram_mib):
|
||||
"""Decide si conviene --lowvram segun la VRAM total en MiB.
|
||||
|
||||
GPUs con <= 8200 MiB (tarjetas de 8 GB) ganan estabilidad con --lowvram para
|
||||
modelos grandes (Flux, video). Si no hay dato de VRAM (None), NO asume
|
||||
lowvram: devuelve False para no penalizar GPUs grandes sin necesidad.
|
||||
"""
|
||||
return vram_mib is not None and vram_mib <= 8200
|
||||
|
||||
|
||||
def _query_vram_mib(runner):
|
||||
"""Lee la VRAM total (MiB) de la primera GPU via nvidia-smi. None si falla."""
|
||||
try:
|
||||
r = runner(
|
||||
[
|
||||
"nvidia-smi",
|
||||
"--query-gpu=memory.total",
|
||||
"--format=csv,noheader,nounits",
|
||||
]
|
||||
)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
return int(r.stdout.strip().splitlines()[0].strip())
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _render_unit(python_bin, main_py, working_dir, port, lowvram, description):
|
||||
"""Construye el texto del unit systemd-user. Pura (sin I/O)."""
|
||||
exec_start = f"{python_bin} {main_py} --port {port}"
|
||||
if lowvram:
|
||||
exec_start += " --lowvram"
|
||||
return (
|
||||
"[Unit]\n"
|
||||
f"Description={description}\n"
|
||||
"After=network-online.target\n"
|
||||
"Wants=network-online.target\n"
|
||||
"\n"
|
||||
"[Service]\n"
|
||||
"Type=simple\n"
|
||||
f"WorkingDirectory={working_dir}\n"
|
||||
f"ExecStart={exec_start}\n"
|
||||
# Restart=always (NO on-failure): un SIGTERM limpio es exit success y
|
||||
# con on-failure el servicio no reviviria. Ver .claude/rules/function_tags.md.
|
||||
"Restart=always\n"
|
||||
"RestartSec=5\n"
|
||||
"\n"
|
||||
"[Install]\n"
|
||||
"WantedBy=default.target\n"
|
||||
)
|
||||
|
||||
|
||||
def _health(port, path="/system_stats", timeout=3):
|
||||
"""True si GET http://127.0.0.1:<port><path> responde 2xx."""
|
||||
url = f"http://127.0.0.1:{port}{path}"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
||||
return 200 <= resp.status < 300
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _wait_health(port, timeout, interval=2.0):
|
||||
"""Sondea la salud hasta que responda 2xx o se agote el timeout."""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if _health(port):
|
||||
return True
|
||||
time.sleep(interval)
|
||||
return _health(port)
|
||||
|
||||
|
||||
def _systemctl(runner, *args):
|
||||
return runner(["systemctl", "--user", *args])
|
||||
|
||||
|
||||
def _unit_active_state(runner, unit_name):
|
||||
"""Devuelve el ActiveState del unit: active|inactive|failed|... o '' si no existe."""
|
||||
r = _systemctl(runner, "is-active", unit_name)
|
||||
return (r.stdout or r.stderr or "").strip()
|
||||
|
||||
|
||||
def _pid_listening_on_port(port, runner):
|
||||
"""PID del proceso que escucha en 127.0.0.1:<port>, o None. Via `ss`."""
|
||||
try:
|
||||
r = runner(["ss", "-ltnpH", f"sport = :{port}"])
|
||||
if r.returncode == 0:
|
||||
m = re.search(r"pid=(\d+)", r.stdout or "")
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _is_comfy_process(pid):
|
||||
"""True si la cmdline del PID contiene 'main.py' (proceso ComfyUI a mano)."""
|
||||
try:
|
||||
with open(f"/proc/{pid}/cmdline", "rb") as f:
|
||||
cmd = f.read().replace(b"\0", b" ").decode(errors="replace")
|
||||
return "main.py" in cmd
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _terminate_manual(pid, port, runner, wait_s=25.0):
|
||||
"""SIGTERM al proceso a mano y espera a que libere el puerto. No usa SIGKILL."""
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
deadline = time.monotonic() + wait_s
|
||||
while time.monotonic() < deadline:
|
||||
if _pid_listening_on_port(port, runner) is None:
|
||||
return True
|
||||
time.sleep(1.0)
|
||||
# Reintento suave de SIGTERM antes de rendirse (nunca SIGKILL: no destructivo).
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(3.0)
|
||||
return _pid_listening_on_port(port, runner) is None
|
||||
|
||||
|
||||
def comfyui_ensure_server(
|
||||
*,
|
||||
port=8188,
|
||||
lowvram=None,
|
||||
health_timeout=60,
|
||||
comfyui_dir="~/ComfyUI",
|
||||
unit_name="comfyui",
|
||||
runner=None,
|
||||
):
|
||||
"""Garantiza ComfyUI corriendo y sano como servicio systemd-user.
|
||||
|
||||
Args:
|
||||
port: puerto HTTP del backend ComfyUI (default 8188).
|
||||
lowvram: True/False fuerza el flag --lowvram; None autodetecta por VRAM
|
||||
(GPUs <= 8 GB -> True).
|
||||
health_timeout: segundos maximos esperando a que /system_stats responda
|
||||
tras arrancar el servicio.
|
||||
comfyui_dir: raiz de la instalacion de ComfyUI (con .venv/ y main.py).
|
||||
unit_name: nombre del unit systemd-user (sin .service).
|
||||
runner: callable(cmd:list)->CompletedProcess inyectable para tests.
|
||||
|
||||
Returns:
|
||||
dict con: ok, active (ActiveState), port, health (bool), error (str|None),
|
||||
lowvram (bool), unit_path, migrated (bool), reloaded (bool),
|
||||
idempotent (bool).
|
||||
"""
|
||||
runner = runner or _default_runner
|
||||
result = {
|
||||
"ok": False,
|
||||
"active": None,
|
||||
"port": port,
|
||||
"health": False,
|
||||
"error": None,
|
||||
"lowvram": None,
|
||||
"unit_path": None,
|
||||
"migrated": False,
|
||||
"reloaded": False,
|
||||
"idempotent": False,
|
||||
}
|
||||
|
||||
comfyui_dir = os.path.abspath(os.path.expanduser(comfyui_dir))
|
||||
python_bin = os.path.join(comfyui_dir, ".venv", "bin", "python")
|
||||
main_py = os.path.join(comfyui_dir, "main.py")
|
||||
if not os.path.exists(python_bin):
|
||||
result["error"] = f"venv python no encontrado: {python_bin}"
|
||||
return result
|
||||
if not os.path.exists(main_py):
|
||||
result["error"] = f"main.py no encontrado: {main_py}"
|
||||
return result
|
||||
|
||||
# 1. Resolver lowvram (autodetect por VRAM si es None).
|
||||
lv = lowvram if lowvram is not None else _detect_lowvram(_query_vram_mib(runner))
|
||||
result["lowvram"] = bool(lv)
|
||||
|
||||
# 2. Renderizar e instalar el unit (solo reescribe si cambio el contenido).
|
||||
content = _render_unit(
|
||||
python_bin, main_py, comfyui_dir, port, lv,
|
||||
"ComfyUI (Stable Diffusion / Flux backend) gestionado por el registry",
|
||||
)
|
||||
unit_dir = os.path.expanduser("~/.config/systemd/user")
|
||||
try:
|
||||
os.makedirs(unit_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
result["error"] = f"no se pudo crear {unit_dir}: {e}"
|
||||
return result
|
||||
unit_path = os.path.join(unit_dir, f"{unit_name}.service")
|
||||
result["unit_path"] = unit_path
|
||||
|
||||
existing = None
|
||||
if os.path.exists(unit_path):
|
||||
try:
|
||||
with open(unit_path, "r") as f:
|
||||
existing = f.read()
|
||||
except Exception:
|
||||
existing = None
|
||||
changed = existing != content
|
||||
if changed:
|
||||
tmp = unit_path + ".tmp"
|
||||
try:
|
||||
with open(tmp, "w") as f:
|
||||
f.write(content)
|
||||
os.replace(tmp, unit_path)
|
||||
except Exception as e:
|
||||
result["error"] = f"no se pudo escribir el unit: {e}"
|
||||
return result
|
||||
rl = _systemctl(runner, "daemon-reload")
|
||||
result["reloaded"] = rl.returncode == 0
|
||||
if rl.returncode != 0:
|
||||
result["error"] = f"daemon-reload fallo: {(rl.stderr or '').strip()}"
|
||||
return result
|
||||
|
||||
# 3. Habilitar (idempotente; el linger del usuario ya debe estar activo).
|
||||
en = _systemctl(runner, "enable", unit_name)
|
||||
if en.returncode != 0:
|
||||
result["error"] = (
|
||||
f"systemctl --user enable {unit_name} fallo: "
|
||||
f"{(en.stderr or '').strip()}. "
|
||||
"Si es por falta de linger: `loginctl enable-linger $USER`."
|
||||
)
|
||||
return result
|
||||
|
||||
# 4. Estado actual: salud HTTP + si systemd ya lo gestiona.
|
||||
active_state = _unit_active_state(runner, unit_name)
|
||||
health_now = _health(port)
|
||||
|
||||
if health_now and active_state == "active":
|
||||
# Ya gestionado por systemd y sano -> idempotente, no tocar.
|
||||
result["ok"] = True
|
||||
result["health"] = True
|
||||
result["active"] = "active"
|
||||
result["idempotent"] = not changed
|
||||
return result
|
||||
|
||||
if health_now and active_state != "active":
|
||||
# Proceso a mano ocupa el puerto y systemd NO lo gestiona -> migrar limpio.
|
||||
pid = _pid_listening_on_port(port, runner)
|
||||
if pid and _is_comfy_process(pid):
|
||||
if not _terminate_manual(pid, port, runner):
|
||||
result["error"] = (
|
||||
f"no se pudo liberar el puerto {port} (PID {pid}) con SIGTERM; "
|
||||
"no arranco el servicio para no duplicar el bind."
|
||||
)
|
||||
return result
|
||||
result["migrated"] = True
|
||||
elif pid:
|
||||
result["error"] = (
|
||||
f"puerto {port} ocupado por PID {pid} que no parece ComfyUI; "
|
||||
"no lo toco ni arranco el servicio."
|
||||
)
|
||||
return result
|
||||
# Si pid es None pero health_now True: race raro; seguimos a start.
|
||||
|
||||
# 5. Arrancar via systemd y esperar salud.
|
||||
st = _systemctl(runner, "start", unit_name)
|
||||
if st.returncode != 0:
|
||||
result["active"] = _unit_active_state(runner, unit_name)
|
||||
result["error"] = (
|
||||
f"systemctl --user start {unit_name} fallo: "
|
||||
f"{(st.stderr or '').strip()}. Diagnostica con "
|
||||
f"`journalctl --user -u {unit_name} -n 50`."
|
||||
)
|
||||
return result
|
||||
|
||||
healthy = _wait_health(port, health_timeout)
|
||||
result["active"] = _unit_active_state(runner, unit_name)
|
||||
result["health"] = healthy
|
||||
result["ok"] = healthy
|
||||
if not healthy:
|
||||
result["error"] = (
|
||||
f"el unit arranco pero /system_stats no respondio 2xx en "
|
||||
f"{health_timeout}s. Revisa `journalctl --user -u {unit_name} -n 50`."
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
kwargs = {}
|
||||
for arg in sys.argv[1:]:
|
||||
if arg.startswith("--port="):
|
||||
kwargs["port"] = int(arg.split("=", 1)[1])
|
||||
elif arg == "--lowvram":
|
||||
kwargs["lowvram"] = True
|
||||
elif arg == "--no-lowvram":
|
||||
kwargs["lowvram"] = False
|
||||
elif arg.startswith("--health-timeout="):
|
||||
kwargs["health_timeout"] = int(arg.split("=", 1)[1])
|
||||
elif arg.startswith("--comfyui-dir="):
|
||||
kwargs["comfyui_dir"] = arg.split("=", 1)[1]
|
||||
print(json.dumps(comfyui_ensure_server(**kwargs), indent=2))
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Tests para comfyui_ensure_server.
|
||||
|
||||
Los tests no tocan systemd ni la red reales: inyectan un runner falso que
|
||||
registra los comandos systemctl y se mockea el health check.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from . import comfyui_ensure_server as mod
|
||||
from .comfyui_ensure_server import (
|
||||
_detect_lowvram,
|
||||
_render_unit,
|
||||
comfyui_ensure_server,
|
||||
)
|
||||
|
||||
|
||||
class FakeRunner:
|
||||
"""Runner inyectable: respuestas programables por prefijo de comando."""
|
||||
|
||||
def __init__(self, active_state="inactive"):
|
||||
self.calls = []
|
||||
self.active_state = active_state
|
||||
|
||||
def __call__(self, cmd):
|
||||
self.calls.append(list(cmd))
|
||||
# nvidia-smi VRAM
|
||||
if cmd[:1] == ["nvidia-smi"]:
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="8192\n", stderr="")
|
||||
if cmd[:2] == ["systemctl", "--user"]:
|
||||
sub = cmd[2] if len(cmd) > 2 else ""
|
||||
if sub == "is-active":
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 0, stdout=self.active_state + "\n", stderr=""
|
||||
)
|
||||
# daemon-reload, enable, start -> exito
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||
if cmd[:1] == ["ss"]:
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||
|
||||
def ran(self, *needle):
|
||||
return any(call[: len(needle)] == list(needle) for call in self.calls)
|
||||
|
||||
|
||||
def _fake_comfy_dir(tmp_path):
|
||||
"""Crea un comfyui_dir falso con .venv/bin/python y main.py."""
|
||||
d = tmp_path / "ComfyUI"
|
||||
(d / ".venv" / "bin").mkdir(parents=True)
|
||||
(d / ".venv" / "bin" / "python").write_text("#!/bin/sh\n")
|
||||
(d / "main.py").write_text("# fake\n")
|
||||
return d
|
||||
|
||||
|
||||
# --- helpers puros ---
|
||||
|
||||
def test_detect_lowvram_umbral_8gb():
|
||||
assert _detect_lowvram(8192) is True
|
||||
assert _detect_lowvram(8200) is True
|
||||
assert _detect_lowvram(8201) is False
|
||||
assert _detect_lowvram(24564) is False
|
||||
assert _detect_lowvram(None) is False
|
||||
|
||||
|
||||
def test_render_unit_restart_always_y_wantedby():
|
||||
unit = _render_unit(
|
||||
"/x/.venv/bin/python", "/x/main.py", "/x", 8188, True, "ComfyUI test"
|
||||
)
|
||||
assert "Restart=always" in unit
|
||||
assert "on-failure" not in unit # regla function_tags
|
||||
assert "WantedBy=default.target" in unit
|
||||
assert "ExecStart=/x/.venv/bin/python /x/main.py --port 8188 --lowvram" in unit
|
||||
assert "WorkingDirectory=/x" in unit
|
||||
|
||||
|
||||
def test_render_unit_sin_lowvram():
|
||||
unit = _render_unit(
|
||||
"/x/.venv/bin/python", "/x/main.py", "/x", 9000, False, "ComfyUI test"
|
||||
)
|
||||
assert "--lowvram" not in unit
|
||||
assert "--port 9000" in unit
|
||||
|
||||
|
||||
# --- orquestacion (runner falso + health mockeado) ---
|
||||
|
||||
def test_error_si_falta_venv(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
res = comfyui_ensure_server(
|
||||
comfyui_dir=str(tmp_path / "no_existe"), runner=FakeRunner()
|
||||
)
|
||||
assert res["ok"] is False
|
||||
assert "venv python no encontrado" in res["error"]
|
||||
|
||||
|
||||
def test_idempotente_si_ya_activo_y_sano(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
monkeypatch.setattr(mod, "_health", lambda *a, **k: True)
|
||||
d = _fake_comfy_dir(tmp_path)
|
||||
runner = FakeRunner(active_state="active")
|
||||
|
||||
# 1a llamada: instala el unit por primera vez (changed -> idempotent False),
|
||||
# pero como ya esta active+sano NO debe arrancar nada.
|
||||
res1 = comfyui_ensure_server(comfyui_dir=str(d), runner=runner)
|
||||
assert res1["ok"] is True
|
||||
assert res1["health"] is True
|
||||
assert res1["active"] == "active"
|
||||
assert res1["idempotent"] is False # escribio el unit por primera vez
|
||||
assert not runner.ran("systemctl", "--user", "start", "comfyui")
|
||||
|
||||
# 2a llamada: el unit ya existe identico -> no toca nada -> idempotent True.
|
||||
runner2 = FakeRunner(active_state="active")
|
||||
res2 = comfyui_ensure_server(comfyui_dir=str(d), runner=runner2)
|
||||
assert res2["ok"] is True
|
||||
assert res2["idempotent"] is True
|
||||
assert res2["reloaded"] is False # no reescribio el unit
|
||||
assert not runner2.ran("systemctl", "--user", "start", "comfyui")
|
||||
|
||||
|
||||
def test_arranque_fresco_escribe_unit_y_arranca(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
# health: False antes de arrancar, True despues
|
||||
estados = iter([False, True, True, True])
|
||||
monkeypatch.setattr(mod, "_health", lambda *a, **k: next(estados, True))
|
||||
d = _fake_comfy_dir(tmp_path)
|
||||
runner = FakeRunner(active_state="inactive")
|
||||
res = comfyui_ensure_server(comfyui_dir=str(d), runner=runner, health_timeout=5)
|
||||
assert res["ok"] is True
|
||||
assert res["health"] is True
|
||||
assert res["lowvram"] is True # nvidia-smi falso devuelve 8192
|
||||
assert runner.ran("systemctl", "--user", "daemon-reload")
|
||||
assert runner.ran("systemctl", "--user", "enable", "comfyui")
|
||||
assert runner.ran("systemctl", "--user", "start", "comfyui")
|
||||
# el unit quedo escrito
|
||||
unit_path = os.path.join(
|
||||
str(tmp_path), ".config", "systemd", "user", "comfyui.service"
|
||||
)
|
||||
assert os.path.exists(unit_path)
|
||||
with open(unit_path) as f:
|
||||
assert "Restart=always" in f.read()
|
||||
|
||||
|
||||
def test_lowvram_forzado_false_omite_flag(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
estados = iter([False, True])
|
||||
monkeypatch.setattr(mod, "_health", lambda *a, **k: next(estados, True))
|
||||
d = _fake_comfy_dir(tmp_path)
|
||||
runner = FakeRunner(active_state="inactive")
|
||||
res = comfyui_ensure_server(
|
||||
comfyui_dir=str(d), runner=runner, lowvram=False, health_timeout=5
|
||||
)
|
||||
assert res["lowvram"] is False
|
||||
unit_path = os.path.join(
|
||||
str(tmp_path), ".config", "systemd", "user", "comfyui.service"
|
||||
)
|
||||
with open(unit_path) as f:
|
||||
assert "--lowvram" not in f.read()
|
||||
Reference in New Issue
Block a user