diff --git a/glob/manager_core.py b/glob/manager_core.py index e6070537..79e8e8e0 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, 11, 3] +version_code = [3, 12] version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') diff --git a/glob/manager_server.py b/glob/manager_server.py index b276ef6d..5a8c19bd 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -18,6 +18,8 @@ import manager_core as core import manager_util import cm_global import logging +import asyncio +import queue logging.info(f"### Loading: ComfyUI-Manager ({core.version_str})") @@ -31,7 +33,6 @@ SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this secu routes = PromptServer.instance.routes - def handle_stream(stream, prefix): stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') for msg in stream: @@ -368,6 +369,147 @@ def nickname_filter(json_obj): return json_obj +install_queue = queue.Queue() +install_result = {} + +async def install_worker(): + global install_result + global install_queue + + async def do_install(item): + ui_id, node_spec_str, channel, mode, skip_post_install = item + + try: + node_spec = core.unified_manager.resolve_node_spec(node_spec_str) + + if node_spec is None: + logging.error(f"Cannot resolve install target: '{node_spec_str}'") + install_result[ui_id] = f"Cannot resolve install target: '{node_spec_str}'" + return + + node_name, version_spec, is_specified = node_spec + res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install) + # discard post install if skip_post_install mode + + if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']: + logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}") + install_result[ui_id] = res.msg + return + + elif not res.result: + logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}") + install_result[ui_id] = res.msg + return + + install_result[ui_id] = 'success' + except Exception: + traceback.print_exc() + install_result[ui_id] = f"Installation failed:\n{node_spec_str}" + + async def do_update(item): + ui_id, node_name, node_ver = item + + try: + res = core.unified_manager.unified_update(node_name, node_ver) + + manager_util.clear_pip_cache() + + if res.result: + install_result[ui_id] = 'success' + return + + logging.error(f"\nERROR: An error occurred while updating '{node_name}'.") + install_result[ui_id] = f"An error occurred while updating '{node_name}'." + except Exception: + traceback.print_exc() + install_result[ui_id] = f"An error occurred while updating '{node_name}'." + + async def do_fix(item): + ui_id, node_name, node_ver = item + + try: + res = core.unified_manager.unified_fix(node_name, node_ver) + + if res.result: + install_result[ui_id] = 'success' + return + else: + logging.error(res.msg) + + logging.error(f"\nERROR: An error occurred while fixing '{node_name}@{node_ver}'.") + install_result[ui_id] = f"An error occurred while fixing '{node_name}@{node_ver}'." + except Exception: + traceback.print_exc() + install_result[ui_id] = f"An error occurred while fixing '{node_name}@{node_ver}'." + + async def do_uninstall(item): + ui_id, node_name, is_unknown = item + + try: + res = core.unified_manager.unified_uninstall(node_name, is_unknown) + + if res.result: + install_result[ui_id] = 'success' + return + + logging.error(f"\nERROR: An error occurred while uninstalling '{node_name}'.") + install_result[ui_id] = f"An error occurred while uninstalling '{node_name}'." + except Exception: + traceback.print_exc() + install_result[ui_id] = f"An error occurred while uninstalling '{node_name}'." + + async def do_disable(item): + ui_id, node_name, is_unknown = item + + try: + res = core.unified_manager.unified_disable(node_name, is_unknown) + + if res: + install_result[ui_id] = 'success' + return + + install_result[ui_id] = f"Failed to disable: '{node_name}'" + except Exception: + traceback.print_exc() + install_result[ui_id] = f"Failed to disable: '{node_name}'" + + stats = {} + + while True: + done_count = len(install_result) + total_count = done_count + install_queue.qsize() + + if install_queue.empty(): + logging.info(f"\n[ComfyUI-Manager] Queued works are completed.\n{stats}") + + logging.info("\nAfter restarting ComfyUI, please refresh the browser.") + PromptServer.instance.send_sync("cm-install-status", + {'status': 'done', 'result': install_result, + 'total_count': total_count, 'done_count': done_count}) + install_result = {} + install_queue = queue.Queue() + return + + kind, item = install_queue.get() + + if kind == 'install': + await do_install(item) + elif kind == 'update': + await do_update(item) + elif kind == 'fix': + await do_fix(item) + elif kind == 'uninstall': + await do_uninstall(item) + elif kind == 'disable': + await do_disable(item) + + stats[kind] = stats.get(kind, 0) + 1 + + PromptServer.instance.send_sync("cm-install-status", + {'status': 'in_progress', 'target': item[0], + 'total_count': total_count, 'done_count': done_count}) + + @routes.get("/customnode/getmappings") async def fetch_customnode_mappings(request): """ @@ -870,7 +1012,24 @@ async def reinstall_custom_node(request): await install_custom_node(request) -@routes.post("/customnode/install") +@routes.get("/customnode/queue/reset") +async def reset_queue(request): + global install_queue + install_queue = queue.Queue() + return web.Response(status=200) + + +@routes.get("/customnode/queue/count") +async def reset_queue(request): + global install_queue + + done_count = len(install_result) + total_count = done_count + install_queue.qsize() + + return web.json_response({'total_count': total_count, 'done_count': done_count}) + + +@routes.post("/customnode/queue/install") async def install_custom_node(request): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) @@ -913,26 +1072,23 @@ async def install_custom_node(request): logging.error(SECURITY_MESSAGE_GENERAL) return web.Response(status=404, text="A security error has occurred. Please check the terminal logs") - node_spec = core.unified_manager.resolve_node_spec(node_spec_str) + install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install + install_queue.put(("install", install_item)) - if node_spec is None: - return web.Response(status=400, text=f"Cannot resolve install target: '{node_spec_str}'") - - node_name, version_spec, is_specified = node_spec - res = await core.unified_manager.install_by_id(node_name, version_spec, json_data['channel'], json_data['mode'], return_postinstall=skip_post_install) - # discard post install if skip_post_install mode - - if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']: - logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}") - return web.Response(status=400, text=res.msg) - elif not res.result: - logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}") - return web.Response(status=400, text=res.msg) - - return web.Response(status=200, text="Installation success.") + return web.Response(status=200) -@routes.post("/customnode/fix") +@routes.get("/customnode/queue/start") +async def queue_start(request): + global install_result + install_result = {} + + threading.Thread(target=lambda: asyncio.run(install_worker())).start() + + return web.Response(status=200) + + +@routes.post("/customnode/queue/fix") async def fix_custom_node(request): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_GENERAL) @@ -948,16 +1104,10 @@ async def fix_custom_node(request): # unknown node_name = os.path.basename(json_data['files'][0]) - res = core.unified_manager.unified_fix(node_name, node_ver) + update_item = json_data.get('ui_id'), node_name, json_data['version'] + install_queue.put(("fix", update_item)) - if res.result: - logging.info("\nAfter restarting ComfyUI, please refresh the browser.") - return web.json_response({}, content_type='application/json') - else: - logging.error(res.msg) - - logging.error(f"\nERROR: An error occurred while fixing '{node_name}@{node_ver}'.") - return web.Response(status=400, text=f"An error occurred while fixing '{node_name}@{node_ver}'.") + return web.Response(status=200) @routes.post("/customnode/install/git_url") @@ -992,7 +1142,7 @@ async def install_custom_node_pip(request): return web.Response(status=200) -@routes.post("/customnode/uninstall") +@routes.post("/customnode/queue/uninstall") async def uninstall_custom_node(request): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) @@ -1009,17 +1159,13 @@ async def uninstall_custom_node(request): is_unknown = True node_name = os.path.basename(json_data['files'][0]) - res = core.unified_manager.unified_uninstall(node_name, is_unknown) + uninstall_item = json_data.get('ui_id'), node_name, is_unknown + install_queue.put(("uninstall", uninstall_item)) - if res.result: - logging.info("\nAfter restarting ComfyUI, please refresh the browser.") - return web.json_response({}, content_type='application/json') - - logging.error(f"\nERROR: An error occurred while uninstalling '{node_name}'.") - return web.Response(status=400, text=f"An error occurred while uninstalling '{node_name}'.") + return web.Response(status=200) -@routes.post("/customnode/update") +@routes.post("/customnode/queue/update") async def update_custom_node(request): if not is_allowed_security_level('middle'): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) @@ -1034,16 +1180,10 @@ async def update_custom_node(request): # unknown node_name = os.path.basename(json_data['files'][0]) - res = core.unified_manager.unified_update(node_name, json_data['version']) + update_item = json_data.get('ui_id'), node_name, json_data['version'] + install_queue.put(("update", update_item)) - manager_util.clear_pip_cache() - - if res.result: - logging.info("\nAfter restarting ComfyUI, please refresh the browser.") - return web.json_response({}, content_type='application/json') - - logging.error(f"\nERROR: An error occurred while updating '{node_name}'.") - return web.Response(status=400, text=f"An error occurred while updating '{node_name}'.") + return web.Response(status=200) @routes.get("/comfyui_manager/update_comfyui") @@ -1092,7 +1232,7 @@ async def comfyui_switch_version(request): return web.Response(status=400) -@routes.post("/customnode/disable") +@routes.post("/customnode/queue/disable") async def disable_node(request): json_data = await request.json() @@ -1105,12 +1245,10 @@ async def disable_node(request): is_unknown = True node_name = os.path.basename(json_data['files'][0]) - res = core.unified_manager.unified_disable(node_name, is_unknown) + update_item = json_data.get('ui_id'), node_name, is_unknown + install_queue.put(("disable", update_item)) - if res: - return web.json_response({}, content_type='application/json') - - return web.Response(status=400, text="Failed to disable") + return web.Response(status=200) @routes.get("/manager/migrate_unmanaged_nodes") @@ -1149,37 +1287,44 @@ async def install_model(request): logging.error(SECURITY_MESSAGE_NORMAL_MINUS) return web.Response(status=403) - res = False + def do_install(): + res = False - try: - if model_path is not None: + try: + if model_path is not None: - model_url = json_data['url'] - logging.info(f"Install model '{json_data['name']}' from '{model_url}' into '{model_path}'") - if not core.get_config()['model_download_by_agent'] and ( - model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')): - model_dir = get_model_dir(json_data, True) - download_url(model_url, model_dir, filename=json_data['filename']) - if model_path.endswith('.zip'): - res = core.unzip(model_path) + model_url = json_data['url'] + logging.info(f"Install model '{json_data['name']}' from '{model_url}' into '{model_path}'") + if not core.get_config()['model_download_by_agent'] and ( + model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')): + model_dir = get_model_dir(json_data, True) + download_url(model_url, model_dir, filename=json_data['filename']) + if model_path.endswith('.zip'): + res = core.unzip(model_path) + else: + res = True + + if res: + return web.json_response({}, content_type='application/json') else: - res = True - - if res: - return web.json_response({}, content_type='application/json') + res = download_url_with_agent(model_url, model_path) + if res and model_path.endswith('.zip'): + res = core.unzip(model_path) else: - res = download_url_with_agent(model_url, model_path) - if res and model_path.endswith('.zip'): - res = core.unzip(model_path) - else: - logging.error(f"Model installation error: invalid model type - {json_data['type']}") + logging.error(f"Model installation error: invalid model type - {json_data['type']}") - if res: - return web.json_response({}, content_type='application/json') - except Exception as e: - logging.error(f"[ERROR] {e}", file=sys.stderr) + if res: + return web.json_response({}, content_type='application/json') + except Exception as e: + logging.error(f"[ERROR] {e}", file=sys.stderr) + return web.Response(status=400) - return web.Response(status=400) + # Run the installation in a thread pool + with concurrent.futures.ThreadPoolExecutor() as executor: + + asyncio.get_event_loop().run_in_executor(executor, do_install) + + return web.Response(status=200) @routes.get("/manager/preview_method") @@ -1408,8 +1553,6 @@ def confirm_try_install(sender, custom_node_url, msg): cm_global.register_api('cm.try-install-custom-node', confirm_try_install) -import asyncio - async def default_cache_update(): async def get_cache(filename): diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index 4e5fe988..36845167 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -17,7 +17,6 @@ import { import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js"; import { CustomNodesManager } from "./custom-nodes-manager.js"; import { ModelManager } from "./model-manager.js"; -import { set_double_click_policy } from "./node_fixer.js"; import { SnapshotManager } from "./snapshot.js"; var docStyle = document.createElement('style'); diff --git a/js/common.js b/js/common.js index 2597ea66..f2e09a92 100644 --- a/js/common.js +++ b/js/common.js @@ -130,6 +130,20 @@ export function customAlert(message) { } } +export function infoToast(summary, message) { + try { + app.extensionManager.toast.add({ + severity: 'info', + summary: summary, + detail: message, + life: 3000 + }) + } + catch { + // do nothing + } +} + export async function customPrompt(title, message) { try { diff --git a/js/custom-nodes-manager.js b/js/custom-nodes-manager.js index 2996be1c..f562878d 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, sanitizeHTML + fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt, sanitizeHTML, infoToast } from "./common.js"; // https://cenfun.github.io/turbogrid/api.html @@ -391,6 +391,8 @@ export class CustomNodesManager { this.restartMap = {}; this.init(); + + api.addEventListener("cm-install-status", this.onInstallStatus); } init() { @@ -1204,7 +1206,7 @@ export class CustomNodesManager { } focusInstall(item, mode) { - const cellNode = this.grid.getCellNode(item, "installed"); + const cellNode = this.grid.getCellNode(item, "action"); if (cellNode) { const cellBtn = cellNode.querySelector(`button[mode="${mode}"]`); if (cellBtn) { @@ -1269,6 +1271,13 @@ export class CustomNodesManager { } async installNodes(list, btn, title, selected_version) { + let stats = await api.fetchApi('/customnode/queue/count'); + stats = await stats.json(); + if(stats.total_count > 0) { + customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`); + return; + } + const { target, label, mode} = btn; if(mode === "uninstall") { @@ -1294,8 +1303,13 @@ export class CustomNodesManager { let needRestart = false; let errorMsg = ""; + + await api.fetchApi('/customnode/queue/reset'); + this.install_context = btn; + for (const hash of list) { const item = this.grid.getRowItemBy("hash", hash); + if (!item) { errorMsg = `Not found custom node: ${hash}`; break; @@ -1315,6 +1329,7 @@ export class CustomNodesManager { data.selected_version = selected_version; data.channel = this.channel; data.mode = this.mode; + data.ui_id = hash; let install_mode = mode; if(mode == 'switch') { @@ -1332,14 +1347,14 @@ export class CustomNodesManager { api_mode = 'reinstall'; } - const res = await api.fetchApi(`/customnode/${api_mode}`, { + const res = await api.fetchApi(`/customnode/queue/${api_mode}`, { method: 'POST', body: JSON.stringify(data) }); if (res.status != 200) { - errorMsg = `${item.title} ${mode} failed: `; + if(res.status == 403) { errorMsg += `This action is not allowed with this security level configuration.`; } else if(res.status == 404) { @@ -1350,32 +1365,69 @@ export class CustomNodesManager { break; } - - needRestart = true; - - this.grid.setRowSelected(item, false); - item.restart = true; - this.restartMap[item.hash] = true; - this.grid.updateCell(item, "action"); - - //console.log(res.data); - } - target.classList.remove("cn-btn-loading"); - - if (errorMsg) { + if(errorMsg) { this.showError(errorMsg); show_message("Installation Error:\n"+errorMsg); + } + else { + await api.fetchApi('/customnode/queue/start'); + } + } + + async onInstallStatus(event) { + let self = CustomNodesManager.instance; + if(event.detail.status == 'in_progress') { + const hash = event.detail.target; + + const item = self.grid.getRowItemBy("hash", hash); + + item.restart = true; + self.restartMap[item.hash] = true; + self.grid.updateCell(item, "action"); + } + else if(event.detail.status == 'done') { + self.onInstallCompleted(event.detail); + } + } + + async onInstallCompleted(info) { + let result = info.result; + + let self = CustomNodesManager.instance; + + if(!self.install_context) { + return; + } + + const { target, label, mode } = self.install_context; + target.classList.remove("cn-btn-loading"); + + let errorMsg = ""; + + for(let hash in result){ + let v = result[hash]; + + const item = self.grid.getRowItemBy("hash", hash); + self.grid.setRowSelected(item, false); + + if(v != 'success') + errorMsg += v; + } + + if (errorMsg) { + self.showError(errorMsg); + show_message("Installation Error:\n"+errorMsg); } else { - this.showStatus(`${label} ${list.length} custom node(s) successfully`); + self.showStatus(`${label} ${result.length} custom node(s) successfully`); } - if (needRestart) { - this.showRestart(); - this.showMessage(`To apply the installed/updated/disabled/enabled custom node, please restart ComfyUI. And refresh browser.`, "red") - } + self.showRestart(); + self.showMessage(`To apply the installed/updated/disabled/enabled custom node, please restart ComfyUI. And refresh browser.`, "red"); + infoToast(`[ComfyUI-Manager] All tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`); + self.install_context = undefined; } // =========================================================================================== diff --git a/js/node_fixer.js b/js/node_fixer.js index 9cb0dac6..feec1cad 100644 --- a/js/node_fixer.js +++ b/js/node_fixer.js @@ -1,16 +1,6 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; -let double_click_policy = "copy-all"; - -api.fetchApi('/manager/dbl_click/policy') - .then(response => response.text()) - .then(data => set_double_click_policy(data)); - -export function set_double_click_policy(mode) { - double_click_policy = mode; -} - function addMenuHandler(nodeType, cb) { const getOpts = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function () { @@ -153,62 +143,6 @@ function node_info_copy(src, dest, connect_both, copy_shape) { app.registerExtension({ name: "Comfy.Manager.NodeFixer", - - async nodeCreated(node, app) { - let orig_dblClick = node.onDblClick; - node.onDblClick = function (e, pos, self) { - orig_dblClick?.apply?.(this, arguments); - - if((!node.inputs && !node.outputs) || pos[1] > 0) - return; - - switch(double_click_policy) { - case "copy-all": - case "copy-full": - case "copy-input": - { - if(node.inputs?.some(x => x.link != null) || node.outputs?.some(x => x.links != null && x.links.length > 0) ) - return; - - let src_node = lookup_nearest_nodes(node); - if(src_node) - { - let both_connection = double_click_policy != "copy-input"; - let copy_shape = double_click_policy == "copy-full"; - node_info_copy(src_node, node, both_connection, copy_shape); - } - } - break; - case "possible-input": - { - let nearest_inputs = lookup_nearest_inputs(node); - if(nearest_inputs) - connect_inputs(nearest_inputs, node); - } - break; - case "dual": - { - if(pos[0] < node.size[0]/2) { - // left: possible-input - let nearest_inputs = lookup_nearest_inputs(node); - if(nearest_inputs) - connect_inputs(nearest_inputs, node); - } - else { - // right: copy-all - if(node.inputs?.some(x => x.link != null) || node.outputs?.some(x => x.links != null && x.links.length > 0) ) - return; - - let src_node = lookup_nearest_nodes(node); - if(src_node) - node_info_copy(src_node, node, true); - } - } - break; - } - } - }, - beforeRegisterNodeDef(nodeType, nodeData, app) { addMenuHandler(nodeType, function (_, options) { options.push({ diff --git a/pyproject.toml b/pyproject.toml index 92584a22..989564ef 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.11.3" +version = "3.12" license = { file = "LICENSE.txt" } dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"]