Files
doubletwisted-ComfyUI-Deadl…/scripts/maintenance/ComfyUISync.py
T

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()