177 lines
5.6 KiB
Python
177 lines
5.6 KiB
Python
from __future__ import absolute_import, print_function
|
|
|
|
r"""
|
|
ComfyUI portable installation sync maintenance script.
|
|
|
|
Mirrors a shared ComfyUI portable install to a local Worker path
|
|
using Robocopy /MIR, excluding user-specific input/ and output/ folders so
|
|
their contents are never erased. `.git` is included so workers can keep
|
|
the same Git metadata as the share. Only ComfyUI/input/example.png is copied separately
|
|
(as required for ComfyUI to work properly).
|
|
|
|
Designed to be launched on Deadline Workers via the CommandLine or
|
|
DeadlineCommand plugin,
|
|
so all stdout/stderr (or Deadline logging if available) ends up in job logs.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
|
|
SOURCE_DIR = os.environ.get("COMFY_SYNC_SOURCE", r"\\YOUR-SERVER\share\AI\ComfyUI_windows_portable")
|
|
DEST_DIR = os.environ.get("COMFY_SYNC_DEST", r"C:\AI\ComfyUI_windows_portable")
|
|
ROBOCOPY_EXECUTABLE = "robocopy"
|
|
ROBOCOPY_FLAGS = [
|
|
"/MIR", # Mirror source to destination (adds + deletes)
|
|
"/FFT", # Assume FAT file times (two-second granularity) for cross-protocol copies
|
|
"/Z", # Restartable mode
|
|
"/R:3", # Retry failed copies 3 times
|
|
"/W:5", # Wait 5 seconds between retries
|
|
"/NFL", # No file list (keeps logs smaller)
|
|
"/NDL", # No directory list
|
|
]
|
|
EXCLUDED_FILE_PATTERNS = ["*.log", "*.tmp"]
|
|
PUBLISH_LOCK_FILE = SOURCE_DIR + ".sync_in_progress"
|
|
PUBLISH_LOCK_POLL_SECONDS = 10
|
|
PUBLISH_LOCK_TIMEOUT_SECONDS = 60 * 60
|
|
PUBLISH_LOCK_STALE_SECONDS = 6 * 60 * 60
|
|
EXCLUDED_DIRS = [
|
|
"__pycache__",
|
|
"node_modules",
|
|
"logs",
|
|
os.path.join(SOURCE_DIR, "ComfyUI", "input"),
|
|
os.path.join(SOURCE_DIR, "ComfyUI", "output"),
|
|
]
|
|
|
|
try:
|
|
from Deadline.Scripting import ClientUtils # type: ignore
|
|
except Exception: # pragma: no cover - Deadline libs unavailable outside Worker
|
|
ClientUtils = None
|
|
|
|
|
|
def log(message):
|
|
"""Log to Deadline if available, otherwise stdout."""
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
line = "[ComfyUISync] {0} {1}".format(timestamp, message)
|
|
if ClientUtils:
|
|
ClientUtils.LogText(line)
|
|
else:
|
|
print(line)
|
|
|
|
|
|
def ensure_destination():
|
|
"""Ensure destination directory exists."""
|
|
if not os.path.isdir(DEST_DIR):
|
|
log("Destination directory does not exist, creating: {0}".format(DEST_DIR))
|
|
os.makedirs(DEST_DIR)
|
|
|
|
|
|
def wait_for_publish_lock():
|
|
"""Avoid copying the share while it is being refreshed from the source machine."""
|
|
start_time = time.time()
|
|
while os.path.exists(PUBLISH_LOCK_FILE):
|
|
try:
|
|
lock_age = time.time() - os.path.getmtime(PUBLISH_LOCK_FILE)
|
|
except OSError:
|
|
continue
|
|
|
|
if lock_age >= PUBLISH_LOCK_STALE_SECONDS:
|
|
log(
|
|
"Ignoring stale publish lock older than {0} seconds: {1}".format(
|
|
PUBLISH_LOCK_STALE_SECONDS,
|
|
PUBLISH_LOCK_FILE,
|
|
)
|
|
)
|
|
return
|
|
|
|
elapsed = time.time() - start_time
|
|
if elapsed >= PUBLISH_LOCK_TIMEOUT_SECONDS:
|
|
raise RuntimeError(
|
|
"Timed out waiting for publish lock to clear: {0}".format(
|
|
PUBLISH_LOCK_FILE
|
|
)
|
|
)
|
|
|
|
log("Publish lock present, waiting before sync: {0}".format(PUBLISH_LOCK_FILE))
|
|
time.sleep(PUBLISH_LOCK_POLL_SECONDS)
|
|
|
|
|
|
def mirror_comfyui():
|
|
"""Run robocopy mirror operation."""
|
|
cmd = [ROBOCOPY_EXECUTABLE, SOURCE_DIR, DEST_DIR] + ROBOCOPY_FLAGS
|
|
if EXCLUDED_FILE_PATTERNS:
|
|
cmd.append("/XF")
|
|
cmd.extend(EXCLUDED_FILE_PATTERNS)
|
|
if EXCLUDED_DIRS:
|
|
cmd.append("/XD")
|
|
cmd.extend(EXCLUDED_DIRS)
|
|
log("Running command: {0}".format(" ".join(cmd)))
|
|
process = subprocess.Popen(cmd)
|
|
process.wait()
|
|
exit_code = process.returncode
|
|
|
|
# Robocopy exit codes < 8 are success (0=No Change, 1=Copied, etc.)
|
|
if exit_code >= 8:
|
|
msg = "Robocopy failed with exit code {0}".format(exit_code)
|
|
log(msg)
|
|
raise RuntimeError(msg)
|
|
|
|
log("Robocopy completed successfully with exit code {0}".format(exit_code))
|
|
|
|
|
|
def ensure_example_png():
|
|
"""Copy ComfyUI/input/example.png from source to destination. Required for ComfyUI to work properly."""
|
|
src_file = os.path.join(SOURCE_DIR, "ComfyUI", "input", "example.png")
|
|
dest_dir = os.path.join(DEST_DIR, "ComfyUI", "input")
|
|
|
|
if not os.path.isfile(src_file):
|
|
log("Source example.png not found, skipping: {0}".format(src_file))
|
|
return
|
|
|
|
if not os.path.isdir(dest_dir):
|
|
log("Creating input directory: {0}".format(dest_dir))
|
|
os.makedirs(dest_dir)
|
|
|
|
cmd = [
|
|
ROBOCOPY_EXECUTABLE,
|
|
os.path.join(SOURCE_DIR, "ComfyUI", "input"),
|
|
dest_dir,
|
|
"example.png",
|
|
"/NFL", "/NDL",
|
|
]
|
|
log("Ensuring input/example.png exists: {0}".format(" ".join(cmd)))
|
|
process = subprocess.Popen(cmd)
|
|
process.wait()
|
|
exit_code = process.returncode
|
|
if exit_code >= 8:
|
|
log("Warning: Failed to copy example.png (exit code {0})".format(exit_code))
|
|
else:
|
|
log("example.png ensured successfully.")
|
|
|
|
|
|
def main():
|
|
log("ComfyUI portable installation sync starting.")
|
|
log("Source: {0}".format(SOURCE_DIR))
|
|
log("Destination: {0}".format(DEST_DIR))
|
|
|
|
wait_for_publish_lock()
|
|
ensure_destination()
|
|
mirror_comfyui()
|
|
ensure_example_png()
|
|
|
|
log("ComfyUI portable installation sync finished.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as exc: # pragma: no cover - runtime safeguard
|
|
log("ERROR: {0}".format(exc))
|
|
raise
|
|
|
|
|
|
def __main__(*args): # Deadline's ExecuteScript entry point
|
|
main()
|