diff --git a/comfyui_manager/common/config_writer.py b/comfyui_manager/common/config_writer.py new file mode 100644 index 000000000..d811830ff --- /dev/null +++ b/comfyui_manager/common/config_writer.py @@ -0,0 +1,76 @@ +"""Shared settings-file writer for glob and legacy.""" +import configparser +import os +import tempfile + +from rich import print + + +class DirtyTrackingConfig(dict): + """Track keys assigned after loading the startup configuration.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.dirty_keys = set() + + def __setitem__(self, key, value): + self.dirty_keys.add(key) + super().__setitem__(key, value) + + def update(self, *args, **kwargs): + incoming = dict(*args, **kwargs) + self.dirty_keys.update(incoming) + super().update(incoming) + + +def write_config_merged(config_path, cached, written_config_keys): + """Merge changed owned keys onto the latest settings on disk. + + Preserve parsed keys and values, not INI comments or formatting.""" + config = configparser.ConfigParser(strict=False) + try: + loaded = config.read(config_path) + except (configparser.Error, UnicodeDecodeError) as e: + # Preserve the existing fallback for malformed files. + print(f"[ComfyUI-Manager] Warning: '{config_path}' could not be parsed ({e}); rewriting it from the current settings.") + config = configparser.ConfigParser(strict=False) + else: + # ConfigParser.read silently skips unreadable files; do not overwrite one. + if not loaded and os.path.exists(config_path): + raise OSError(f"'{config_path}' exists but could not be read; refusing to overwrite it with default settings. Fix the file's permissions, then retry.") + + if config.has_section('default'): + keys = [key for key in written_config_keys if key in cached.dirty_keys] + else: + config['default'] = {} + keys = list(written_config_keys) + + section = config['default'] + for key in keys: + section[key] = str(cached[key]).replace('\r', '').replace('\n', '').replace('\x00', '') + + directory = os.path.dirname(config_path) + if directory and not os.path.exists(directory): + os.makedirs(directory) + + # Replace from the same directory so readers never see a partial write. + tmp_path = None + try: + with tempfile.NamedTemporaryFile( + 'w', dir=directory or '.', prefix='.config-', suffix='.tmp', delete=False + ) as configfile: + tmp_path = configfile.name + config.write(configfile) + configfile.flush() + os.fsync(configfile.fileno()) + # Retain the existing mode instead of the temporary file's 0600. + if os.path.exists(config_path): + os.chmod(tmp_path, os.stat(config_path).st_mode & 0o777) + os.replace(tmp_path, config_path) + tmp_path = None + finally: + if tmp_path is not None and os.path.exists(tmp_path): + os.remove(tmp_path) + + # Clear only persisted keys, and only after a successful write. + cached.dirty_keys.difference_update(keys) diff --git a/comfyui_manager/common/security_messages.py b/comfyui_manager/common/security_messages.py new file mode 100644 index 000000000..785fad2e7 --- /dev/null +++ b/comfyui_manager/common/security_messages.py @@ -0,0 +1,7 @@ +"""Installation-denial messages shared by the glob and legacy servers.""" + +SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy" +SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy" +SECURITY_MESSAGE_HIGH_P = "ERROR: To use this action, '--listen' must be set to a local IP and security_level must be 'normal-' or lower. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy" +SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy" +SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower." diff --git a/comfyui_manager/glob/constants.py b/comfyui_manager/glob/constants.py index fa3c10911..a21fc12f2 100644 --- a/comfyui_manager/glob/constants.py +++ b/comfyui_manager/glob/constants.py @@ -1,12 +1,3 @@ - -SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" -SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" -SECURITY_MESSAGE_HIGH_P = "ERROR: To use this action, '--listen' must be set to a local IP and security_level must be 'normal-' or lower. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" -SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" -SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy" -SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower." - - def is_loopback(address): import ipaddress diff --git a/comfyui_manager/glob/manager_core.py b/comfyui_manager/glob/manager_core.py index 7bf758a13..8f1b84d93 100644 --- a/comfyui_manager/glob/manager_core.py +++ b/comfyui_manager/glob/manager_core.py @@ -35,6 +35,7 @@ from packaging import version import uuid from ..common import cm_global +from ..common.config_writer import DirtyTrackingConfig, write_config_merged from ..common import cnr_utils from ..common import manager_util from ..common import git_utils @@ -1686,41 +1687,32 @@ class ManagerFuncs: manager_funcs = ManagerFuncs() +# Settings owned by this server; other keys and sections are preserved. +WRITTEN_CONFIG_KEYS = ( + 'git_exe', + 'use_uv', + 'use_unified_resolver', + 'channel_url', + 'share_option', + 'bypass_ssl', + 'file_logging', + 'update_policy', + 'windows_selector_event_loop_policy', + 'model_download_by_agent', + 'downgrade_blacklist', + 'security_level', + 'always_lazy_install', + 'network_mode', + 'db_mode', + 'verbose', + 'allow_git_url_install', + 'allow_pip_install', +) + + def write_config(): - config = configparser.ConfigParser(strict=False) - - config['default'] = { - 'git_exe': get_config()['git_exe'], - 'use_uv': get_config()['use_uv'], - 'use_unified_resolver': get_config()['use_unified_resolver'], - 'channel_url': get_config()['channel_url'], - 'share_option': get_config()['share_option'], - 'bypass_ssl': get_config()['bypass_ssl'], - "file_logging": get_config()['file_logging'], - 'update_policy': get_config()['update_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'], - 'always_lazy_install': get_config()['always_lazy_install'], - 'network_mode': get_config()['network_mode'], - 'db_mode': get_config()['db_mode'], - 'verbose': get_config()['verbose'], - 'allow_git_url_install': get_config()['allow_git_url_install'], - 'allow_pip_install': get_config()['allow_pip_install'], - } - - # Sanitize all string values to prevent CRLF injection attacks - for key, value in config['default'].items(): - if isinstance(value, str): - config['default'][key] = value.replace('\r', '').replace('\n', '').replace('\x00', '') - - directory = os.path.dirname(context.manager_config_path) - if not os.path.exists(directory): - os.makedirs(directory) - - with open(context.manager_config_path, 'w') as configfile: - config.write(configfile) + """Persist changed settings through the shared writer.""" + write_config_merged(context.manager_config_path, get_config(), WRITTEN_CONFIG_KEYS) def read_config(): @@ -1796,7 +1788,8 @@ def get_config(): global cached_config if cached_config is None: - cached_config = read_config() + # Start tracking changes after the startup configuration is loaded. + cached_config = DirtyTrackingConfig(read_config()) if cached_config['http_channel_enabled']: print("[ComfyUI-Manager] Warning: http channel enabled, make sure server in secure env") diff --git a/comfyui_manager/glob/manager_server.py b/comfyui_manager/glob/manager_server.py index 6a5408746..93121901c 100644 --- a/comfyui_manager/glob/manager_server.py +++ b/comfyui_manager/glob/manager_server.py @@ -83,12 +83,14 @@ from ..data_models import ( ComfyUISwitchVersionParams, ) -from .constants import ( - model_dir_name_map, +from ..common.security_messages import ( SECURITY_MESSAGE_MIDDLE, SECURITY_MESSAGE_MIDDLE_P, SECURITY_MESSAGE_HIGH_P, ) +from .constants import ( + model_dir_name_map, +) if not manager_util.is_manager_pip_package(): network_mode_description = "offline" diff --git a/comfyui_manager/glob/utils/formatting_utils.py b/comfyui_manager/glob/utils/formatting_utils.py index 357112ebd..e4396f7a5 100644 --- a/comfyui_manager/glob/utils/formatting_utils.py +++ b/comfyui_manager/glob/utils/formatting_utils.py @@ -1,6 +1,5 @@ import locale import sys -import re def handle_stream(stream, prefix): @@ -20,41 +19,3 @@ def handle_stream(stream, prefix): print(prefix, msg, end="", file=sys.stderr) else: print(prefix, msg, end="") - - -def convert_markdown_to_html(input_text): - pattern_a = re.compile(r"\[a/([^]]+)]\(([^)]+)\)") - pattern_w = re.compile(r"\[w/([^]]+)]") - pattern_i = re.compile(r"\[i/([^]]+)]") - pattern_bold = re.compile(r"\*\*([^*]+)\*\*") - pattern_white = re.compile(r"%%([^*]+)%%") - - def replace_a(match): - return f"{match.group(1)}" - - def replace_w(match): - return f"

{match.group(1)}

" - - def replace_i(match): - return f"

{match.group(1)}

" - - def replace_bold(match): - return f"{match.group(1)}" - - def replace_white(match): - return f"{match.group(1)}" - - input_text = ( - input_text.replace("\\[", "[") - .replace("\\]", "]") - .replace("<", "<") - .replace(">", ">") - ) - - result_text = re.sub(pattern_a, replace_a, input_text) - result_text = re.sub(pattern_w, replace_w, result_text) - result_text = re.sub(pattern_i, replace_i, result_text) - result_text = re.sub(pattern_bold, replace_bold, result_text) - result_text = re.sub(pattern_white, replace_white, result_text) - - return result_text.replace("\n", "
") diff --git a/comfyui_manager/js/comfyui-manager.js b/comfyui_manager/js/comfyui-manager.js index afb15bafe..7110f1adc 100644 --- a/comfyui_manager/js/comfyui-manager.js +++ b/comfyui_manager/js/comfyui-manager.js @@ -14,7 +14,7 @@ import { OpenArtShareDialog } from "./comfyui-share-openart.js"; import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, setManagerInstance, show_message, customAlert, customPrompt, - infoToast, showTerminal, setNeedRestart, generateUUID + infoToast, showTerminal, setNeedRestart, generateUUID, sanitizeHTML, sanitizeUrl } from "./common.js"; import { CustomNodesManager } from "./custom-nodes-manager.js"; import { ModelManager } from "./model-manager.js"; @@ -722,13 +722,14 @@ async function onQueueStatus(event) { msg += "The following custom nodes have been updated:"; @@ -741,13 +742,13 @@ async function onQueueStatus(event) { msg += '
The update for the following custom nodes has failed: