From 656d948e0b3f18fb3fb0fe01540c4e8b374a8a27 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:35:31 +0300 Subject: [PATCH] Offer ComfyUI-Manager install first and allow cancelling stale node installs. --- movie_editor/backend/pipeline_deps.py | 262 +++++++++++++++++++---- movie_editor/frontend/api.js | 2 + movie_editor/frontend/pipeline_setup.js | 126 +++++++++-- movie_editor/frontend/restart_ui.js | 8 +- movie_editor/frontend/styles.css | 2 + movie_editor/server.py | 33 ++- movie_editor/tests/test_pipeline_deps.py | 45 +++- 7 files changed, 410 insertions(+), 68 deletions(-) diff --git a/movie_editor/backend/pipeline_deps.py b/movie_editor/backend/pipeline_deps.py index 01b32ce7..01c62cb2 100644 --- a/movie_editor/backend/pipeline_deps.py +++ b/movie_editor/backend/pipeline_deps.py @@ -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." diff --git a/movie_editor/frontend/api.js b/movie_editor/frontend/api.js index 8434b374..9097e843 100644 --- a/movie_editor/frontend/api.js +++ b/movie_editor/frontend/api.js @@ -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. diff --git a/movie_editor/frontend/pipeline_setup.js b/movie_editor/frontend/pipeline_setup.js index 5b55eb73..dc906c88 100644 --- a/movie_editor/frontend/pipeline_setup.js +++ b/movie_editor/frontend/pipeline_setup.js @@ -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); } diff --git a/movie_editor/frontend/restart_ui.js b/movie_editor/frontend/restart_ui.js index baf86384..9ef0be23 100644 --- a/movie_editor/frontend/restart_ui.js +++ b/movie_editor/frontend/restart_ui.js @@ -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; diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index b1f885bc..46fc4881 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -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; diff --git a/movie_editor/server.py b/movie_editor/server.py index 0ea361ad..b334f398 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -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: diff --git a/movie_editor/tests/test_pipeline_deps.py b/movie_editor/tests/test_pipeline_deps.py index 3a2be3e8..9894f955 100644 --- a/movie_editor/tests/test_pipeline_deps.py +++ b/movie_editor/tests/test_pipeline_deps.py @@ -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