diff --git a/src/gpu_rent/provision.py b/src/gpu_rent/provision.py index c758907..da24e8c 100644 --- a/src/gpu_rent/provision.py +++ b/src/gpu_rent/provision.py @@ -287,41 +287,85 @@ import sys import time from pathlib import Path + +def comfy_on_disk() -> bool: + """True only after first Comfy install — not on empty data volume.""" + override = (os.environ.get("GPU_RENT_COMFY_PRESENT") or "").strip().lower() + if override in ("1", "true", "yes"): + return True + if override in ("0", "false", "no"): + return False + for cand in ( + Path("/mnt/swarm_data/dlbackend/ComfyUI/venv/bin/python"), + Path("/mnt/swarm_data/dlbackend/ComfyUI/main.py"), + Path("/opt/swarmui/dlbackend/ComfyUI/venv/bin/python"), + Path("/opt/swarmui/dlbackend/ComfyUI/main.py"), + ): + if cand.is_file(): + return True + backends = Path("/mnt/swarm_data/Data/Backends.fds") + try: + if backends.is_file() and "StartScript" in backends.read_text( + encoding="utf-8", errors="replace" + ): + return True + except OSError: + pass + return False + + +def sync_is_installed(text: str) -> tuple[str, str | None]: + """Set IsInstalled true only when Comfy exists; else clear leftover true.""" + if comfy_on_disk(): + if re.search(r"(?im)^\s*IsInstalled:\s*true\s*$", text): + return text, None + installed_block = ( + "IsInstalled: true\n" + f"InstallDate: {time.strftime('%Y-%m-%d')}\n" + "InstallVersion: gpu-rent\n" + ) + if re.search(r"(?im)^\s*IsInstalled:\s*", text): + text = re.sub(r"(?im)^(\s*IsInstalled:\s*).*$", r"\1true", text, count=1) + else: + text = installed_block + text + return text, "patched IsInstalled: true (Comfy on disk)" + if re.search(r"(?im)^\s*IsInstalled:\s*true\s*$", text): + text = re.sub(r"(?im)^(\s*IsInstalled:\s*).*$", r"\1false", text, count=1) + return text, "cleared IsInstalled: false (нет Comfy — первый InstallConfirmWS)" + return text, None + + p = Path(os.environ.get("GPU_RENT_SETTINGS_FDS") or "/mnt/swarm_data/Data/Settings.fds") fname = (os.environ.get("GPU_RENT_AUTOCOMPLETE_FILE") or "").strip() if not fname: print("no GPU_RENT_AUTOCOMPLETE_FILE", file=sys.stderr) raise SystemExit(1) -installed_block = ( +ac_block = ( + "DefaultUser:\n" + " AutoComplete:\n" + f" Source: {fname}\n" + " EscapeParens: true\n" +) +installed_prefix = ( "IsInstalled: true\n" f"InstallDate: {time.strftime('%Y-%m-%d')}\n" "InstallVersion: gpu-rent\n" ) -block = ( - installed_block - + "DefaultUser:\n" - " AutoComplete:\n" - f" Source: {fname}\n" - " EscapeParens: true\n" -) - if not p.is_file(): p.parent.mkdir(parents=True, exist_ok=True) - p.write_text(block, encoding="utf-8") - print(f"created Settings.fds IsInstalled+AutoComplete.Source={fname}") + prefix = installed_prefix if comfy_on_disk() else "" + p.write_text(prefix + ac_block, encoding="utf-8") + extra = "+IsInstalled" if prefix else "без IsInstalled (первый Comfy install)" + print(f"created Settings.fds AutoComplete.Source={fname} {extra}") raise SystemExit(0) text = p.read_text(encoding="utf-8", errors="replace") -# Never leave a Settings.fds that sends the UI to /Install. -if not re.search(r"(?im)^\s*IsInstalled:\s*true\s*$", text): - if re.search(r"(?im)^\s*IsInstalled:\s*", text): - text = re.sub(r"(?im)^(\s*IsInstalled:\s*).*$", r"\1true", text, count=1) - else: - text = installed_block + text +text, note = sync_is_installed(text) +if note: p.write_text(text, encoding="utf-8") - print("patched IsInstalled: true (was missing/false)") + print(note) text = p.read_text(encoding="utf-8", errors="replace") if re.search(rf"^\s*Source:\s*{re.escape(fname)}\s*$", text, re.M): @@ -356,14 +400,19 @@ if re.search(r"^DefaultUser:\s*$", text, re.M): p.write_text(new, encoding="utf-8") print(f"inserted AutoComplete under DefaultUser Source={fname}") else: - p.write_text(text.rstrip() + "\n\n" + block, encoding="utf-8") + p.write_text(text.rstrip() + "\n\n" + ac_block, encoding="utf-8") print(f"appended DefaultUser.AutoComplete Source={fname}") ''' _ENSURE_INSTALLED_PY = r''' #!/usr/bin/env python3 -"""Ensure Settings.fds has IsInstalled: true so UI skips /Install wizard.""" +"""Sync Settings.fds IsInstalled with whether Comfy actually exists. + +Warm re-up: backends/venv present → IsInstalled true (UI skips /Install). +First boot / empty dlbackend: do NOT set true — InstallConfirmWS refuses +with "Server is already installed!" and never clones ComfyUI. +""" from __future__ import annotations import os @@ -372,6 +421,31 @@ import sys import time from pathlib import Path + +def comfy_on_disk() -> bool: + override = (os.environ.get("GPU_RENT_COMFY_PRESENT") or "").strip().lower() + if override in ("1", "true", "yes"): + return True + if override in ("0", "false", "no"): + return False + Path("/mnt/swarm_data/dlbackend/ComfyUI/venv/bin/python"), + Path("/mnt/swarm_data/dlbackend/ComfyUI/main.py"), + Path("/opt/swarmui/dlbackend/ComfyUI/venv/bin/python"), + Path("/opt/swarmui/dlbackend/ComfyUI/main.py"), + ): + if cand.is_file(): + return True + backends = Path("/mnt/swarm_data/Data/Backends.fds") + try: + if backends.is_file() and "StartScript" in backends.read_text( + encoding="utf-8", errors="replace" + ): + return True + except OSError: + pass + return False + + paths = [] env = (os.environ.get("GPU_RENT_SETTINGS_FDS") or "").strip() if env: @@ -392,6 +466,7 @@ for p in paths: seen.add(key) uniq.append(p) +present = comfy_on_disk() changed = False for p in uniq: try: @@ -408,7 +483,10 @@ for p in uniq: if same and changed: continue if not p.is_file(): - # Only create on data volume path + # Don't invent IsInstalled=true on empty first boot. + if not present: + print(f"skip create {p}: Comfy ещё нет") + continue if "/mnt/swarm_data/" not in str(p): continue p.parent.mkdir(parents=True, exist_ok=True) @@ -424,6 +502,17 @@ for p in uniq: changed = True continue text = p.read_text(encoding="utf-8", errors="replace") + if not present: + if re.search(r"(?im)^\s*IsInstalled:\s*true\s*$", text): + text = re.sub( + r"(?im)^(\s*IsInstalled:\s*).*$", r"\1false", text, count=1 + ) + p.write_text(text, encoding="utf-8") + print(f"cleared {p}: IsInstalled false (нет Comfy — первый install)") + changed = True + else: + print(f"ok {p}: IsInstalled not true, Comfy отсутствует") + continue if re.search(r"(?im)^\s*IsInstalled:\s*true\s*$", text): print(f"ok {p}: IsInstalled true") continue @@ -475,7 +564,10 @@ def _merge_autocomplete_into_settings( def ensure_settings_is_installed(cfg: Config, host: str, log: Log) -> bool: - """Make sure Settings.fds has IsInstalled:true (UI /Install wizard). + """Sync Settings.fds IsInstalled with Comfy on disk. + + Warm: backends/venv exist → true (skip UI /Install). First boot / empty + dlbackend → leave false/missing so InstallConfirmWS can clone Comfy. Returns True if the file was created/patched (caller may need SwarmUI restart). """ @@ -936,7 +1028,7 @@ def provision_vm( ) if cfg.pull_output: pull_tree(cfg, host, f"{DATA}/Output", cfg.local_output_dir, log) - # Avoid /Install wizard when backends already exist but Settings lack flag. + # Warm: IsInstalled true if Comfy exists. First boot: keep false. if ensure_settings_is_installed(cfg, host, log): restart = True else: diff --git a/src/gpu_rent/remote/install_swarm_comfy.py b/src/gpu_rent/remote/install_swarm_comfy.py index 42a53ee..6cfe3b1 100644 --- a/src/gpu_rent/remote/install_swarm_comfy.py +++ b/src/gpu_rent/remote/install_swarm_comfy.py @@ -2,7 +2,11 @@ """Headless SwarmUI first-install: ComfyUI backend via InstallConfirmWS. Runs on the VM (stdlib only). Idempotent: skips when backends exist or -dlbackend/ComfyUI venv is already present and IsInstalled. +dlbackend/ComfyUI venv is already present. + +InstallConfirmWS refuses with ``Server is already installed!`` if +Settings.fds has IsInstalled:true — even when Comfy was never cloned. +First boot and leftover flags must clear that bit before the WS call. """ from __future__ import annotations @@ -778,6 +782,35 @@ def settings_is_installed() -> bool | None: return None +def clear_settings_installed_flag() -> bool: + """Set IsInstalled:false so InstallConfirmWS is allowed to run.""" + if not SETTINGS.is_file(): + return False + try: + text = SETTINGS.read_text(encoding="utf-8", errors="replace") + except OSError as exc: + print(f"WARN Settings read: {exc}", flush=True) + return False + if re.search(r"(?im)^\s*IsInstalled:\s*false\s*$", text): + return False + if re.search(r"(?im)^\s*IsInstalled:\s*", text): + text = re.sub(r"(?im)^(\s*IsInstalled:\s*).*$", r"\1false", text, count=1) + else: + text = "IsInstalled: false\n" + text + SETTINGS.write_text(text, encoding="utf-8") + print( + "patched Settings.fds IsInstalled=false (InstallConfirmWS)", + flush=True, + ) + return True + + +def prepare_first_install() -> None: + """Clear leftover IsInstalled and reload Swarm so the installer WS works.""" + if clear_settings_installed_flag(): + restart_swarmui_local() + + def ensure_settings_installed_flag() -> bool: """Write IsInstalled:true if missing — UI otherwise stays on /Install.""" SETTINGS.parent.mkdir(parents=True, exist_ok=True) @@ -1086,14 +1119,16 @@ def main() -> int: return 0 installed = settings_is_installed() - if installed is True and not comfy_venv_ok() and bstat == "empty": + # Leftover IsInstalled=true (autocomplete/warm patch, failed previous up) + # makes InstallConfirmWS return "Server is already installed!" and skip clone. + if installed is True and not comfy_venv_ok() and need_recover: print( - "WARN: Settings IsInstalled=true but backends empty and no Comfy venv. " - "Open SwarmUI → Server → Backends and add ComfyUI Self-Starting, " - "or delete Data/Settings.fds IsInstalled and re-run up." + "IsInstalled=true, backends empty, нет Comfy venv — " + "сбрасываю флаг и ставлю Comfy через InstallConfirmWS", + flush=True, ) - run_diagnostics() - return 1 + prepare_first_install() + installed = settings_is_installed() if ( installed is True @@ -1139,7 +1174,20 @@ def main() -> int: pass try: sid = get_session(time.time() + 120) - run_install(sid) + try: + run_install(sid) + except SystemExit as exc: + msg = str(exc).lower() + if "already installed" not in msg: + raise + print( + "InstallConfirmWS: Server is already installed — " + "сброс IsInstalled и повтор", + flush=True, + ) + prepare_first_install() + sid = get_session(time.time() + 120) + run_install(sid) finally: try: marker.unlink(missing_ok=True)