Skip to content

Commit affc3e7

Browse files
committed
fix: Windows git clone failures — URL reinstall + pipe deadlock + file lock
Three fixes for Windows E2E failures: 1. cm_cli reinstall_node(): URL-based node specs used the full URL as lookup key, but internal dicts are keyed by repo basename or cnr_id. Use get_cnr_by_repo() for CNR-aware lookup with correct is_unknown flag. 2. git_helper.py gitclone(): disable tqdm progress when stderr is piped (sys.stderr.isatty() gate) to prevent pipe buffer deadlock. Also move stale directories from previous failed clones into .disabled/.trash/ before cloning (GitPython handle leak on Windows). 3. try_rmtree(): 3-tier deletion strategy for Windows file locks: retry 3x with delay, rename into .disabled/.trash/, then lazy-delete via reserve_script as final fallback.
1 parent 099aed1 commit affc3e7

5 files changed

Lines changed: 93 additions & 20 deletions

File tree

cm_cli/__main__.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -238,18 +238,29 @@ def install_node(node_spec_str, is_all=False, cnt_msg='', **kwargs):
238238

239239

240240
def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):
241-
node_spec = unified_manager.resolve_node_spec(node_spec_str)
242-
243-
node_name, version_spec, _ = node_spec
241+
if core.is_valid_url(node_spec_str):
242+
# URL-based: resolve_node_spec returns the full URL as node_name,
243+
# but internal dicts are keyed by repo basename or cnr_id.
244+
url = node_spec_str.rstrip('/')
245+
cnr = unified_manager.get_cnr_by_repo(url)
246+
if cnr:
247+
node_id = cnr['id']
248+
unified_manager.unified_uninstall(node_id, False)
249+
unified_manager.purge_node_state(node_id)
250+
else:
251+
repo_name = os.path.splitext(os.path.basename(url))[0]
252+
unified_manager.unified_uninstall(repo_name, True)
253+
unified_manager.purge_node_state(repo_name)
244254

245-
# Best-effort uninstall via normal path
246-
unified_manager.unified_uninstall(node_name, version_spec == 'unknown')
255+
install_node(node_spec_str, is_all=is_all, cnt_msg=cnt_msg, raise_on_fail=True)
256+
else:
257+
node_spec = unified_manager.resolve_node_spec(node_spec_str)
258+
node_name, version_spec, _ = node_spec
247259

248-
# Fallback: purge all state and directories regardless of categorization
249-
# Handles categorization mismatch between cm_cli invocations (e.g. unknown→nightly)
250-
unified_manager.purge_node_state(node_name)
260+
unified_manager.unified_uninstall(node_name, version_spec == 'unknown')
261+
unified_manager.purge_node_state(node_name)
251262

252-
install_node(node_name, is_all=is_all, cnt_msg=cnt_msg, raise_on_fail=True)
263+
install_node(node_name, is_all=is_all, cnt_msg=cnt_msg, raise_on_fail=True)
253264

254265

255266
def fix_node(node_spec_str, is_all=False, cnt_msg=''):

comfyui_manager/common/git_helper.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,24 @@ def gitclone(custom_nodes_path, url, target_hash=None, repo_path=None):
100100
if repo_path is None:
101101
repo_path = os.path.join(custom_nodes_path, repo_name)
102102

103-
# Clone the repository from the remote URL
104-
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress())
103+
# On Windows, previous failed clones may leave directories with locked
104+
# .git/objects/pack/* files (GitPython memory-mapped handle leak).
105+
# Rename stale directory out of the way so clone can proceed.
106+
if os.path.exists(repo_path):
107+
import shutil
108+
import uuid as _uuid
109+
trash_dir = os.path.join(custom_nodes_path, '.disabled', '.trash')
110+
os.makedirs(trash_dir, exist_ok=True)
111+
trash = os.path.join(trash_dir, repo_name + f'_{_uuid.uuid4().hex[:8]}')
112+
try:
113+
os.rename(repo_path, trash)
114+
shutil.rmtree(trash, ignore_errors=True)
115+
except OSError:
116+
shutil.rmtree(repo_path, ignore_errors=True)
117+
118+
# Disable tqdm progress when stderr is piped to avoid deadlock on Windows.
119+
progress = GitProgress() if sys.stderr.isatty() else None
120+
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=progress)
105121

106122
if target_hash is not None:
107123
print(f"CHECKOUT: {repo_name} [{target_hash}]")

comfyui_manager/glob/manager_core.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,11 +1854,34 @@ def reserve_script(repo_path, install_cmds):
18541854

18551855

