Offer ComfyUI-Manager install first and allow cancelling stale node installs.
This commit is contained in:
@@ -3,13 +3,22 @@ them through ComfyUI-Manager's queue (same path as the Manager UI)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any, Optional
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from . import config
|
||||
from .builder import CORE
|
||||
|
||||
MANAGER_FOLDER = "ComfyUI-Manager"
|
||||
MANAGER_GIT_URL = "https://github.com/ComfyUI-Manager/ComfyUI-Manager"
|
||||
MANAGER_TITLE = "ComfyUI-Manager"
|
||||
STALE_PROGRESS_SEC = 90
|
||||
POLL_INTERVAL_SEC = 1.5
|
||||
|
||||
# Curated mapping: pack id (ComfyUI-Manager / registry) -> node classes used by the
|
||||
# fixed core graph. FunPack and comfy-core primitives are excluded.
|
||||
PIPELINE_PACKS: list[dict[str, Any]] = [
|
||||
@@ -45,6 +54,27 @@ _ALL_MAPPED = frozenset().union(*(p["classes"] for p in PIPELINE_PACKS))
|
||||
_install_jobs: dict[str, dict] = {}
|
||||
|
||||
|
||||
def custom_nodes_dir() -> Path:
|
||||
"""ComfyUI ``custom_nodes`` directory (FunPack lives one level below)."""
|
||||
try:
|
||||
import folder_paths
|
||||
paths = folder_paths.get_folder_paths("custom_nodes")
|
||||
if paths:
|
||||
return Path(paths[0])
|
||||
except Exception:
|
||||
pass
|
||||
return Path(__file__).resolve().parents[2].parent
|
||||
|
||||
|
||||
def manager_dir() -> Path:
|
||||
return custom_nodes_dir() / MANAGER_FOLDER
|
||||
|
||||
|
||||
def manager_dir_on_disk() -> bool:
|
||||
d = manager_dir()
|
||||
return d.is_dir() and ((d / ".git").exists() or (d / "__init__.py").exists())
|
||||
|
||||
|
||||
def required_core_classes() -> list[str]:
|
||||
return sorted(set(CORE.values()))
|
||||
|
||||
@@ -74,15 +104,28 @@ def unmapped_missing_classes(object_info: dict | None) -> list[str]:
|
||||
return sorted(missing - _ALL_MAPPED)
|
||||
|
||||
|
||||
def status_payload(object_info: dict | None, *, manager_available: bool) -> dict:
|
||||
def status_payload(
|
||||
object_info: dict | None,
|
||||
*,
|
||||
manager_available: bool,
|
||||
manager_on_disk: bool | None = None,
|
||||
) -> dict:
|
||||
packs = missing_packs(object_info)
|
||||
unmapped = unmapped_missing_classes(object_info)
|
||||
on_disk = manager_dir_on_disk() if manager_on_disk is None else manager_on_disk
|
||||
needs_setup = bool(packs)
|
||||
needs_manager = needs_setup and not manager_available
|
||||
return {
|
||||
"manager_available": manager_available,
|
||||
"manager_on_disk": on_disk,
|
||||
"needs_manager_install": needs_manager and not on_disk,
|
||||
"needs_manager_restart": needs_manager and on_disk,
|
||||
"missing_classes": missing_core_classes(object_info),
|
||||
"missing_packs": packs,
|
||||
"unmapped_classes": unmapped,
|
||||
"needs_install": bool(packs),
|
||||
"needs_install": needs_setup,
|
||||
"needs_setup": needs_setup,
|
||||
"manager_git_url": MANAGER_GIT_URL,
|
||||
"manual_urls": [
|
||||
{"id": p["id"], "title": p["title"], "url": p["git_urls"][0]}
|
||||
for p in PIPELINE_PACKS
|
||||
@@ -194,6 +237,11 @@ async def _start_manager_queue() -> None:
|
||||
return
|
||||
|
||||
|
||||
async def _reset_manager_queue() -> None:
|
||||
for path in ("/manager/queue/reset", "/api/manager/queue/reset"):
|
||||
await _manager_request("POST", path)
|
||||
|
||||
|
||||
async def _manager_queue_status() -> dict:
|
||||
for path in ("/manager/queue/status", "/api/manager/queue/status"):
|
||||
status, body = await _manager_request("GET", path)
|
||||
@@ -209,25 +257,84 @@ async def _manager_queue_status() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def install_manager_sync() -> tuple[bool, str]:
|
||||
"""Clone ComfyUI-Manager into custom_nodes (no Manager HTTP required)."""
|
||||
import shutil
|
||||
|
||||
if manager_dir_on_disk():
|
||||
return True, "already_installed"
|
||||
if not shutil.which("git"):
|
||||
return False, "git not found on PATH."
|
||||
root = custom_nodes_dir()
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
target = manager_dir()
|
||||
proc = subprocess.run(
|
||||
["git", "clone", MANAGER_GIT_URL, str(target)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or "git clone failed").strip()
|
||||
return False, err
|
||||
req = target / "requirements.txt"
|
||||
if req.is_file():
|
||||
pip = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "-r", str(req)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
if pip.returncode != 0:
|
||||
err = (pip.stderr or pip.stdout or "pip install failed").strip()
|
||||
return False, f"ComfyUI-Manager cloned but pip install failed: {err}"
|
||||
return True, ""
|
||||
|
||||
|
||||
def _new_job(*, kind: str, packs: list[dict]) -> dict:
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
job = {
|
||||
"job_id": job_id,
|
||||
"kind": kind,
|
||||
"state": "queued",
|
||||
"packs": packs,
|
||||
"total": max(1, len(packs)),
|
||||
"done": 0,
|
||||
"current_title": packs[0]["title"] if packs else "",
|
||||
"error": "",
|
||||
"stale": False,
|
||||
"cancel_requested": False,
|
||||
"started_at": time.time(),
|
||||
"last_progress_at": time.time(),
|
||||
}
|
||||
_install_jobs[job_id] = job
|
||||
return job
|
||||
|
||||
|
||||
def create_manager_install_job() -> dict:
|
||||
return _new_job(
|
||||
kind="manager",
|
||||
packs=[{"id": MANAGER_FOLDER, "title": MANAGER_TITLE}],
|
||||
)
|
||||
|
||||
|
||||
def create_install_job(pack_ids: list[str]) -> dict:
|
||||
packs = []
|
||||
for pid in pack_ids:
|
||||
spec = _PACK_BY_ID.get(pid)
|
||||
if spec:
|
||||
packs.append({"id": pid, "title": spec["title"]})
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
job = {
|
||||
"job_id": job_id,
|
||||
"state": "queued",
|
||||
"packs": packs,
|
||||
"total": len(packs),
|
||||
"done": 0,
|
||||
"current_title": packs[0]["title"] if packs else "",
|
||||
"error": "",
|
||||
"started_at": time.time(),
|
||||
}
|
||||
_install_jobs[job_id] = job
|
||||
return job
|
||||
return _new_job(kind="packs", packs=packs)
|
||||
|
||||
|
||||
def cancel_install_job(job_id: str) -> bool:
|
||||
job = _install_jobs.get(job_id)
|
||||
if not job:
|
||||
return False
|
||||
if job["state"] in ("restarting", "cancelled", "done"):
|
||||
return False
|
||||
job["cancel_requested"] = True
|
||||
return True
|
||||
|
||||
|
||||
def get_install_job(job_id: str) -> Optional[dict]:
|
||||
@@ -236,22 +343,119 @@ def get_install_job(job_id: str) -> Optional[dict]:
|
||||
return None
|
||||
return {
|
||||
"job_id": job["job_id"],
|
||||
"kind": job.get("kind") or "packs",
|
||||
"state": job["state"],
|
||||
"packs": job["packs"],
|
||||
"total": job["total"],
|
||||
"done": job["done"],
|
||||
"current_title": job.get("current_title") or "",
|
||||
"error": job.get("error") or "",
|
||||
"stale": bool(job.get("stale")),
|
||||
}
|
||||
|
||||
|
||||
async def run_install_job(job_id: str, restart_fn) -> None:
|
||||
def _job_snapshot(job: dict) -> dict:
|
||||
return get_install_job(job["job_id"]) or {}
|
||||
|
||||
|
||||
async def _sleep_poll(job: dict) -> bool:
|
||||
"""Return True if cancel was requested during the wait."""
|
||||
steps = max(1, int(POLL_INTERVAL_SEC / 0.25))
|
||||
for _ in range(steps):
|
||||
if job.get("cancel_requested"):
|
||||
return True
|
||||
await asyncio.sleep(0.25)
|
||||
return bool(job.get("cancel_requested"))
|
||||
|
||||
|
||||
async def _finish_cancelled(job: dict) -> None:
|
||||
job["state"] = "cancelled"
|
||||
job["current_title"] = ""
|
||||
if job.get("kind") == "packs":
|
||||
await _reset_manager_queue()
|
||||
|
||||
|
||||
def _touch_progress(job: dict, done: int) -> None:
|
||||
now = time.time()
|
||||
if done != job.get("_last_done"):
|
||||
job["_last_done"] = done
|
||||
job["last_progress_at"] = now
|
||||
job["stale"] = False
|
||||
elif now - float(job.get("last_progress_at") or now) >= STALE_PROGRESS_SEC:
|
||||
job["stale"] = True
|
||||
|
||||
|
||||
async def _poll_manager_queue(job: dict, restart_fn: Callable[[], None]) -> None:
|
||||
deadline = time.time() + 1800
|
||||
while time.time() < deadline:
|
||||
if job.get("cancel_requested"):
|
||||
await _finish_cancelled(job)
|
||||
return
|
||||
st = await _manager_queue_status()
|
||||
done = int(st.get("done_count") or 0)
|
||||
total = int(st.get("total_count") or 0) or job["total"]
|
||||
processing = bool(st.get("is_processing"))
|
||||
job["total"] = max(job["total"], total)
|
||||
job["done"] = min(done, job["total"])
|
||||
_touch_progress(job, done)
|
||||
if job["packs"]:
|
||||
idx = min(max(done, 0), len(job["packs"]) - 1)
|
||||
job["current_title"] = job["packs"][idx]["title"]
|
||||
if job["total"] > 0 and done >= job["total"] and not processing:
|
||||
break
|
||||
if await _sleep_poll(job):
|
||||
await _finish_cancelled(job)
|
||||
return
|
||||
else:
|
||||
job["state"] = "error"
|
||||
job["error"] = "Install timed out. Cancel and try again, or check ComfyUI-Manager in the main ComfyUI window."
|
||||
job["stale"] = True
|
||||
return
|
||||
job["done"] = job["total"]
|
||||
job["state"] = "restarting"
|
||||
job["current_title"] = ""
|
||||
await asyncio.sleep(0.7)
|
||||
restart_fn()
|
||||
|
||||
|
||||
async def run_manager_install_job(job_id: str, restart_fn: Callable[[], None]) -> None:
|
||||
job = _install_jobs.get(job_id)
|
||||
if not job:
|
||||
return
|
||||
job["state"] = "installing"
|
||||
job["current_title"] = MANAGER_TITLE
|
||||
try:
|
||||
if job.get("cancel_requested"):
|
||||
await _finish_cancelled(job)
|
||||
return
|
||||
ok, err = await asyncio.to_thread(install_manager_sync)
|
||||
if job.get("cancel_requested"):
|
||||
await _finish_cancelled(job)
|
||||
return
|
||||
if not ok:
|
||||
job["state"] = "error"
|
||||
job["error"] = err or "ComfyUI-Manager install failed."
|
||||
return
|
||||
job["done"] = 1
|
||||
job["state"] = "restarting"
|
||||
job["current_title"] = ""
|
||||
await asyncio.sleep(0.7)
|
||||
restart_fn()
|
||||
except Exception as e: # noqa: BLE001
|
||||
job["state"] = "error"
|
||||
job["error"] = str(e) or "ComfyUI-Manager install failed."
|
||||
|
||||
|
||||
async def run_install_job(job_id: str, restart_fn: Callable[[], None]) -> None:
|
||||
job = _install_jobs.get(job_id)
|
||||
if not job:
|
||||
return
|
||||
job["state"] = "installing"
|
||||
try:
|
||||
for i, pack in enumerate(job["packs"]):
|
||||
if job.get("cancel_requested"):
|
||||
await _finish_cancelled(job)
|
||||
return
|
||||
pid = pack["id"]
|
||||
spec = _PACK_BY_ID.get(pid)
|
||||
if not spec:
|
||||
@@ -266,29 +470,7 @@ async def run_install_job(job_id: str, restart_fn) -> None:
|
||||
job["error"] = err or f"Failed to queue {pack['title']}."
|
||||
return
|
||||
await _start_manager_queue()
|
||||
deadline = time.time() + 1800
|
||||
while time.time() < deadline:
|
||||
st = await _manager_queue_status()
|
||||
done = int(st.get("done_count") or 0)
|
||||
total = int(st.get("total_count") or 0) or job["total"]
|
||||
processing = bool(st.get("is_processing"))
|
||||
job["total"] = max(job["total"], total)
|
||||
job["done"] = min(done, job["total"])
|
||||
if job["packs"]:
|
||||
idx = min(max(done, 0), len(job["packs"]) - 1)
|
||||
job["current_title"] = job["packs"][idx]["title"]
|
||||
if job["total"] > 0 and done >= job["total"] and not processing:
|
||||
break
|
||||
await asyncio.sleep(1.5)
|
||||
else:
|
||||
job["state"] = "error"
|
||||
job["error"] = "Install timed out. Check ComfyUI-Manager queue in the main ComfyUI window."
|
||||
return
|
||||
job["done"] = job["total"]
|
||||
job["state"] = "restarting"
|
||||
job["current_title"] = ""
|
||||
await asyncio.sleep(0.7)
|
||||
restart_fn()
|
||||
await _poll_manager_queue(job, restart_fn)
|
||||
except Exception as e: # noqa: BLE001
|
||||
job["state"] = "error"
|
||||
job["error"] = str(e) or "Install failed."
|
||||
|
||||
@@ -111,7 +111,9 @@
|
||||
pipelinePorts: () => j("GET", API("/pipeline-ports")),
|
||||
pipelineDeps: () => j("GET", API("/pipeline-deps")),
|
||||
pipelineDepsInstall: (packIds) => j("POST", API("/pipeline-deps/install"), { pack_ids: packIds }),
|
||||
pipelineDepsInstallManager: () => j("POST", API("/pipeline-deps/install"), { install_manager: true }),
|
||||
pipelineDepsInstallStatus: (jobId) => j("GET", API(`/pipeline-deps/install/${encodeURIComponent(jobId)}`)),
|
||||
pipelineDepsInstallCancel: (jobId) => j("POST", API(`/pipeline-deps/install/${encodeURIComponent(jobId)}/cancel`), {}),
|
||||
imageTargets: (pid) => j("GET", API("/image-targets" + (pid ? `?pid=${encodeURIComponent(pid)}` : ""))),
|
||||
// Models config is per-project; pass the project id. Falls back to the global
|
||||
// default route (used as the seed/template) when no project is given.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
let overlay = null;
|
||||
let _pollTimer = null;
|
||||
let _activeJobId = null;
|
||||
|
||||
function dismissed() {
|
||||
try { return localStorage.getItem(LS_KEY) === "1"; } catch (_) { return false; }
|
||||
@@ -51,7 +52,7 @@
|
||||
|
||||
function manualBlock(deps) {
|
||||
const box = el("div", "pipe-setup-manual");
|
||||
box.append(el("div", "pipe-setup-manual-title", "Install ComfyUI-Manager or add these repos under custom_nodes:"));
|
||||
box.append(el("div", "pipe-setup-manual-title", "Or clone these repos into custom_nodes manually:"));
|
||||
const ul = el("ul", "pipe-setup-list");
|
||||
(deps.manual_urls || []).forEach((u) => {
|
||||
const li = el("li");
|
||||
@@ -66,45 +67,112 @@
|
||||
return box;
|
||||
}
|
||||
|
||||
async function cancelInstall() {
|
||||
const jobId = _activeJobId;
|
||||
if (!jobId) {
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
return;
|
||||
}
|
||||
try { await API.pipelineDepsInstallCancel(jobId); } catch (_) { /* server may already be gone */ }
|
||||
_activeJobId = null;
|
||||
if (_pollTimer) { clearTimeout(_pollTimer); _pollTimer = null; }
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
}
|
||||
|
||||
function progressMessage(st) {
|
||||
if (st.kind === "manager") {
|
||||
return "Cloning ComfyUI-Manager…\nRestart follows automatically when ready.";
|
||||
}
|
||||
const n = Math.min(st.done + 1, st.total || 1);
|
||||
const title = st.current_title || "…";
|
||||
let msg = `Downloading and installing missing nodes:\n${n} out of ${st.total || "?"} - ${title}`;
|
||||
if (st.stale) {
|
||||
msg += "\n\nNo progress for a while. You can cancel and try again from ComfyUI-Manager.";
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
async function pollInstall(jobId, msgEl) {
|
||||
try {
|
||||
const st = await API.pipelineDepsInstallStatus(jobId);
|
||||
if (st.state === "installing" || st.state === "queued") {
|
||||
const n = Math.min(st.done + 1, st.total || 1);
|
||||
const title = st.current_title || "…";
|
||||
msgEl.textContent = `Downloading and installing missing nodes:\n${n} out of ${st.total || "?"} - ${title}`;
|
||||
msgEl.textContent = progressMessage(st);
|
||||
_pollTimer = setTimeout(() => pollInstall(jobId, msgEl), 1200);
|
||||
return;
|
||||
}
|
||||
if (st.state === "cancelled") {
|
||||
_activeJobId = null;
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
return;
|
||||
}
|
||||
if (st.state === "restarting") {
|
||||
msgEl.textContent = "Install complete.\nRestarting ComfyUI…";
|
||||
msgEl.textContent = st.kind === "manager"
|
||||
? "ComfyUI-Manager installed.\nRestarting ComfyUI…"
|
||||
: "Install complete.\nRestarting ComfyUI…";
|
||||
_activeJobId = null;
|
||||
window.FunPackRestart?.waitForReload?.(msgEl, Date.now());
|
||||
return;
|
||||
}
|
||||
if (st.state === "error") {
|
||||
_activeJobId = null;
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert(st.error || "Install failed.");
|
||||
return;
|
||||
}
|
||||
_pollTimer = setTimeout(() => pollInstall(jobId, msgEl), 1200);
|
||||
} catch (e) {
|
||||
_activeJobId = null;
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert(String(e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
function beginInstallOverlay(initialMessage) {
|
||||
closeModal();
|
||||
const msg = window.FunPackRestart?.showOverlay?.(initialMessage, {
|
||||
cancelLabel: "Cancel install",
|
||||
onCancel: () => cancelInstall(),
|
||||
});
|
||||
return msg;
|
||||
}
|
||||
|
||||
async function startManagerInstall() {
|
||||
const msg = beginInstallOverlay("Cloning ComfyUI-Manager…\nRestart follows automatically when ready.");
|
||||
if (!msg) return;
|
||||
try {
|
||||
const res = await API.pipelineDepsInstallManager();
|
||||
_activeJobId = res.job_id;
|
||||
pollInstall(res.job_id, msg);
|
||||
} catch (e) {
|
||||
_activeJobId = null;
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert("Install failed: " + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
async function restartComfyOnly() {
|
||||
closeModal();
|
||||
const msg = beginInstallOverlay("Restarting ComfyUI…\nComfyUI-Manager is on disk but not loaded yet.");
|
||||
if (!msg) return;
|
||||
try {
|
||||
await API.restart();
|
||||
} catch (_) { /* expected while server exits */ }
|
||||
window.FunPackRestart?.waitForReload?.(msg, Date.now());
|
||||
}
|
||||
|
||||
async function startInstall(deps) {
|
||||
const ids = (deps.missing_packs || []).map((p) => p.id);
|
||||
if (!ids.length) return;
|
||||
closeModal();
|
||||
const msg = window.FunPackRestart?.showOverlay?.(
|
||||
const msg = beginInstallOverlay(
|
||||
"Downloading and installing missing nodes:\n1 out of " + ids.length + " - " + (deps.missing_packs[0]?.title || "…"),
|
||||
);
|
||||
if (!msg) return;
|
||||
try {
|
||||
const res = await API.pipelineDepsInstall(ids);
|
||||
_activeJobId = res.job_id;
|
||||
pollInstall(res.job_id, msg);
|
||||
} catch (e) {
|
||||
_activeJobId = null;
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert("Install failed: " + (e.message || e));
|
||||
}
|
||||
@@ -116,21 +184,27 @@
|
||||
const modal = el("div", "modal pipe-setup-modal");
|
||||
const head = el("div", "modal-head");
|
||||
head.append(el("div", "modal-title", "Pipeline setup"));
|
||||
const closeBtn = el("button", "btn ghost", "Close");
|
||||
closeBtn.type = "button";
|
||||
closeBtn.onclick = () => closeModal();
|
||||
const headRight = el("div", "modal-head-right");
|
||||
headRight.append(closeBtn);
|
||||
head.append(headRight);
|
||||
modal.append(head);
|
||||
|
||||
const content = el("div", "modal-content");
|
||||
content.append(el("p", "pipe-setup-lead",
|
||||
"Nodes required for the built-in pipeline are missing. Do you want to install them?"));
|
||||
content.append(packList(deps));
|
||||
if (!deps.manager_available) {
|
||||
content.append(el("div", "pipe-setup-warn",
|
||||
"ComfyUI-Manager was not detected. Automatic install is unavailable."));
|
||||
if (deps.needs_manager_install) {
|
||||
content.append(el("p", "pipe-setup-lead",
|
||||
"Nodes required for the built-in pipeline are missing. Install ComfyUI-Manager first, then we can fetch the rest."));
|
||||
} else if (deps.needs_manager_restart) {
|
||||
content.append(el("p", "pipe-setup-lead",
|
||||
"ComfyUI-Manager is on disk but not running yet. Restart ComfyUI to load it, then install the missing node packs."));
|
||||
} else {
|
||||
content.append(el("p", "pipe-setup-lead",
|
||||
"Nodes required for the built-in pipeline are missing. Do you want to install them?"));
|
||||
}
|
||||
if ((deps.missing_packs || []).length) {
|
||||
content.append(packList(deps));
|
||||
}
|
||||
if (deps.needs_manager_install) {
|
||||
content.append(el("div", "pipe-setup-hint",
|
||||
"ComfyUI-Manager handles downloads and updates for custom node packs."));
|
||||
content.append(manualBlock(deps));
|
||||
} else if (!deps.manager_available) {
|
||||
content.append(manualBlock(deps));
|
||||
}
|
||||
if ((deps.unmapped_classes || []).length) {
|
||||
@@ -140,7 +214,17 @@
|
||||
modal.append(content);
|
||||
|
||||
const foot = el("div", "pipe-setup-foot");
|
||||
if (deps.manager_available) {
|
||||
if (deps.needs_manager_install) {
|
||||
const mgrBtn = el("button", "btn primary", "Install ComfyUI-Manager");
|
||||
mgrBtn.type = "button";
|
||||
mgrBtn.onclick = () => startManagerInstall();
|
||||
foot.append(mgrBtn);
|
||||
} else if (deps.needs_manager_restart) {
|
||||
const restartBtn = el("button", "btn primary", "Restart ComfyUI");
|
||||
restartBtn.type = "button";
|
||||
restartBtn.onclick = () => restartComfyOnly();
|
||||
foot.append(restartBtn);
|
||||
} else if (deps.manager_available) {
|
||||
const installBtn = el("button", "btn primary", "Install missing nodes");
|
||||
installBtn.type = "button";
|
||||
installBtn.onclick = () => startInstall(deps);
|
||||
@@ -168,7 +252,7 @@
|
||||
if (!st.project || !builtInPipelineActive(st)) return;
|
||||
let deps;
|
||||
try { deps = await API.pipelineDeps(); } catch (_) { return; }
|
||||
if (!deps?.needs_install) return;
|
||||
if (!deps?.needs_setup) return;
|
||||
openModal(deps);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
(function () {
|
||||
const { el } = window.dom;
|
||||
|
||||
function showOverlay(message) {
|
||||
function showOverlay(message, options) {
|
||||
document.querySelector(".restart-overlay")?.remove();
|
||||
const ov = el("div", "restart-overlay");
|
||||
const card = el("div", "restart-card");
|
||||
card.append(el("div", "restart-spin"));
|
||||
const msg = el("div", "restart-msg", message);
|
||||
card.append(msg);
|
||||
if (options?.onCancel) {
|
||||
const btn = el("button", "btn ghost restart-cancel", options.cancelLabel || "Cancel install");
|
||||
btn.type = "button";
|
||||
btn.onclick = options.onCancel;
|
||||
card.append(btn);
|
||||
}
|
||||
ov.append(card);
|
||||
document.body.append(ov);
|
||||
return msg;
|
||||
|
||||
@@ -1261,6 +1261,8 @@ label.field.linked input, label.field.linked select { opacity: .6; }
|
||||
border: 1px solid color-mix(in srgb, var(--amber) 35%, transparent); color: var(--muted); }
|
||||
.pipe-setup-manual-title { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
||||
.pipe-setup-foot { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px 16px 16px; border-top: 1px solid var(--line); }
|
||||
.pipe-setup-hint { margin: 0 0 10px; font-size: 12px; color: var(--muted); line-height: 1.45; }
|
||||
.restart-card .restart-cancel { margin-top: 4px; }
|
||||
|
||||
/* restart overlay */
|
||||
.restart-overlay { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center;
|
||||
|
||||
+29
-4
@@ -1717,23 +1717,36 @@ if web is not None and PromptServer is not None:
|
||||
except Exception:
|
||||
oi = {}
|
||||
mgr = await pipeline_deps.manager_available()
|
||||
return web.json_response(pipeline_deps.status_payload(oi, manager_available=mgr))
|
||||
return web.json_response(pipeline_deps.status_payload(
|
||||
oi,
|
||||
manager_available=mgr,
|
||||
manager_on_disk=pipeline_deps.manager_dir_on_disk(),
|
||||
))
|
||||
|
||||
@routes.post(UI_PREFIX + "/api/pipeline-deps/install")
|
||||
async def _pipeline_deps_install(req):
|
||||
import asyncio
|
||||
body = await req.json() if req.can_read_body else {}
|
||||
if body.get("install_manager"):
|
||||
if pipeline_deps.manager_dir_on_disk():
|
||||
return web.json_response(
|
||||
{"detail": "ComfyUI-Manager is already present on disk. Restart ComfyUI to load it."},
|
||||
status=400,
|
||||
)
|
||||
job = pipeline_deps.create_manager_install_job()
|
||||
asyncio.create_task(pipeline_deps.run_manager_install_job(job["job_id"], _restart_comfy))
|
||||
return web.json_response({"job_id": job["job_id"], "total": job["total"], "kind": "manager"})
|
||||
if not await pipeline_deps.manager_available():
|
||||
return web.json_response(
|
||||
{"detail": "ComfyUI-Manager is not installed or not reachable."},
|
||||
{"detail": "ComfyUI-Manager is not installed or not reachable. Install it first."},
|
||||
status=503,
|
||||
)
|
||||
body = await req.json() if req.can_read_body else {}
|
||||
pack_ids = body.get("pack_ids") or body.get("packs") or []
|
||||
if not isinstance(pack_ids, list) or not pack_ids:
|
||||
return web.json_response({"detail": "pack_ids required."}, status=400)
|
||||
job = pipeline_deps.create_install_job([str(x) for x in pack_ids])
|
||||
asyncio.create_task(pipeline_deps.run_install_job(job["job_id"], _restart_comfy))
|
||||
return web.json_response({"job_id": job["job_id"], "total": job["total"]})
|
||||
return web.json_response({"job_id": job["job_id"], "total": job["total"], "kind": "packs"})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/pipeline-deps/install/{job_id}")
|
||||
async def _pipeline_deps_install_status(req):
|
||||
@@ -1742,6 +1755,18 @@ if web is not None and PromptServer is not None:
|
||||
raise web.HTTPNotFound(reason="Install job not found")
|
||||
return web.json_response(job)
|
||||
|
||||
@routes.post(UI_PREFIX + "/api/pipeline-deps/install/{job_id}/cancel")
|
||||
async def _pipeline_deps_install_cancel(req):
|
||||
import asyncio
|
||||
job_id = req.match_info["job_id"]
|
||||
if not pipeline_deps.cancel_install_job(job_id):
|
||||
job = pipeline_deps.get_install_job(job_id)
|
||||
if not job:
|
||||
raise web.HTTPNotFound(reason="Install job not found")
|
||||
return web.json_response({"detail": "Install cannot be cancelled."}, status=409)
|
||||
asyncio.create_task(pipeline_deps._reset_manager_queue())
|
||||
return web.json_response({"cancelled": True, "job_id": job_id})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/pipeline-ports")
|
||||
async def _pipeline_ports(_req):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Tests for built-in pipeline dependency detection."""
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from movie_editor.backend import pipeline_deps
|
||||
|
||||
|
||||
@@ -8,6 +11,7 @@ def test_no_missing_when_all_core_present():
|
||||
assert pipeline_deps.missing_packs(oi) == []
|
||||
st = pipeline_deps.status_payload(oi, manager_available=True)
|
||||
assert st["needs_install"] is False
|
||||
assert st["needs_manager_install"] is False
|
||||
|
||||
|
||||
def test_missing_ltx_and_vhs_packs():
|
||||
@@ -27,15 +31,52 @@ def test_missing_ltx_and_vhs_packs():
|
||||
ids = {p["id"] for p in packs}
|
||||
assert "ComfyUI-LTXVideo" in ids
|
||||
assert "comfyui-videohelpersuite" in ids
|
||||
st = pipeline_deps.status_payload(oi, manager_available=False)
|
||||
st = pipeline_deps.status_payload(oi, manager_available=False, manager_on_disk=False)
|
||||
assert st["needs_install"] is True
|
||||
assert st["manager_available"] is False
|
||||
assert st["needs_manager_install"] is True
|
||||
assert st["needs_manager_restart"] is False
|
||||
assert len(st["manual_urls"]) >= 2
|
||||
|
||||
|
||||
def test_manager_on_disk_needs_restart_not_install():
|
||||
oi = {
|
||||
"FunPackStudio": {},
|
||||
"FunPackLTXAVSceneChainSampler": {},
|
||||
}
|
||||
st = pipeline_deps.status_payload(oi, manager_available=False, manager_on_disk=True)
|
||||
assert st["needs_manager_install"] is False
|
||||
assert st["needs_manager_restart"] is True
|
||||
|
||||
|
||||
def test_install_body_fallback_git():
|
||||
pack = pipeline_deps._PACK_BY_ID["ComfyUI-LTXVideo"]
|
||||
body = pipeline_deps._install_body(pack, None)
|
||||
assert body["version"] == "unknown"
|
||||
assert body["files"]
|
||||
assert "github.com" in body["files"][0]
|
||||
|
||||
|
||||
def test_cancel_install_job():
|
||||
job = pipeline_deps.create_install_job(["ComfyUI-LTXVideo"])
|
||||
assert pipeline_deps.cancel_install_job(job["job_id"]) is True
|
||||
snap = pipeline_deps.get_install_job(job["job_id"])
|
||||
assert snap is not None
|
||||
|
||||
|
||||
def test_install_manager_sync_skips_when_present(tmp_path):
|
||||
cn = tmp_path / "custom_nodes"
|
||||
mgr = cn / "ComfyUI-Manager"
|
||||
mgr.mkdir(parents=True)
|
||||
(mgr / ".git").mkdir()
|
||||
with patch.object(pipeline_deps, "custom_nodes_dir", return_value=cn):
|
||||
ok, msg = pipeline_deps.install_manager_sync()
|
||||
assert ok is True
|
||||
assert msg == "already_installed"
|
||||
|
||||
|
||||
def test_custom_nodes_dir_uses_folder_paths(tmp_path, monkeypatch):
|
||||
cn = tmp_path / "custom_nodes"
|
||||
cn.mkdir()
|
||||
fp = type("FP", (), {"get_folder_paths": staticmethod(lambda _: [str(cn)])})()
|
||||
monkeypatch.setitem(__import__("sys").modules, "folder_paths", fp)
|
||||
assert pipeline_deps.custom_nodes_dir() == cn
|
||||
|
||||
Reference in New Issue
Block a user