From 0eb1cbce43238e98db3d402bb920b5e7d7024a63 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 18 Jan 2025 13:04:33 +0900 Subject: [PATCH] feat: provide error messages for `import failed` custom node. --- glob/cm_global.py | 3 ++ glob/manager_core.py | 22 +++++++---- glob/manager_server.py | 18 +++++++++ glob/manager_util.py | 2 +- js/common.js | 9 +++++ js/custom-nodes-manager.js | 71 ++++++++++++++++++++++++++++++----- prestartup_script.py | 77 +++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- 8 files changed, 184 insertions(+), 20 deletions(-) diff --git a/glob/cm_global.py b/glob/cm_global.py index 4041bcb6..118d475b 100644 --- a/glob/cm_global.py +++ b/glob/cm_global.py @@ -110,3 +110,6 @@ def add_on_revision_detected(k, f): traceback.print_exc() else: variables['cm.on_revision_detected_handler'].append((k, f)) + + +error_dict = {} \ No newline at end of file diff --git a/glob/manager_core.py b/glob/manager_core.py index fa780ee9..220ea9bb 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -41,7 +41,7 @@ import manager_downloader from node_package import InstalledNodePackage -version_code = [3, 7, 6] +version_code = [3, 8] version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') @@ -122,7 +122,6 @@ def check_invalid_nodes(): if subdir in ['.disabled', '__pycache__']: continue - package = unified_manager.installed_node_packages.get(subdir) if not package: continue @@ -368,6 +367,16 @@ class UnifiedManager: self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json self.processed_install = set() + def get_module_name(self, x): + info = self.active_nodes.get(x) + if info is None: + for url, fullpath in self.unknown_active_nodes.values(): + if url == x: + return os.path.basename(fullpath) + else: + return os.path.basename(info[1]) + + return None def get_cnr_by_repo(self, url): return self.repo_cnr_map.get(git_utils.normalize_url(url)) @@ -501,8 +510,8 @@ class UnifiedManager: self.installed_node_packages[node_package.id] = node_package if node_package.is_disabled and node_package.is_unknown: - # NOTE: unknown package does not have an url. - self.unknown_inactive_nodes[node_package.id] = ('', node_package.fullpath) + url = git_utils.git_url(node_package.fullpath) + self.unknown_inactive_nodes[node_package.id] = (url, node_package.fullpath) if node_package.is_disabled and node_package.is_nightly: self.nightly_inactive_nodes[node_package.id] = node_package.fullpath @@ -511,8 +520,8 @@ class UnifiedManager: self.active_nodes[node_package.id] = node_package.version, node_package.fullpath if node_package.is_enabled and node_package.is_unknown: - # NOTE: unknown package does not have an url. - self.unknown_active_nodes[node_package.id] = ('', node_package.fullpath) + url = git_utils.git_url(node_package.fullpath) + self.unknown_active_nodes[node_package.id] = (url, node_package.fullpath) if node_package.is_from_cnr and node_package.is_disabled: self.add_to_cnr_inactive_nodes(node_package.id, node_package.version, node_package.fullpath) @@ -726,7 +735,6 @@ class UnifiedManager: # default_channel = normalize_channel('default') # cache = self.custom_node_map_cache.get((default_channel, mode)) # CNR/nightly should always be based on the default channel. - channel = normalize_channel(channel) cache = self.custom_node_map_cache.get((channel, mode)) # CNR/nightly should always be based on the default channel. diff --git a/glob/manager_server.py b/glob/manager_server.py index ef625242..99b36e5d 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -839,6 +839,23 @@ async def get_disabled_versions(request): return web.Response(status=400) +@routes.post("/customnode/import_fail_info") +async def import_fail_info(request): + json_data = await request.json() + + if 'cnr_id' in json_data: + module_name = core.unified_manager.get_module_name(json_data['cnr_id']) + else: + module_name = core.unified_manager.get_module_name(json_data['url']) + + if module_name is not None: + info = cm_global.error_dict.get(module_name) + if info is not None: + return web.json_response(info) + + return web.Response(status=400) + + @routes.post("/customnode/reinstall") async def reinstall_custom_node(request): await uninstall_custom_node(request) @@ -1437,3 +1454,4 @@ cm_global.register_extension('ComfyUI-Manager', 'nodes': {}, 'description': 'This extension provides the ability to manage custom nodes in ComfyUI.', }) + diff --git a/glob/manager_util.py b/glob/manager_util.py index 0cfa8da8..7ab6b625 100644 --- a/glob/manager_util.py +++ b/glob/manager_util.py @@ -125,7 +125,7 @@ async def get_data(uri, silent=False): json_obj = json.loads(json_text) if not silent: - logging.info(" [DONE]") + print(" [DONE]") return json_obj diff --git a/js/common.js b/js/common.js index ace1885a..2597ea66 100644 --- a/js/common.js +++ b/js/common.js @@ -397,3 +397,12 @@ export const icons = { passed: '', download: '' } + +export function sanitizeHTML(str) { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} \ No newline at end of file diff --git a/js/custom-nodes-manager.js b/js/custom-nodes-manager.js index 7ad6e9b4..e147c34a 100644 --- a/js/custom-nodes-manager.js +++ b/js/custom-nodes-manager.js @@ -4,7 +4,7 @@ import { api } from "../../scripts/api.js"; import { manager_instance, rebootAPI, install_via_git_url, - fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt + fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt, sanitizeHTML } from "./common.js"; // https://cenfun.github.io/turbogrid/api.html @@ -250,6 +250,13 @@ const pageCss = ` color: white; } +.cn-manager .cn-btn-import-failed { + background-color: #AA1111; + font-size: 10px; + font-weight: bold; + color: white; +} + .cn-manager .cn-btn-install { background-color: black; color: white; @@ -872,6 +879,38 @@ export class CustomNodesManager { return this.filter === ShowMode.ALTERNATIVES } + async handleImportFail(rowItem) { + var info; + if(rowItem.version == 'unknown'){ + info = { + 'url': rowItem.originalData.files[0] + }; + } + else{ + info = { + 'cnr_id': rowItem.originalData.id + }; + } + + const response = await api.fetchApi(`/customnode/import_fail_info`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(info) + }); + + let res = await response.json(); + + let title = `Error message occurred while importing the '${rowItem.title}' module.


