feat(easy_pattern): add test-pattern node + auto-restart dev-sync (#135)

* feat(easy_pattern): add test-pattern node + auto-restart dev-sync

Adds an `Easy_Pattern` node (Koolook/Testing) for solid-color frame
batches with optional per-frame index overlays — verifies sequence
merging, frame insertion, batch indexing, and video-pipeline frame
order at a glance. Color inputs use the same
`White/Black/Gray/Custom` dropdown idiom `EasyResize_Koolook` already
exposes for pad/panel colors, so "Gray" means the same 50% sRGB across
nodes.

`scripts/sync_to_dev.py` now restarts the live ComfyUI server after a
successful sync (via ComfyUI-Manager's `/manager/reboot`). Default-on
because dev-sync is only ever invoked to see new code, and Python
custom-node files load once at server start — `--no-restart` is the
escape hatch. The `RUNTIME_PATHS` manifest also picks up
`k_easy_pattern.py` so adding a new top-level node doesn't silently
break the pack at import time on the next sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync-dev): restrict restart URL to http(s) for Bandit B310

`urllib.request.urlopen` accepts arbitrary schemes including `file://`,
which Bandit B310 flags as a path-traversal vector. The script's typical
caller is the maintainer's own shell, but a typo in `--restart-url`
could surprise — and the static gate is right to flag it. Validate the
scheme is http/https before the urlopen call and annotate it so Bandit
accepts the now-truthful narrowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
malkuthro
2026-05-13 21:00:34 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent bf02db8ad8
commit 9dab85c695
5 changed files with 337 additions and 2 deletions
+109 -2
View File
@@ -22,15 +22,26 @@ Example layouts:
Windows: C:/ComfyUI_portable/ComfyUI/custom_nodes/ComfyUI-Koolook
Usage:
python scripts/sync_to_dev.py # copy all runtime files
python scripts/sync_to_dev.py # copy + auto-restart ComfyUI
python scripts/sync_to_dev.py --dry-run # show what would copy
python scripts/sync_to_dev.py --no-restart # copy only; don't restart
python scripts/sync_to_dev.py --init # first-run: create the
# target folder if missing
# (parent custom_nodes/ must
# already exist), then sync
After copying, the script POSTs to ComfyUI-Manager's reboot endpoint
(`http://127.0.0.1:8188/manager/reboot` by default) so the live server
re-execs and picks up the new Python files. This is on by default
because the only reason to invoke this script is to see new code —
file-only sync without a restart leaves Python `.py` changes invisible
(custom-node modules load once at server start). Use `--no-restart` to
opt out, e.g. when staging files for a session you don't want to disturb.
Restart failures (Manager not installed, ComfyUI not running, etc.)
print a diagnostic but do NOT fail the sync.
Exit codes:
0 success
0 success (sync completed; restart is best-effort and won't change this)
2 KOLOOK_COMFYUI_DEV_PATH unset, parent missing, or target missing
(without --init)
3 --init refused: parent is not an existing directory or doesn't
@@ -48,8 +59,19 @@ import os
import shutil
import subprocess
import sys
import urllib.error
import urllib.request
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse
# Default ComfyUI-Manager reboot endpoint. ComfyUI-Manager registers this
# as a GET route; hitting it makes the server `os.execv` itself, so the
# same terminal/process picks the new node code up without a manual kill
# + relaunch cycle. Override via `--restart-url` or skip via
# `--no-restart` if your install runs on a different port / has no
# Manager / you want to stage files without disturbing the live session.
DEFAULT_RESTART_URL = "http://127.0.0.1:8188/manager/reboot"
REPO_ROOT = Path(__file__).resolve().parent.parent
@@ -101,6 +123,63 @@ def _build_line() -> str:
return f"{sha} - {_get_worktree_name()}"
def trigger_restart(url: str, timeout: float = 2.0) -> tuple[bool, str]:
"""Best-effort POST/GET to ComfyUI-Manager's reboot endpoint.
Returns ``(success, human_message)``. Never raises — the sync
itself succeeded by the time we call this, and a failed restart
should never mask that with a non-zero exit. The maintainer can
always restart manually if the auto-restart didn't take.
Failure-mode mapping:
- Connection refused → ComfyUI not running; nothing to do.
- HTTP 404 → Manager not installed at this URL.
- HTTP 2xx → restart triggered cleanly.
- Connection reset / timeout → the server died mid-response (the
restart's `os.execv` raced our read); the request still landed,
so we treat this as success.
"""
# Restrict to http/https. `urlopen` would otherwise accept `file://`,
# `ftp://`, etc. — a typo in `--restart-url` could surprise the
# maintainer by opening a local file via the same code path that
# normally talks to ComfyUI-Manager. Static analyzers (Bandit B310)
# also flag the broader urlopen surface; this guard makes the
# `# nosec` annotation below truthful rather than dismissive.
scheme = urlparse(url).scheme.lower()
if scheme not in ("http", "https"):
return False, (
f"restart skipped: --restart-url must be http(s); got "
f"scheme '{scheme}' in {url}"
)
try:
# ComfyUI-Manager registers /manager/reboot as GET, but POST
# also lands harmlessly — using GET keeps it simple.
urllib.request.urlopen(url, timeout=timeout) # nosec B310 - scheme validated above
return True, "restart triggered"
except urllib.error.HTTPError as e:
if e.code == 404:
return False, (
f"restart skipped: ComfyUI-Manager not detected at {url} "
f"(404). Install Manager or pass --no-restart."
)
return False, f"restart skipped: endpoint returned HTTP {e.code}"
except urllib.error.URLError as e:
reason = e.reason
if isinstance(reason, ConnectionRefusedError):
return False, f"restart skipped: ComfyUI not running at {url}"
# Read timeout / connection reset usually means the server
# received the request and rebooted before completing the
# response — desired outcome.
if isinstance(reason, (TimeoutError, ConnectionResetError)) or "timed out" in str(reason).lower():
return True, "restart triggered (server stopped responding during reboot — normal)"
return False, f"restart skipped: unreachable ({reason})"
except (ConnectionResetError, OSError):
# Same race as above: server began rebooting before we finished
# reading. The request landed; that's what matters.
return True, "restart triggered (connection reset during reboot)"
def write_build_info(target: Path, scope: str | None) -> None:
"""Drop a tiny JSON next to the sidebar JS so the in-browser footer
can render `dev <sha> · <time>` (and an italic <scope> on a second
@@ -133,6 +212,7 @@ RUNTIME_PATHS: tuple[str, ...] = (
"config.json",
"k_ai_pipeline.py",
"k_easy_image_batch.py",
"k_easy_pattern.py",
"k_easy_resize.py",
"k_easy_track.py",
"k_easy_wan22_prompt.py",
@@ -286,6 +366,30 @@ def main() -> int:
"the footer renders just identifier + timestamp."
),
)
parser.add_argument(
"--no-restart",
action="store_true",
help=(
"Skip the auto-restart of the live ComfyUI server. By default "
"the script POSTs to ComfyUI-Manager's reboot endpoint after a "
"successful sync so the new Python files are actually loaded "
"(custom-node `.py` files load once at server start; without a "
"restart the maintainer's changes won't be visible). Use this "
"flag when you want to stage files without disturbing a live "
"session (rare — `dev-sync` is normally invoked precisely "
"because you want to see new code)."
),
)
parser.add_argument(
"--restart-url",
type=str,
default=DEFAULT_RESTART_URL,
help=(
f"Override the restart endpoint. Default: {DEFAULT_RESTART_URL}. "
f"Adjust if your ComfyUI listens on a non-standard host/port. "
f"Ignored when --no-restart is set or --dry-run is used."
),
)
args = parser.parse_args()
load_dotenv(REPO_ROOT / ".env")
@@ -314,6 +418,9 @@ def main() -> int:
print(f"{verb} {n} entries -> {target}")
if not args.dry_run:
write_build_info(target, args.scope)
if not args.no_restart:
ok, msg = trigger_restart(args.restart_url)
print(msg)
return 0