Spaces:
Running
on
Zero
Running
on
Zero
import os | |
import sys | |
import subprocess | |
import re | |
import shutil | |
import configparser | |
import platform | |
from datetime import datetime | |
import git | |
from git.remote import RemoteProgress | |
from urllib.parse import urlparse | |
from tqdm.auto import tqdm | |
import aiohttp | |
import threading | |
import json | |
import time | |
import yaml | |
import zipfile | |
glob_path = os.path.join(os.path.dirname(__file__)) # ComfyUI-Manager/glob | |
sys.path.append(glob_path) | |
import cm_global | |
from manager_util import * | |
version = [2, 54] | |
version_str = f"V{version[0]}.{version[1]}" + (f'.{version[2]}' if len(version) > 2 else '') | |
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) | |
custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, '..')) | |
default_custom_nodes_path = None | |
def get_default_custom_nodes_path(): | |
global default_custom_nodes_path | |
if default_custom_nodes_path is None: | |
try: | |
import folder_paths | |
default_custom_nodes_path = folder_paths.get_folder_paths("custom_nodes")[0] | |
except: | |
default_custom_nodes_path = custom_nodes_path | |
return default_custom_nodes_path | |
def get_custom_nodes_paths(): | |
try: | |
import folder_paths | |
return folder_paths.get_folder_paths("custom_nodes") | |
except: | |
return [custom_nodes_path] | |
comfy_path = os.environ.get('COMFYUI_PATH') | |
if comfy_path is None: | |
comfy_path = os.path.abspath(os.path.join(custom_nodes_path, '..')) | |
channel_list_path = os.path.join(comfyui_manager_path, 'channels.list') | |
config_path = os.path.join(comfyui_manager_path, "config.ini") | |
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts") | |
git_script_path = os.path.join(comfyui_manager_path, "git_helper.py") | |
cache_dir = os.path.join(comfyui_manager_path, '.cache') | |
cached_config = None | |
js_path = None | |
comfy_ui_required_revision = 1930 | |
comfy_ui_required_commit_datetime = datetime(2024, 1, 24, 0, 0, 0) | |
comfy_ui_revision = "Unknown" | |
comfy_ui_commit_datetime = datetime(1900, 1, 1, 0, 0, 0) | |
cache_lock = threading.Lock() | |
channel_dict = None | |
channel_list = None | |
pip_map = None | |
def remap_pip_package(pkg): | |
if pkg in cm_global.pip_overrides: | |
res = cm_global.pip_overrides[pkg] | |
print(f"[ComfyUI-Manager] '{pkg}' is remapped to '{res}'") | |
return res | |
else: | |
return pkg | |
def get_installed_packages(): | |
global pip_map | |
if pip_map is None: | |
try: | |
result = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], universal_newlines=True) | |
pip_map = {} | |
for line in result.split('\n'): | |
x = line.strip() | |
if x: | |
y = line.split() | |
if y[0] == 'Package' or y[0].startswith('-'): | |
continue | |
pip_map[y[0]] = y[1] | |
except subprocess.CalledProcessError as e: | |
print(f"[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.") | |
return set() | |
return pip_map | |
def clear_pip_cache(): | |
global pip_map | |
pip_map = None | |
def is_blacklisted(name): | |
name = name.strip() | |
pattern = r'([^<>!=]+)([<>!=]=?)([^ ]*)' | |
match = re.search(pattern, name) | |
if match: | |
name = match.group(1) | |
if name in cm_global.pip_blacklist: | |
return True | |
if name in cm_global.pip_downgrade_blacklist: | |
pips = get_installed_packages() | |
if match is None: | |
if name in pips: | |
return True | |
elif match.group(2) in ['<=', '==', '<']: | |
if name in pips: | |
if StrictVersion(pips[name]) >= StrictVersion(match.group(3)): | |
return True | |
return False | |
def is_installed(name): | |
name = name.strip() | |
if name.startswith('#'): | |
return True | |
pattern = r'([^<>!=]+)([<>!=]=?)([0-9.a-zA-Z]*)' | |
match = re.search(pattern, name) | |
if match: | |
name = match.group(1) | |
if name in cm_global.pip_blacklist: | |
return True | |
if name in cm_global.pip_downgrade_blacklist: | |
pips = get_installed_packages() | |
if match is None: | |
if name in pips: | |
return True | |
elif match.group(2) in ['<=', '==', '<']: | |
if name in pips: | |
if StrictVersion(pips[name]) >= StrictVersion(match.group(3)): | |
print(f"[ComfyUI-Manager] skip black listed pip installation: '{name}'") | |
return True | |
return name.lower() in get_installed_packages() | |
def get_channel_dict(): | |
global channel_dict | |
if channel_dict is None: | |
channel_dict = {} | |
if not os.path.exists(channel_list_path): | |
shutil.copy(channel_list_path+'.template', channel_list_path) | |
with open(os.path.join(comfyui_manager_path, 'channels.list'), 'r') as file: | |
channels = file.read() | |
for x in channels.split('\n'): | |
channel_info = x.split("::") | |
if len(channel_info) == 2: | |
channel_dict[channel_info[0]] = channel_info[1] | |
return channel_dict | |
def get_channel_list(): | |
global channel_list | |
if channel_list is None: | |
channel_list = [] | |
for k, v in get_channel_dict().items(): | |
channel_list.append(f"{k}::{v}") | |
return channel_list | |
class ManagerFuncs: | |
def __init__(self): | |
pass | |
def get_current_preview_method(self): | |
return "none" | |
def run_script(self, cmd, cwd='.'): | |
if len(cmd) > 0 and cmd[0].startswith("#"): | |
print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`") | |
return 0 | |
new_env = os.environ.copy() | |
new_env["COMFYUI_PATH"] = comfy_path | |
subprocess.check_call(cmd, cwd=cwd, env=new_env) | |
return 0 | |
manager_funcs = ManagerFuncs() | |
def write_config(): | |
config = configparser.ConfigParser() | |
config['default'] = { | |
'preview_method': manager_funcs.get_current_preview_method(), | |
'badge_mode': get_config()['badge_mode'], | |
'git_exe': get_config()['git_exe'], | |
'channel_url': get_config()['channel_url'], | |
'share_option': get_config()['share_option'], | |
'bypass_ssl': get_config()['bypass_ssl'], | |
"file_logging": get_config()['file_logging'], | |
'default_ui': get_config()['default_ui'], | |
'component_policy': get_config()['component_policy'], | |
'double_click_policy': get_config()['double_click_policy'], | |
'windows_selector_event_loop_policy': get_config()['windows_selector_event_loop_policy'], | |
'model_download_by_agent': get_config()['model_download_by_agent'], | |
'downgrade_blacklist': get_config()['downgrade_blacklist'], | |
'security_level': get_config()['security_level'], | |
} | |
with open(config_path, 'w') as configfile: | |
config.write(configfile) | |
def read_config(): | |
try: | |
config = configparser.ConfigParser() | |
config.read(config_path) | |
default_conf = config['default'] | |
# policy migration: disable_unsecure_features -> security_level | |
if 'disable_unsecure_features' in default_conf: | |
if default_conf['disable_unsecure_features'].lower() == 'true': | |
security_level = 'strong' | |
else: | |
security_level = 'normal' | |
else: | |
security_level = default_conf['security_level'] if 'security_level' in default_conf else 'normal' | |
return { | |
'preview_method': default_conf['preview_method'] if 'preview_method' in default_conf else manager_funcs.get_current_preview_method(), | |
'badge_mode': default_conf['badge_mode'] if 'badge_mode' in default_conf else 'none', | |
'git_exe': default_conf['git_exe'] if 'git_exe' in default_conf else '', | |
'channel_url': default_conf['channel_url'] if 'channel_url' in default_conf else 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main', | |
'share_option': default_conf['share_option'] if 'share_option' in default_conf else 'all', | |
'bypass_ssl': default_conf['bypass_ssl'].lower() == 'true' if 'bypass_ssl' in default_conf else False, | |
'file_logging': default_conf['file_logging'].lower() == 'true' if 'file_logging' in default_conf else True, | |
'default_ui': default_conf['default_ui'] if 'default_ui' in default_conf else 'none', | |
'component_policy': default_conf['component_policy'] if 'component_policy' in default_conf else 'workflow', | |
'double_click_policy': default_conf['double_click_policy'] if 'double_click_policy' in default_conf else 'copy-all', | |
'windows_selector_event_loop_policy': default_conf['windows_selector_event_loop_policy'].lower() == 'true' if 'windows_selector_event_loop_policy' in default_conf else False, | |
'model_download_by_agent': default_conf['model_download_by_agent'].lower() == 'true' if 'model_download_by_agent' in default_conf else False, | |
'downgrade_blacklist': default_conf['downgrade_blacklist'] if 'downgrade_blacklist' in default_conf else '', | |
'security_level': security_level | |
} | |
except Exception: | |
return { | |
'preview_method': manager_funcs.get_current_preview_method(), | |
'badge_mode': 'none', | |
'git_exe': '', | |
'channel_url': 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main', | |
'share_option': 'all', | |
'bypass_ssl': False, | |
'file_logging': True, | |
'default_ui': 'none', | |
'component_policy': 'workflow', | |
'double_click_policy': 'copy-all', | |
'windows_selector_event_loop_policy': False, | |
'model_download_by_agent': False, | |
'downgrade_blacklist': '', | |
'security_level': 'normal', | |
} | |
def get_config(): | |
global cached_config | |
if cached_config is None: | |
cached_config = read_config() | |
return cached_config | |
def switch_to_default_branch(repo): | |
show_result = repo.git.remote("show", "origin") | |
matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result) | |
if matches: | |
default_branch = matches.group(1) | |
repo.git.checkout(default_branch) | |
def try_install_script(url, repo_path, install_cmd, instant_execution=False): | |
if not instant_execution and ((len(install_cmd) > 0 and install_cmd[0].startswith('#')) or (platform.system() == "Windows" and comfy_ui_commit_datetime.date() >= comfy_ui_required_commit_datetime.date())): | |
if not os.path.exists(startup_script_path): | |
os.makedirs(startup_script_path) | |
script_path = os.path.join(startup_script_path, "install-scripts.txt") | |
with open(script_path, "a") as file: | |
obj = [repo_path] + install_cmd | |
file.write(f"{obj}\n") | |
return True | |
else: | |
if len(install_cmd) == 5 and install_cmd[2:4] == ['pip', 'install']: | |
if is_blacklisted(install_cmd[4]): | |
print(f"[ComfyUI-Manager] skip black listed pip installation: '{install_cmd[4]}'") | |
return True | |
print(f"\n## ComfyUI-Manager: EXECUTE => {install_cmd}") | |
code = manager_funcs.run_script(install_cmd, cwd=repo_path) | |
if platform.system() != "Windows": | |
try: | |
if comfy_ui_commit_datetime.date() < comfy_ui_required_commit_datetime.date(): | |
print("\n\n###################################################################") | |
print(f"[WARN] ComfyUI-Manager: Your ComfyUI version ({comfy_ui_revision})[{comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version.") | |
print(f"[WARN] The extension installation feature may not work properly in the current installed ComfyUI version on Windows environment.") | |
print("###################################################################\n\n") | |
except: | |
pass | |
if code != 0: | |
if url is None: | |
url = os.path.dirname(repo_path) | |
print(f"install script failed: {url}") | |
return False | |
# use subprocess to avoid file system lock by git (Windows) | |
def __win_check_git_update(path, do_fetch=False, do_update=False): | |
if do_fetch: | |
command = [sys.executable, git_script_path, "--fetch", path] | |
elif do_update: | |
command = [sys.executable, git_script_path, "--pull", path] | |
else: | |
command = [sys.executable, git_script_path, "--check", path] | |
new_env = os.environ.copy() | |
new_env["COMFYUI_PATH"] = comfy_path | |
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=get_default_custom_nodes_path()) | |
output, _ = process.communicate() | |
output = output.decode('utf-8').strip() | |
if 'detected dubious' in output: | |
# fix and try again | |
safedir_path = path.replace('\\', '/') | |
try: | |
print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on '{safedir_path}' repo") | |
process = subprocess.Popen(['git', 'config', '--global', '--add', 'safe.directory', safedir_path], env=new_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
output, _ = process.communicate() | |
process = subprocess.Popen(command, env=new_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
output, _ = process.communicate() | |
output = output.decode('utf-8').strip() | |
except Exception: | |
print(f'[ComfyUI-Manager] failed to fixing') | |
if 'detected dubious' in output: | |
print(f'\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n' | |
f'-----------------------------------------------------------------------------------------\n' | |
f'git config --global --add safe.directory "{safedir_path}"\n' | |
f'-----------------------------------------------------------------------------------------\n') | |
if do_update: | |
if "CUSTOM NODE PULL: Success" in output: | |
process.wait() | |
print(f"\x1b[2K\rUpdated: {path}") | |
return True, True # updated | |
elif "CUSTOM NODE PULL: None" in output: | |
process.wait() | |
return False, True # there is no update | |
else: | |
print(f"\x1b[2K\rUpdate error: {path}") | |
process.wait() | |
return False, False # update failed | |
else: | |
if "CUSTOM NODE CHECK: True" in output: | |
process.wait() | |
return True, True | |
elif "CUSTOM NODE CHECK: False" in output: | |
process.wait() | |
return False, True | |
else: | |
print(f"\x1b[2K\rFetch error: {path}") | |
print(f"\n{output}\n") | |
process.wait() | |
return False, True | |
def __win_check_git_pull(path): | |
new_env = os.environ.copy() | |
new_env["COMFYUI_PATH"] = comfy_path | |
command = [sys.executable, git_script_path, "--pull", path] | |
process = subprocess.Popen(command, env=new_env, cwd=get_default_custom_nodes_path()) | |
process.wait() | |
def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False): | |
install_script_path = os.path.join(repo_path, "install.py") | |
requirements_path = os.path.join(repo_path, "requirements.txt") | |
if lazy_mode: | |
install_cmd = ["#LAZY-INSTALL-SCRIPT", sys.executable] | |
try_install_script(url, repo_path, install_cmd) | |
else: | |
if os.path.exists(requirements_path): | |
print("Install: pip packages") | |
pip_fixer = PIPFixer(get_installed_packages()) | |
with open(requirements_path, "r") as requirements_file: | |
for line in requirements_file: | |
#handle comments | |
if '#' in line: | |
if line.strip()[0] == '#': | |
print("Line is comment...skipping") | |
continue | |
else: | |
line = line.split('#')[0].strip() | |
package_name = remap_pip_package(line.strip()) | |
if package_name and not package_name.startswith('#'): | |
if '--index-url' in package_name: | |
s = package_name.split('--index-url') | |
install_cmd = [sys.executable, "-m", "pip", "install", s[0].strip(), '--index-url', s[1].strip()] | |
else: | |
install_cmd = [sys.executable, "-m", "pip", "install", package_name] | |
if package_name.strip() != "" and not package_name.startswith('#'): | |
try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) | |
pip_fixer.fix_broken() | |
if os.path.exists(install_script_path): | |
print(f"Install: install script") | |
install_cmd = [sys.executable, "install.py"] | |
try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) | |
return True | |
def git_repo_has_updates(path, do_fetch=False, do_update=False): | |
if do_fetch: | |
print(f"\x1b[2K\rFetching: {path}", end='') | |
elif do_update: | |
print(f"\x1b[2K\rUpdating: {path}", end='') | |
# Check if the path is a git repository | |
if not os.path.exists(os.path.join(path, '.git')): | |
raise ValueError('Not a git repository') | |
if platform.system() == "Windows": | |
updated, success = __win_check_git_update(path, do_fetch, do_update) | |
if updated and success: | |
execute_install_script(None, path, lazy_mode=True) | |
return updated, success | |
else: | |
# Fetch the latest commits from the remote repository | |
repo = git.Repo(path) | |
current_branch = repo.active_branch | |
branch_name = current_branch.name | |
remote_name = 'origin' | |
remote = repo.remote(name=remote_name) | |
# Get the current commit hash | |
commit_hash = repo.head.commit.hexsha | |
if do_fetch or do_update: | |
remote.fetch() | |
if do_update: | |
if repo.head.is_detached: | |
switch_to_default_branch(repo) | |
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha | |
if commit_hash == remote_commit_hash: | |
repo.close() | |
return False, True | |
try: | |
remote.pull() | |
repo.git.submodule('update', '--init', '--recursive') | |
new_commit_hash = repo.head.commit.hexsha | |
if commit_hash != new_commit_hash: | |
execute_install_script(None, path) | |
print(f"\x1b[2K\rUpdated: {path}") | |
return True, True | |
else: | |
return False, False | |
except Exception as e: | |
print(f"\nUpdating failed: {path}\n{e}", file=sys.stderr) | |
return False, False | |
if repo.head.is_detached: | |
repo.close() | |
return True, True | |
# Get commit hash of the remote branch | |
current_branch = repo.active_branch | |
branch_name = current_branch.name | |
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha | |
# Compare the commit hashes to determine if the local repository is behind the remote repository | |
if commit_hash != remote_commit_hash: | |
# Get the commit dates | |
commit_date = repo.head.commit.committed_datetime | |
remote_commit_date = repo.refs[f'{remote_name}/{branch_name}'].object.committed_datetime | |
# Compare the commit dates to determine if the local repository is behind the remote repository | |
if commit_date < remote_commit_date: | |
repo.close() | |
return True, True | |
repo.close() | |
return False, True | |
class GitProgress(RemoteProgress): | |
def __init__(self): | |
super().__init__() | |
self.pbar = tqdm() | |
def update(self, op_code, cur_count, max_count=None, message=''): | |
self.pbar.total = max_count | |
self.pbar.n = cur_count | |
self.pbar.pos = 0 | |
self.pbar.refresh() | |
def is_valid_url(url): | |
try: | |
# Check for HTTP/HTTPS URL format | |
result = urlparse(url) | |
if all([result.scheme, result.netloc]): | |
return True | |
finally: | |
# Check for SSH git URL format | |
pattern = re.compile(r"^(.+@|ssh:\/\/).+:.+$") | |
if pattern.match(url): | |
return True | |
return False | |
def gitclone_install(files, instant_execution=False, msg_prefix=''): | |
print(f"{msg_prefix}Install: {files}") | |
for url in files: | |
if not is_valid_url(url): | |
print(f"Invalid git url: '{url}'") | |
return False | |
if url.endswith("/"): | |
url = url[:-1] | |
try: | |
print(f"Download: git clone '{url}'") | |
repo_name = os.path.splitext(os.path.basename(url))[0] | |
repo_path = os.path.join(get_default_custom_nodes_path(), repo_name) | |
# Clone the repository from the remote URL | |
if not instant_execution and platform.system() == 'Windows': | |
res = manager_funcs.run_script([sys.executable, git_script_path, "--clone", get_default_custom_nodes_path(), url], cwd=get_default_custom_nodes_path()) | |
if res != 0: | |
return False | |
else: | |
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress()) | |
repo.git.clear_cache() | |
repo.close() | |
if not execute_install_script(url, repo_path, instant_execution=instant_execution): | |
return False | |
except Exception as e: | |
print(f"Install(git-clone) error: {url} / {e}", file=sys.stderr) | |
return False | |
print("Installation was successful.") | |
return True | |
def git_pull(path): | |
# Check if the path is a git repository | |
if not os.path.exists(os.path.join(path, '.git')): | |
raise ValueError('Not a git repository') | |
# Pull the latest changes from the remote repository | |
if platform.system() == "Windows": | |
return __win_check_git_pull(path) | |
else: | |
repo = git.Repo(path) | |
if repo.is_dirty(): | |
repo.git.stash() | |
if repo.head.is_detached: | |
switch_to_default_branch(repo) | |
current_branch = repo.active_branch | |
remote_name = current_branch.tracking_branch().remote_name | |
remote = repo.remote(name=remote_name) | |
remote.pull() | |
repo.git.submodule('update', '--init', '--recursive') | |
repo.close() | |
return True | |
async def get_data(uri, silent=False): | |
if not silent: | |
print(f"FETCH DATA from: {uri}", end="") | |
if uri.startswith("http"): | |
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: | |
async with session.get(uri) as resp: | |
json_text = await resp.text() | |
else: | |
with cache_lock: | |
with open(uri, "r", encoding="utf-8") as f: | |
json_text = f.read() | |
json_obj = json.loads(json_text) | |
if not silent: | |
print(f" [DONE]") | |
return json_obj | |
def simple_hash(input_string): | |
hash_value = 0 | |
for char in input_string: | |
hash_value = (hash_value * 31 + ord(char)) % (2**32) | |
return hash_value | |
def is_file_created_within_one_day(file_path): | |
if not os.path.exists(file_path): | |
return False | |
file_creation_time = os.path.getctime(file_path) | |
current_time = datetime.now().timestamp() | |
time_difference = current_time - file_creation_time | |
return time_difference <= 86400 | |
async def get_data_by_mode(mode, filename, channel_url=None): | |
if channel_url in get_channel_dict(): | |
channel_url = get_channel_dict()[channel_url] | |
try: | |
if mode == "local": | |
uri = os.path.join(comfyui_manager_path, filename) | |
json_obj = await get_data(uri) | |
else: | |
if channel_url is None: | |
uri = get_config()['channel_url'] + '/' + filename | |
else: | |
uri = channel_url + '/' + filename | |
cache_uri = str(simple_hash(uri))+'_'+filename | |
cache_uri = os.path.join(cache_dir, cache_uri) | |
if mode == "cache": | |
if is_file_created_within_one_day(cache_uri): | |
json_obj = await get_data(cache_uri) | |
else: | |
json_obj = await get_data(uri) | |
with cache_lock: | |
with open(cache_uri, "w", encoding='utf-8') as file: | |
json.dump(json_obj, file, indent=4, sort_keys=True) | |
else: | |
json_obj = await get_data(uri) | |
with cache_lock: | |
with open(cache_uri, "w", encoding='utf-8') as file: | |
json.dump(json_obj, file, indent=4, sort_keys=True) | |
except Exception as e: | |
print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename}\n=> {e}") | |
uri = os.path.join(comfyui_manager_path, filename) | |
json_obj = await get_data(uri) | |
return json_obj | |
def lookup_installed_custom_nodes(repo_name): | |
try: | |
import folder_paths | |
base_paths = folder_paths.get_folder_paths("custom_nodes") | |
except: | |
base_paths = [custom_nodes_path] | |
for base_path in base_paths: | |
repo_path = os.path.join(base_path, repo_name) | |
if os.path.exists(repo_path): | |
return True, repo_path | |
elif os.path.exists(repo_path+'.disabled'): | |
return False, repo_path | |
return None | |
def gitclone_fix(files, instant_execution=False): | |
print(f"Try fixing: {files}") | |
for url in files: | |
if not is_valid_url(url): | |
print(f"Invalid git url: '{url}'") | |
return False | |
if url.endswith("/"): | |
url = url[:-1] | |
try: | |
repo_name = os.path.splitext(os.path.basename(url))[0] | |
repo_path = lookup_installed_custom_nodes(repo_name) | |
if repo_path is not None: | |
repo_path = repo_path[1] | |
if not execute_install_script(url, repo_path, instant_execution=instant_execution): | |
return False | |
else: | |
print(f"Custom node not found: {repo_name}") | |
except Exception as e: | |
print(f"Install(git-clone) error: {url} / {e}", file=sys.stderr) | |
return False | |
print(f"Attempt to fixing '{files}' is done.") | |
return True | |
def pip_install(packages): | |
install_cmd = ['#FORCE', sys.executable, "-m", "pip", "install", '-U'] + packages | |
try_install_script('pip install via manager', '..', install_cmd) | |
def rmtree(path): | |
retry_count = 3 | |
while True: | |
try: | |
retry_count -= 1 | |
if platform.system() == "Windows": | |
manager_funcs.run_script(['attrib', '-R', path + '\\*', '/S']) | |
shutil.rmtree(path) | |
return True | |
except Exception as ex: | |
print(f"ex: {ex}") | |
time.sleep(3) | |
if retry_count < 0: | |
raise ex | |
print(f"Uninstall retry({retry_count})") | |
def gitclone_uninstall(files): | |
import os | |
print(f"Uninstall: {files}") | |
for url in files: | |
if url.endswith("/"): | |
url = url[:-1] | |
try: | |
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") | |
repo_path = lookup_installed_custom_nodes(dir_name) | |
if repo_path is None: | |
continue | |
dir_path = repo_path[1] | |
install_script_path = os.path.join(dir_path, "uninstall.py") | |
disable_script_path = os.path.join(dir_path, "disable.py") | |
if os.path.exists(install_script_path): | |
uninstall_cmd = [sys.executable, "uninstall.py"] | |
code = manager_funcs.run_script(uninstall_cmd, cwd=dir_path) | |
if code != 0: | |
print(f"An error occurred during the execution of the uninstall.py script. Only the '{dir_path}' will be deleted.") | |
elif os.path.exists(disable_script_path): | |
disable_script = [sys.executable, "disable.py"] | |
code = manager_funcs.run_script(disable_script, cwd=dir_path) | |
if code != 0: | |
print(f"An error occurred during the execution of the disable.py script. Only the '{dir_path}' will be deleted.") | |
rmtree(dir_path) | |
except Exception as e: | |
print(f"Uninstall(git-clone) error: {url} / {e}", file=sys.stderr) | |
return False | |
print("Uninstallation was successful.") | |
return True | |
def gitclone_set_active(files, is_disable): | |
import os | |
if is_disable: | |
action_name = "Disable" | |
else: | |
action_name = "Enable" | |
print(f"{action_name}: {files}") | |
for url in files: | |
if url.endswith("/"): | |
url = url[:-1] | |
try: | |
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") | |
repo_path = lookup_installed_custom_nodes(dir_name) | |
if repo_path is None: | |
continue | |
dir_path = repo_path[1] | |
if is_disable: | |
current_path = dir_path | |
new_path = dir_path + ".disabled" | |
else: | |
current_path = dir_path + ".disabled" | |
new_path = dir_path | |
os.rename(current_path, new_path) | |
if is_disable: | |
if os.path.exists(os.path.join(new_path, "disable.py")): | |
disable_script = [sys.executable, "disable.py"] | |
try_install_script(url, new_path, disable_script) | |
else: | |
if os.path.exists(os.path.join(new_path, "enable.py")): | |
enable_script = [sys.executable, "enable.py"] | |
try_install_script(url, new_path, enable_script) | |
except Exception as e: | |
print(f"{action_name}(git-clone) error: {url} / {e}", file=sys.stderr) | |
return False | |
print(f"{action_name} was successful.") | |
return True | |
def gitclone_update(files, instant_execution=False, skip_script=False, msg_prefix=""): | |
import os | |
print(f"{msg_prefix}Update: {files}") | |
for url in files: | |
if url.endswith("/"): | |
url = url[:-1] | |
try: | |
repo_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") | |
repo_path = lookup_installed_custom_nodes(repo_name) | |
if repo_path is None: | |
continue | |
repo_path = repo_path[1] | |
git_pull(repo_path) | |
if not skip_script: | |
if instant_execution: | |
if not execute_install_script(url, repo_path, lazy_mode=False, instant_execution=True): | |
return False | |
else: | |
if not execute_install_script(url, repo_path, lazy_mode=True): | |
return False | |
except Exception as e: | |
print(f"Update(git-clone) error: {url} / {e}", file=sys.stderr) | |
return False | |
if not skip_script: | |
print("Update was successful.") | |
return True | |
def update_path(repo_path, instant_execution=False): | |
if not os.path.exists(os.path.join(repo_path, '.git')): | |
return "fail" | |
# version check | |
repo = git.Repo(repo_path) | |
if repo.head.is_detached: | |
switch_to_default_branch(repo) | |
current_branch = repo.active_branch | |
branch_name = current_branch.name | |
if current_branch.tracking_branch() is None: | |
print(f"[ComfyUI-Manager] There is no tracking branch ({current_branch})") | |
remote_name = 'origin' | |
else: | |
remote_name = current_branch.tracking_branch().remote_name | |
remote = repo.remote(name=remote_name) | |
try: | |
remote.fetch() | |
except Exception as e: | |
if 'detected dubious' in str(e): | |
print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on 'ComfyUI' repository") | |
safedir_path = comfy_path.replace('\\', '/') | |
subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', safedir_path]) | |
try: | |
remote.fetch() | |
except Exception: | |
print(f"\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n" | |
f"-----------------------------------------------------------------------------------------\n" | |
f'git config --global --add safe.directory "{safedir_path}"\n' | |
f"-----------------------------------------------------------------------------------------\n") | |
commit_hash = repo.head.commit.hexsha | |
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha | |
if commit_hash != remote_commit_hash: | |
git_pull(repo_path) | |
execute_install_script("ComfyUI", repo_path, instant_execution=instant_execution) | |
return "updated" | |
else: | |
return "skipped" | |
def lookup_customnode_by_url(data, target): | |
for x in data['custom_nodes']: | |
if target in x['files']: | |
dir_name = os.path.splitext(os.path.basename(target))[0].replace(".git", "") | |
repo_path = lookup_installed_custom_nodes(dir_name) | |
if repo_path is None: | |
continue | |
if repo_path[0]: | |
x['installed'] = 'True' | |
else: | |
x['installed'] = 'Disabled' | |
return x | |
return None | |
def simple_check_custom_node(url): | |
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") | |
repo_path = lookup_installed_custom_nodes(dir_name) | |
if repo_path is None: | |
return 'not-installed' | |
if repo_path[0]: | |
return 'installed' | |
else: | |
return 'disabled' | |
def check_a_custom_node_installed(item, do_fetch=False, do_update_check=True, do_update=False): | |
item['installed'] = 'None' | |
if item['install_type'] == 'git-clone' and len(item['files']) == 1: | |
url = item['files'][0] | |
if url.endswith("/"): | |
url = url[:-1] | |
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") | |
repo_path = lookup_installed_custom_nodes(dir_name) | |
if repo_path is None: | |
item['installed'] = 'False' | |
elif repo_path[0]: | |
dir_path = repo_path[1] | |
try: | |
item['installed'] = 'True' # default | |
if cm_global.try_call(api="cm.is_import_failed_extension", name=dir_name): | |
item['installed'] = 'Fail' | |
if do_update_check: | |
update_state, success = git_repo_has_updates(dir_path, do_fetch, do_update) | |
if (do_update_check or do_update) and update_state: | |
item['installed'] = 'Update' | |
elif do_update and not success: | |
item['installed'] = 'Fail' | |
except: | |
if cm_global.try_call(api="cm.is_import_failed_extension", name=dir_name): | |
item['installed'] = 'Fail' | |
else: | |
item['installed'] = 'True' | |
else: | |
item['installed'] = 'Disabled' | |
elif item['install_type'] == 'copy' and len(item['files']) == 1: | |
dir_name = os.path.basename(item['files'][0]) | |
if item['files'][0].endswith('.py'): | |
base_path = lookup_installed_custom_nodes(item['files'][0]) | |
if base_path is None: | |
item['installed'] = 'False' | |
return | |
elif base_path[0]: | |
item['installed'] = 'True' | |
else: | |
item['installed'] = 'Disabled' | |
return | |
elif 'js_path' in item: | |
base_path = os.path.join(js_path, item['js_path']) | |
else: | |
base_path = js_path | |
file_path = os.path.join(base_path, dir_name) | |
if os.path.exists(file_path): | |
if cm_global.try_call(api="cm.is_import_failed_extension", name=dir_name): | |
item['installed'] = 'Fail' | |
else: | |
item['installed'] = 'True' | |
else: | |
item['installed'] = 'False' | |
def get_installed_pip_packages(): | |
# extract pip package infos | |
pips = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'], text=True).split('\n') | |
res = {} | |
for x in pips: | |
if x.strip() == "": | |
continue | |
if ' @ ' in x: | |
spec_url = x.split(' @ ') | |
res[spec_url[0]] = spec_url[1] | |
else: | |
res[x] = "" | |
return res | |
def get_current_snapshot(): | |
# Get ComfyUI hash | |
repo_path = comfy_path | |
if not os.path.exists(os.path.join(repo_path, '.git')): | |
print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.") | |
return {} | |
repo = git.Repo(repo_path) | |
comfyui_commit_hash = repo.head.commit.hexsha | |
git_custom_nodes = {} | |
file_custom_nodes = [] | |
try: | |
import folder_paths | |
base_paths = folder_paths.get_folder_paths("custom_nodes") | |
except: | |
base_paths = [custom_nodes_path] | |
# Get custom nodes hash | |
for base_path in base_paths: | |
for path in os.listdir(base_path): | |
fullpath = os.path.join(base_path, path) | |
if os.path.isdir(fullpath): | |
is_disabled = path.endswith(".disabled") | |
try: | |
git_dir = os.path.join(fullpath, '.git') | |
if not os.path.exists(git_dir): | |
continue | |
repo = git.Repo(fullpath) | |
commit_hash = repo.head.commit.hexsha | |
url = repo.remotes.origin.url | |
git_custom_nodes[url] = { | |
'hash': commit_hash, | |
'disabled': is_disabled | |
} | |
except: | |
print(f"Failed to extract snapshots for the custom node '{path}'.") | |
elif path.endswith('.py'): | |
is_disabled = path.endswith(".py.disabled") | |
filename = os.path.basename(path) | |
item = { | |
'filename': filename, | |
'disabled': is_disabled | |
} | |
file_custom_nodes.append(item) | |
pip_packages = get_installed_pip_packages() | |
return { | |
'comfyui': comfyui_commit_hash, | |
'git_custom_nodes': git_custom_nodes, | |
'file_custom_nodes': file_custom_nodes, | |
'pips': pip_packages, | |
} | |
def save_snapshot_with_postfix(postfix, path=None): | |
if path is None: | |
now = datetime.now() | |
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S") | |
file_name = f"{date_time_format}_{postfix}" | |
path = os.path.join(comfyui_manager_path, 'snapshots', f"{file_name}.json") | |
else: | |
file_name = path.replace('\\', '/').split('/')[-1] | |
file_name = file_name.split('.')[-2] | |
snapshot = get_current_snapshot() | |
if path.endswith('.json'): | |
with open(path, "w") as json_file: | |
json.dump(snapshot, json_file, indent=4) | |
return file_name + '.json' | |
elif path.endswith('.yaml'): | |
with open(path, "w") as yaml_file: | |
snapshot = {'custom_nodes': snapshot} | |
yaml.dump(snapshot, yaml_file, allow_unicode=True) | |
return path | |
async def extract_nodes_from_workflow(filepath, mode='local', channel_url='default'): | |
# prepare json data | |
workflow = None | |
if filepath.endswith('.json'): | |
with open(filepath, "r", encoding="UTF-8", errors="ignore") as json_file: | |
try: | |
workflow = json.load(json_file) | |
except: | |
print(f"Invalid workflow file: {filepath}") | |
exit(-1) | |
elif filepath.endswith('.png'): | |
from PIL import Image | |
with Image.open(filepath) as img: | |
if 'workflow' not in img.info: | |
print(f"The specified .png file doesn't have a workflow: {filepath}") | |
exit(-1) | |
else: | |
try: | |
workflow = json.loads(img.info['workflow']) | |
except: | |
print(f"This is not a valid .png file containing a ComfyUI workflow: {filepath}") | |
exit(-1) | |
if workflow is None: | |
print(f"Invalid workflow file: {filepath}") | |
exit(-1) | |
# extract nodes | |
used_nodes = set() | |
def extract_nodes(sub_workflow): | |
for x in sub_workflow['nodes']: | |
node_name = x.get('type') | |
# skip virtual nodes | |
if node_name in ['Reroute', 'Note']: | |
continue | |
if node_name is not None and not (node_name.startswith('workflow/') or node_name.startswith('workflow>')): | |
used_nodes.add(node_name) | |
if 'nodes' in workflow: | |
extract_nodes(workflow) | |
if 'extra' in workflow: | |
if 'groupNodes' in workflow['extra']: | |
for x in workflow['extra']['groupNodes'].values(): | |
extract_nodes(x) | |
# lookup dependent custom nodes | |
ext_map = await get_data_by_mode(mode, 'extension-node-map.json', channel_url) | |
rext_map = {} | |
preemption_map = {} | |
patterns = [] | |
for k, v in ext_map.items(): | |
if k == 'https://github.com/comfyanonymous/ComfyUI': | |
for x in v[0]: | |
if x not in preemption_map: | |
preemption_map[x] = [] | |
preemption_map[x] = k | |
continue | |
for x in v[0]: | |
if x not in rext_map: | |
rext_map[x] = [] | |
rext_map[x].append(k) | |
if 'preemptions' in v[1]: | |
for x in v[1]['preemptions']: | |
if x not in preemption_map: | |
preemption_map[x] = [] | |
preemption_map[x] = k | |
if 'nodename_pattern' in v[1]: | |
patterns.append((v[1]['nodename_pattern'], k)) | |
# identify used extensions | |
used_exts = set() | |
unknown_nodes = set() | |
for node_name in used_nodes: | |
ext = preemption_map.get(node_name) | |
if ext is None: | |
ext = rext_map.get(node_name) | |
if ext is not None: | |
ext = ext[0] | |
if ext is None: | |
for pat_ext in patterns: | |
if re.search(pat_ext[0], node_name): | |
ext = pat_ext[1] | |
break | |
if ext == 'https://github.com/comfyanonymous/ComfyUI': | |
pass | |
elif ext is not None: | |
if 'Fooocus' in ext: | |
print(f">> {node_name}") | |
used_exts.add(ext) | |
else: | |
unknown_nodes.add(node_name) | |
return used_exts, unknown_nodes | |
def unzip(model_path): | |
if not os.path.exists(model_path): | |
print(f"[ComfyUI-Manager] unzip: File not found: {model_path}") | |
return False | |
base_dir = os.path.dirname(model_path) | |
filename = os.path.basename(model_path) | |
target_dir = os.path.join(base_dir, filename[:-4]) | |
os.makedirs(target_dir, exist_ok=True) | |
with zipfile.ZipFile(model_path, 'r') as zip_ref: | |
zip_ref.extractall(target_dir) | |
# Check if there's only one directory inside the target directory | |
contents = os.listdir(target_dir) | |
if len(contents) == 1 and os.path.isdir(os.path.join(target_dir, contents[0])): | |
nested_dir = os.path.join(target_dir, contents[0]) | |
# Move each file and sub-directory in the nested directory up to the target directory | |
for item in os.listdir(nested_dir): | |
shutil.move(os.path.join(nested_dir, item), os.path.join(target_dir, item)) | |
# Remove the now empty nested directory | |
os.rmdir(nested_dir) | |
os.remove(model_path) | |
return True | |