18561856
def try_rmtree(title, fullpath):
1857+
# Tier 1: retry with delay for transient Windows file locks
1858+
for attempt in range(3):
1859+
try:
1860+
shutil.rmtree(fullpath)
1861+
return
1862+
except OSError:
1863+
if attempt < 2:
1864+
time.sleep(1)
1865+
1866+
# Tier 2: rename into .disabled/.trash/ so scanner ignores it
1867+
trash_dir = os.path.join(os.path.dirname(fullpath), '.disabled', '.trash')
1868+
os.makedirs(trash_dir, exist_ok=True)
1869+
trash = os.path.join(trash_dir, os.path.basename(fullpath) + f'_{uuid.uuid4().hex[:8]}')
18571870
try:
1858-
shutil.rmtree(fullpath)
1859-
except Exception as e:
1860-
logging.warning(f"[ComfyUI-Manager] An error occurred while deleting '{fullpath}', so it has been scheduled for deletion upon restart.\nEXCEPTION: {e}")
1861-
reserve_script(title, ["#LAZY-DELETE-NODEPACK", fullpath])
1871+
os.rename(fullpath, trash)
1872+
shutil.rmtree(trash, ignore_errors=True)
1873+
if not os.path.exists(trash):
1874+
return
1875+
# Rename succeeded but delete failed — schedule trash path for lazy delete
1876+
logging.warning(f"[ComfyUI-Manager] Renamed '{fullpath}' to '{trash}' but could not delete; scheduled for restart.")
1877+
reserve_script(title, ["#LAZY-DELETE-NODEPACK", trash])
1878+
return
1879+
except OSError:
1880+
pass
1881+
1882+
# Tier 3: lazy delete on restart (ComfyUI GUI fallback)
1883+
logging.warning(f"[ComfyUI-Manager] An error occurred while deleting '{fullpath}', so it has been scheduled for deletion upon restart.")
1884+
reserve_script(title, ["#LAZY-DELETE-NODEPACK", fullpath])
18621885

18631886

18641887
def try_install_script(url, repo_path, install_cmd, instant_execution=False):

comfyui_manager/legacy/manager_core.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,11 +1833,34 @@ def reserve_script(repo_path, install_cmds):
18331833

18341834

18351835
def try_rmtree(title, fullpath):
1836+
# Tier 1: retry with delay for transient Windows file locks
1837+
for attempt in range(3):
1838+
try:
1839+
shutil.rmtree(fullpath)
1840+
return
1841+
except OSError:
1842+
if attempt < 2:
1843+
time.sleep(1)
1844+
1845+
# Tier 2: rename into .disabled/.trash/ so scanner ignores it
1846+
trash_dir = os.path.join(os.path.dirname(fullpath), '.disabled', '.trash')
1847+
os.makedirs(trash_dir, exist_ok=True)
1848+
trash = os.path.join(trash_dir, os.path.basename(fullpath) + f'_{uuid.uuid4().hex[:8]}')
18361849
try:
1837-
shutil.rmtree(fullpath)
1838-
except Exception as e:
1839-
logging.warning(f"[ComfyUI-Manager] An error occurred while deleting '{fullpath}', so it has been scheduled for deletion upon restart.\nEXCEPTION: {e}")
1840-
reserve_script(title, ["#LAZY-DELETE-NODEPACK", fullpath])
1850+
os.rename(fullpath, trash)
1851+
shutil.rmtree(trash, ignore_errors=True)
1852+
if not os.path.exists(trash):
1853+
return
1854+
# Rename succeeded but delete failed — schedule trash path for lazy delete
1855+
logging.warning(f"[ComfyUI-Manager] Renamed '{fullpath}' to '{trash}' but could not delete; scheduled for restart.")
1856+
reserve_script(title, ["#LAZY-DELETE-NODEPACK", trash])
1857+
return
1858+
except OSError:
1859+
pass
1860+
1861+
# Tier 3: lazy delete on restart (ComfyUI GUI fallback)
1862+
logging.warning(f"[ComfyUI-Manager] An error occurred while deleting '{fullpath}', so it has been scheduled for deletion upon restart.")
1863+
reserve_script(title, ["#LAZY-DELETE-NODEPACK", fullpath])
18411864

18421865

18431866
def try_install_script(url, repo_path, install_cmd, instant_execution=False):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "comfyui-manager"
77
license = { text = "GPL-3.0-only" }
8-
version = "4.1b7"
8+
version = "4.1b8"
99
requires-python = ">= 3.9"
1010
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
1111
readme = "README.md"

0 commit comments

Comments
 (0)