` + + if(res.code == 400) + { + show_message(title+'The information is not available.') + } + else { + show_message(title+sanitizeHTML(res['msg']).replace(/ /g, ' ').replace(/\n/g, '
')); + } + } + renderGrid() { // update theme @@ -905,6 +944,7 @@ export class CustomNodesManager { } } + let self = this; const columns = [{ id: 'id', name: 'ID', @@ -918,16 +958,29 @@ export class CustomNodesManager { maxWidth: 500, classMap: 'cn-node-name', formatter: (title, rowItem, columnItem) => { - var prefix = ''; - if(rowItem.action === 'invalid-installation') { - prefix = '(INVALID)'; - } + const container = document.createElement('div'); - else if(rowItem.action === 'import-fail') { - prefix = '(IMPORT FAILED)'; - } + if (rowItem.action === 'invalid-installation') { + const invalidTag = document.createElement('span'); + invalidTag.style.color = 'red'; + invalidTag.innerHTML = '(INVALID)'; + container.appendChild(invalidTag); + } else if (rowItem.action === 'import-fail') { + const button = document.createElement('button'); + button.className = 'cn-btn-import-failed'; + button.innerText = 'IMPORT FAILED ↗'; + button.onclick = () => self.handleImportFail(rowItem); + container.appendChild(button); + container.appendChild(document.createElement('br')); + } - return `${prefix}${title}`; + const link = document.createElement('a'); + link.href = rowItem.reference; + link.target = '_blank'; + link.innerHTML = `${title}`; + container.appendChild(link); + + return container; } }, { id: 'version', diff --git a/prestartup_script.py b/prestartup_script.py index 2ee76874..249eeb9b 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -9,6 +9,7 @@ import platform import json import ast import logging +import traceback glob_path = os.path.join(os.path.dirname(__file__), "glob") sys.path.append(glob_path) @@ -146,6 +147,48 @@ def process_wrap(cmd_str, cwd_path, handler=None, env=None): return process.wait() +original_stdout = sys.stdout + + +def try_get_custom_nodes(x): + for custom_nodes_dir in folder_paths.get_folder_paths('custom_nodes'): + if x.startswith(custom_nodes_dir): + relative_path = os.path.relpath(x, custom_nodes_dir) + next_segment = relative_path.split(os.sep)[0] + if next_segment.lower() != 'comfyui-manager': + return next_segment, os.path.join(custom_nodes_dir, next_segment) + return None + + +def extract_origin_module(): + stack = traceback.extract_stack()[:-2] + for frame in reversed(stack): + info = try_get_custom_nodes(frame.filename) + if info is None: + continue + else: + return info + return None + +def extract_origin_module_from_strings(file_paths): + for filepath in file_paths: + info = try_get_custom_nodes(filepath) + if info is None: + continue + else: + return info + return None + + +def finalize_startup(): + res = {} + for k, v in cm_global.error_dict.items(): + if v['path'] in import_failed_extensions: + res[k] = v + + cm_global.error_dict = res + + try: if '--port' in sys.argv: port_index = sys.argv.index('--port') @@ -225,8 +268,16 @@ try: if match: import_failed_extensions.add(match.group(1).strip()) - if 'Starting server' in message: - is_start_mode = False + if not self.is_stdout: + origin_info = extract_origin_module() + if origin_info is not None: + name, origin_path = origin_info + + if name != 'comfyui-manager': + if name not in cm_global.error_dict: + cm_global.error_dict[name] = {'name': name, 'path': origin_path, 'msg': ''} + + cm_global.error_dict[name]['msg'] += message if not self.is_stdout: match = re.search(pat_tqdm, message) @@ -311,12 +362,34 @@ try: if match: import_failed_extensions.add(match.group(1).strip()) + if 'Traceback' in message: + file_lists = self._extract_file_paths(message) + origin_info = extract_origin_module_from_strings(file_lists) + if origin_info is not None: + name, origin_path = origin_info + + if name != 'comfyui-manager': + if name not in cm_global.error_dict: + cm_global.error_dict[name] = {'name': name, 'path': origin_path, 'msg': ''} + + cm_global.error_dict[name]['msg'] += message + if 'Starting server' in message: is_start_mode = False + finalize_startup() if stderr_wrapper: stderr_wrapper.sync_write(message+'\n', file_only=True) + def _extract_file_paths(self, msg): + file_paths = [] + for line in msg.split('\n'): + match = re.findall(r'File \"(.*?)\", line \d+', line) + for x in match: + if not x.startswith('<'): + file_paths.extend(match) + return file_paths + logging.getLogger().addHandler(LoggingHandler()) diff --git a/pyproject.toml b/pyproject.toml index 1d93c1e8..199f9103 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-manager" description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI." -version = "3.7.6" +version = "3.8" license = { file = "LICENSE.txt" } dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"]