comfy-env 0.1.15__py3-none-any.whl → 0.1.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfy_env/__init__.py +117 -40
- comfy_env/cli.py +122 -311
- comfy_env/config/__init__.py +12 -4
- comfy_env/config/parser.py +30 -79
- comfy_env/config/types.py +37 -0
- comfy_env/detection/__init__.py +77 -0
- comfy_env/detection/cuda.py +61 -0
- comfy_env/detection/gpu.py +230 -0
- comfy_env/detection/platform.py +70 -0
- comfy_env/detection/runtime.py +103 -0
- comfy_env/environment/__init__.py +53 -0
- comfy_env/environment/cache.py +141 -0
- comfy_env/environment/libomp.py +41 -0
- comfy_env/environment/paths.py +38 -0
- comfy_env/environment/setup.py +91 -0
- comfy_env/install.py +134 -331
- comfy_env/isolation/__init__.py +32 -2
- comfy_env/isolation/tensor_utils.py +83 -0
- comfy_env/isolation/workers/__init__.py +16 -0
- comfy_env/{workers → isolation/workers}/mp.py +1 -1
- comfy_env/{workers → isolation/workers}/subprocess.py +1 -1
- comfy_env/isolation/wrap.py +128 -509
- comfy_env/packages/__init__.py +60 -0
- comfy_env/packages/apt.py +36 -0
- comfy_env/packages/cuda_wheels.py +97 -0
- comfy_env/packages/node_dependencies.py +77 -0
- comfy_env/packages/pixi.py +85 -0
- comfy_env/packages/toml_generator.py +88 -0
- comfy_env-0.1.17.dist-info/METADATA +225 -0
- comfy_env-0.1.17.dist-info/RECORD +36 -0
- comfy_env/cache.py +0 -203
- comfy_env/nodes.py +0 -187
- comfy_env/pixi/__init__.py +0 -48
- comfy_env/pixi/core.py +0 -587
- comfy_env/pixi/cuda_detection.py +0 -303
- comfy_env/pixi/platform/__init__.py +0 -21
- comfy_env/pixi/platform/base.py +0 -96
- comfy_env/pixi/platform/darwin.py +0 -53
- comfy_env/pixi/platform/linux.py +0 -68
- comfy_env/pixi/platform/windows.py +0 -284
- comfy_env/pixi/resolver.py +0 -198
- comfy_env/prestartup.py +0 -208
- comfy_env/workers/__init__.py +0 -38
- comfy_env/workers/tensor_utils.py +0 -188
- comfy_env-0.1.15.dist-info/METADATA +0 -291
- comfy_env-0.1.15.dist-info/RECORD +0 -31
- /comfy_env/{workers → isolation/workers}/base.py +0 -0
- {comfy_env-0.1.15.dist-info → comfy_env-0.1.17.dist-info}/WHEEL +0 -0
- {comfy_env-0.1.15.dist-info → comfy_env-0.1.17.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.1.15.dist-info → comfy_env-0.1.17.dist-info}/licenses/LICENSE +0 -0
comfy_env/isolation/wrap.py
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Process isolation for ComfyUI node packs.
|
|
3
|
-
|
|
4
|
-
This module provides wrap_isolated_nodes() which wraps node classes
|
|
5
|
-
to run their FUNCTION methods in an isolated Python environment.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
# In your node pack's __init__.py:
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from comfy_env import wrap_isolated_nodes
|
|
11
|
-
|
|
12
|
-
NODE_CLASS_MAPPINGS = {}
|
|
13
|
-
|
|
14
|
-
# Main nodes (no isolation)
|
|
15
|
-
from .nodes.main import NODE_CLASS_MAPPINGS as main_nodes
|
|
16
|
-
NODE_CLASS_MAPPINGS.update(main_nodes)
|
|
17
|
-
|
|
18
|
-
# Isolated nodes (has comfy-env.toml in that directory)
|
|
19
|
-
from .nodes.isolated import NODE_CLASS_MAPPINGS as isolated_nodes
|
|
20
|
-
NODE_CLASS_MAPPINGS.update(
|
|
21
|
-
wrap_isolated_nodes(isolated_nodes, Path(__file__).parent / "nodes/isolated")
|
|
22
|
-
)
|
|
23
|
-
"""
|
|
1
|
+
"""Process isolation for ComfyUI nodes - wraps FUNCTION methods to run in isolated env."""
|
|
24
2
|
|
|
25
3
|
import atexit
|
|
4
|
+
import glob
|
|
26
5
|
import inspect
|
|
27
6
|
import os
|
|
28
7
|
import sys
|
|
@@ -31,529 +10,194 @@ from functools import wraps
|
|
|
31
10
|
from pathlib import Path
|
|
32
11
|
from typing import Any, Dict, Optional
|
|
33
12
|
|
|
34
|
-
# Debug logging (set COMFY_ENV_DEBUG=1 to enable)
|
|
35
13
|
_DEBUG = os.environ.get("COMFY_ENV_DEBUG", "").lower() in ("1", "true", "yes")
|
|
14
|
+
_workers: Dict[str, Any] = {}
|
|
15
|
+
_workers_lock = threading.Lock()
|
|
36
16
|
|
|
37
17
|
|
|
38
|
-
def
|
|
39
|
-
"""
|
|
40
|
-
name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
|
|
41
|
-
return f"_env_{name}"
|
|
18
|
+
def _is_enabled() -> bool:
|
|
19
|
+
return os.environ.get("USE_COMFY_ENV", "1").lower() not in ("0", "false", "no", "off")
|
|
42
20
|
|
|
43
|
-
# Global worker cache (one per isolated environment)
|
|
44
|
-
_workers: Dict[str, Any] = {}
|
|
45
|
-
_workers_lock = threading.Lock()
|
|
46
21
|
|
|
22
|
+
def _env_name(dir_name: str) -> str:
|
|
23
|
+
return f"_env_{dir_name.lower().replace('-', '_').lstrip('comfyui_')}"
|
|
47
24
|
|
|
48
|
-
|
|
49
|
-
|
|
25
|
+
|
|
26
|
+
def _get_env_paths(env_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
|
|
27
|
+
"""Get (site_packages, lib_dir) from env."""
|
|
50
28
|
if sys.platform == "win32":
|
|
51
|
-
|
|
29
|
+
sp = env_dir / "Lib" / "site-packages"
|
|
30
|
+
lib = env_dir / "Library" / "bin"
|
|
52
31
|
else:
|
|
53
|
-
|
|
32
|
+
matches = glob.glob(str(env_dir / "lib/python*/site-packages"))
|
|
33
|
+
sp = Path(matches[0]) if matches else None
|
|
34
|
+
lib = env_dir / "lib"
|
|
35
|
+
return (sp, lib) if sp and sp.exists() else (None, None)
|
|
54
36
|
|
|
55
|
-
if not python_path.exists():
|
|
56
|
-
return None
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
38
|
+
def _find_env_dir(node_dir: Path) -> Optional[Path]:
|
|
39
|
+
"""Find env dir: marker -> _env_<name> -> .pixi -> .venv"""
|
|
40
|
+
marker = node_dir / ".comfy-env-marker.toml"
|
|
41
|
+
if marker.exists():
|
|
42
|
+
try:
|
|
43
|
+
import tomli
|
|
44
|
+
with open(marker, "rb") as f:
|
|
45
|
+
env_path = tomli.load(f).get("env", {}).get("path")
|
|
46
|
+
if env_path and Path(env_path).exists():
|
|
47
|
+
return Path(env_path)
|
|
48
|
+
except Exception: pass
|
|
49
|
+
|
|
50
|
+
for candidate in [node_dir / _env_name(node_dir.name),
|
|
51
|
+
node_dir / ".pixi/envs/default",
|
|
52
|
+
node_dir / ".venv"]:
|
|
53
|
+
if candidate.exists(): return candidate
|
|
68
54
|
return None
|
|
69
55
|
|
|
70
56
|
|
|
71
|
-
def
|
|
72
|
-
env_dir
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
57
|
+
def _get_python_version(env_dir: Path) -> Optional[str]:
|
|
58
|
+
python = env_dir / ("python.exe" if sys.platform == "win32" else "bin/python")
|
|
59
|
+
if not python.exists(): return None
|
|
60
|
+
try:
|
|
61
|
+
import subprocess
|
|
62
|
+
r = subprocess.run([str(python), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
|
|
63
|
+
capture_output=True, text=True, timeout=5)
|
|
64
|
+
return r.stdout.strip() if r.returncode == 0 else None
|
|
65
|
+
except Exception: return None
|
|
66
|
+
|
|
80
67
|
|
|
68
|
+
def _get_worker(env_dir: Path, working_dir: Path, sys_path: list[str],
|
|
69
|
+
lib_path: Optional[str] = None, env_vars: Optional[dict] = None):
|
|
70
|
+
cache_key = str(env_dir)
|
|
81
71
|
with _workers_lock:
|
|
82
|
-
if cache_key in _workers:
|
|
83
|
-
|
|
84
|
-
if worker.is_alive():
|
|
85
|
-
return worker
|
|
86
|
-
# Worker died, will recreate
|
|
87
|
-
|
|
88
|
-
# Check if Python versions match
|
|
89
|
-
host_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
90
|
-
isolated_version = _get_isolated_python_version(env_dir)
|
|
91
|
-
|
|
92
|
-
if isolated_version and isolated_version != host_version:
|
|
93
|
-
# Different Python version - must use SubprocessWorker
|
|
94
|
-
from ..workers.subprocess import SubprocessWorker
|
|
95
|
-
|
|
96
|
-
if sys.platform == "win32":
|
|
97
|
-
python_path = env_dir / "python.exe"
|
|
98
|
-
else:
|
|
99
|
-
python_path = env_dir / "bin" / "python"
|
|
100
|
-
|
|
101
|
-
print(f"[comfy-env] Starting isolated worker (SubprocessWorker)")
|
|
102
|
-
print(f"[comfy-env] Python: {python_path} ({isolated_version} vs host {host_version})")
|
|
103
|
-
|
|
104
|
-
worker = SubprocessWorker(
|
|
105
|
-
python=str(python_path),
|
|
106
|
-
working_dir=working_dir,
|
|
107
|
-
sys_path=sys_path,
|
|
108
|
-
name=working_dir.name,
|
|
109
|
-
)
|
|
110
|
-
else:
|
|
111
|
-
# Same Python version - use MPWorker (faster)
|
|
112
|
-
from ..workers.mp import MPWorker
|
|
72
|
+
if cache_key in _workers and _workers[cache_key].is_alive():
|
|
73
|
+
return _workers[cache_key]
|
|
113
74
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if env_vars:
|
|
117
|
-
print(f"[comfy-env] env_vars: {', '.join(f'{k}={v}' for k, v in env_vars.items())}")
|
|
75
|
+
host_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
76
|
+
iso_ver = _get_python_version(env_dir)
|
|
118
77
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
78
|
+
if iso_ver and iso_ver != host_ver:
|
|
79
|
+
from .workers.subprocess import SubprocessWorker
|
|
80
|
+
python = env_dir / ("python.exe" if sys.platform == "win32" else "bin/python")
|
|
81
|
+
print(f"[comfy-env] SubprocessWorker: {python} ({iso_ver} vs {host_ver})")
|
|
82
|
+
worker = SubprocessWorker(python=str(python), working_dir=working_dir, sys_path=sys_path, name=working_dir.name)
|
|
83
|
+
else:
|
|
84
|
+
from .workers.mp import MPWorker
|
|
85
|
+
print(f"[comfy-env] MPWorker: {env_dir}")
|
|
86
|
+
worker = MPWorker(name=working_dir.name, sys_path=sys_path, lib_path=lib_path, env_vars=env_vars)
|
|
125
87
|
|
|
126
88
|
_workers[cache_key] = worker
|
|
127
89
|
return worker
|
|
128
90
|
|
|
129
91
|
|
|
92
|
+
@atexit.register
|
|
130
93
|
def _shutdown_workers():
|
|
131
|
-
"""Shutdown all cached workers. Called at exit."""
|
|
132
94
|
with _workers_lock:
|
|
133
|
-
for
|
|
134
|
-
try:
|
|
135
|
-
|
|
136
|
-
except Exception:
|
|
137
|
-
pass
|
|
95
|
+
for w in _workers.values():
|
|
96
|
+
try: w.shutdown()
|
|
97
|
+
except Exception: pass
|
|
138
98
|
_workers.clear()
|
|
139
99
|
|
|
140
100
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _find_env_paths(node_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
|
|
145
|
-
"""
|
|
146
|
-
Find site-packages and lib directories for the isolated environment.
|
|
147
|
-
|
|
148
|
-
Fallback order:
|
|
149
|
-
1. Marker file -> central cache
|
|
150
|
-
2. _env_<name> (local)
|
|
151
|
-
3. .pixi/envs/default (old pixi)
|
|
152
|
-
4. .venv
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
(site_packages, lib_dir) - lib_dir is for LD_LIBRARY_PATH
|
|
156
|
-
"""
|
|
157
|
-
import glob
|
|
158
|
-
|
|
159
|
-
def _get_paths_from_env(env_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
|
|
160
|
-
"""Extract site-packages and lib_dir from an env directory."""
|
|
161
|
-
if sys.platform == "win32":
|
|
162
|
-
site_packages = env_dir / "Lib" / "site-packages"
|
|
163
|
-
lib_dir = env_dir / "Library" / "bin"
|
|
164
|
-
else:
|
|
165
|
-
pattern = str(env_dir / "lib" / "python*" / "site-packages")
|
|
166
|
-
matches = glob.glob(pattern)
|
|
167
|
-
site_packages = Path(matches[0]) if matches else None
|
|
168
|
-
lib_dir = env_dir / "lib"
|
|
169
|
-
if site_packages and site_packages.exists():
|
|
170
|
-
return site_packages, lib_dir if lib_dir and lib_dir.exists() else None
|
|
171
|
-
return None, None
|
|
172
|
-
|
|
173
|
-
# 1. Check marker file -> central cache
|
|
174
|
-
marker_path = node_dir / ".comfy-env-marker.toml"
|
|
175
|
-
if marker_path.exists():
|
|
176
|
-
try:
|
|
177
|
-
import tomli
|
|
178
|
-
with open(marker_path, "rb") as f:
|
|
179
|
-
marker = tomli.load(f)
|
|
180
|
-
env_path = marker.get("env", {}).get("path")
|
|
181
|
-
if env_path:
|
|
182
|
-
env_dir = Path(env_path)
|
|
183
|
-
if env_dir.exists():
|
|
184
|
-
result = _get_paths_from_env(env_dir)
|
|
185
|
-
if result[0]:
|
|
186
|
-
return result
|
|
187
|
-
except Exception:
|
|
188
|
-
pass # Fall through to other options
|
|
189
|
-
|
|
190
|
-
# 2. Check _env_<name> directory (local)
|
|
191
|
-
env_name = get_env_name(node_dir.name)
|
|
192
|
-
env_dir = node_dir / env_name
|
|
193
|
-
if env_dir.exists():
|
|
194
|
-
result = _get_paths_from_env(env_dir)
|
|
195
|
-
if result[0]:
|
|
196
|
-
return result
|
|
197
|
-
|
|
198
|
-
# 3. Fallback: Check old .pixi/envs/default (for backward compat)
|
|
199
|
-
pixi_env = node_dir / ".pixi" / "envs" / "default"
|
|
200
|
-
if pixi_env.exists():
|
|
201
|
-
result = _get_paths_from_env(pixi_env)
|
|
202
|
-
if result[0]:
|
|
203
|
-
return result
|
|
204
|
-
|
|
205
|
-
# 4. Check .venv directory
|
|
206
|
-
venv_dir = node_dir / ".venv"
|
|
207
|
-
if venv_dir.exists():
|
|
208
|
-
if sys.platform == "win32":
|
|
209
|
-
site_packages = venv_dir / "Lib" / "site-packages"
|
|
210
|
-
else:
|
|
211
|
-
pattern = str(venv_dir / "lib" / "python*" / "site-packages")
|
|
212
|
-
matches = glob.glob(pattern)
|
|
213
|
-
site_packages = Path(matches[0]) if matches else None
|
|
214
|
-
if site_packages and site_packages.exists():
|
|
215
|
-
return site_packages, None # venvs don't have separate lib
|
|
216
|
-
|
|
217
|
-
return None, None
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _find_env_dir(node_dir: Path) -> Optional[Path]:
|
|
221
|
-
"""
|
|
222
|
-
Find the environment directory (for cache key).
|
|
223
|
-
|
|
224
|
-
Fallback order:
|
|
225
|
-
1. Marker file -> central cache
|
|
226
|
-
2. _env_<name> (local)
|
|
227
|
-
3. .pixi/envs/default (old pixi)
|
|
228
|
-
4. .venv
|
|
229
|
-
"""
|
|
230
|
-
# 1. Check marker file -> central cache
|
|
231
|
-
marker_path = node_dir / ".comfy-env-marker.toml"
|
|
232
|
-
if marker_path.exists():
|
|
233
|
-
try:
|
|
234
|
-
import tomli
|
|
235
|
-
with open(marker_path, "rb") as f:
|
|
236
|
-
marker = tomli.load(f)
|
|
237
|
-
env_path = marker.get("env", {}).get("path")
|
|
238
|
-
if env_path:
|
|
239
|
-
env_dir = Path(env_path)
|
|
240
|
-
if env_dir.exists():
|
|
241
|
-
return env_dir
|
|
242
|
-
except Exception:
|
|
243
|
-
pass
|
|
244
|
-
|
|
245
|
-
# 2. Check _env_<name> first
|
|
246
|
-
env_name = get_env_name(node_dir.name)
|
|
247
|
-
env_dir = node_dir / env_name
|
|
248
|
-
if env_dir.exists():
|
|
249
|
-
return env_dir
|
|
250
|
-
|
|
251
|
-
# 3. Fallback to old .pixi path
|
|
252
|
-
pixi_env = node_dir / ".pixi" / "envs" / "default"
|
|
253
|
-
if pixi_env.exists():
|
|
254
|
-
return pixi_env
|
|
255
|
-
|
|
256
|
-
# 4. Check .venv
|
|
257
|
-
venv_dir = node_dir / ".venv"
|
|
258
|
-
if venv_dir.exists():
|
|
259
|
-
return venv_dir
|
|
260
|
-
|
|
261
|
-
return None
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _find_custom_node_root(nodes_dir: Path) -> Optional[Path]:
|
|
265
|
-
"""
|
|
266
|
-
Find the custom node root (direct child of custom_nodes/).
|
|
267
|
-
|
|
268
|
-
Uses folder_paths to find custom_nodes directories, then finds
|
|
269
|
-
which one is an ancestor of nodes_dir.
|
|
270
|
-
|
|
271
|
-
Example: /path/custom_nodes/ComfyUI-UniRig/nodes/nodes_gpu
|
|
272
|
-
-> returns /path/custom_nodes/ComfyUI-UniRig
|
|
273
|
-
"""
|
|
274
|
-
try:
|
|
275
|
-
import folder_paths
|
|
276
|
-
custom_nodes_dirs = folder_paths.get_folder_paths("custom_nodes")
|
|
277
|
-
except (ImportError, KeyError):
|
|
278
|
-
return None
|
|
279
|
-
|
|
280
|
-
for cn_dir in custom_nodes_dirs:
|
|
281
|
-
cn_path = Path(cn_dir)
|
|
282
|
-
try:
|
|
283
|
-
rel = nodes_dir.relative_to(cn_path)
|
|
284
|
-
if rel.parts:
|
|
285
|
-
return cn_path / rel.parts[0]
|
|
286
|
-
except ValueError:
|
|
287
|
-
continue
|
|
288
|
-
|
|
289
|
-
return None
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def _wrap_node_class(
|
|
293
|
-
cls: type,
|
|
294
|
-
env_dir: Path,
|
|
295
|
-
working_dir: Path,
|
|
296
|
-
sys_path: list[str],
|
|
297
|
-
lib_path: Optional[str] = None,
|
|
298
|
-
env_vars: Optional[dict] = None,
|
|
299
|
-
) -> type:
|
|
300
|
-
"""
|
|
301
|
-
Wrap a node class so its FUNCTION method runs in the isolated environment.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
cls: The node class to wrap
|
|
305
|
-
env_dir: Path to the isolated environment directory
|
|
306
|
-
working_dir: Working directory for the worker
|
|
307
|
-
sys_path: Additional paths to add to sys.path in the worker
|
|
308
|
-
lib_path: Path to add to LD_LIBRARY_PATH for conda libraries
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
The wrapped class (modified in place)
|
|
312
|
-
"""
|
|
101
|
+
def _wrap_node_class(cls: type, env_dir: Path, working_dir: Path, sys_path: list[str],
|
|
102
|
+
lib_path: Optional[str] = None, env_vars: Optional[dict] = None) -> type:
|
|
313
103
|
func_name = getattr(cls, "FUNCTION", None)
|
|
314
|
-
if not func_name:
|
|
315
|
-
|
|
104
|
+
if not func_name: return cls
|
|
105
|
+
original = getattr(cls, func_name, None)
|
|
106
|
+
if not original: return cls
|
|
316
107
|
|
|
317
|
-
original_method = getattr(cls, func_name, None)
|
|
318
|
-
if original_method is None:
|
|
319
|
-
return cls
|
|
320
|
-
|
|
321
|
-
# Get source file for the class
|
|
322
108
|
try:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
109
|
+
source = Path(inspect.getfile(cls)).resolve()
|
|
110
|
+
module_name = str(source.relative_to(working_dir).with_suffix("")).replace("/", ".").replace("\\", ".")
|
|
111
|
+
except (TypeError, OSError, ValueError):
|
|
112
|
+
module_name = source.stem if 'source' in dir() else cls.__module__
|
|
327
113
|
|
|
328
|
-
|
|
329
|
-
# e.g., /path/to/nodes/io/load_mesh.py -> nodes.io.load_mesh
|
|
330
|
-
try:
|
|
331
|
-
relative_path = source_file.relative_to(working_dir)
|
|
332
|
-
# Convert path to module: nodes/io/load_mesh.py -> nodes.io.load_mesh
|
|
333
|
-
module_name = str(relative_path.with_suffix("")).replace("/", ".").replace("\\", ".")
|
|
334
|
-
except ValueError:
|
|
335
|
-
# File not under working_dir, use stem as fallback
|
|
336
|
-
module_name = source_file.stem
|
|
337
|
-
|
|
338
|
-
@wraps(original_method)
|
|
114
|
+
@wraps(original)
|
|
339
115
|
def proxy(self, **kwargs):
|
|
340
|
-
if _DEBUG:
|
|
341
|
-
print(f"[comfy-env] PROXY CALLED: {cls.__name__}.{func_name}", flush=True)
|
|
342
|
-
print(f"[comfy-env] kwargs keys: {list(kwargs.keys())}", flush=True)
|
|
343
|
-
|
|
344
116
|
worker = _get_worker(env_dir, working_dir, sys_path, lib_path, env_vars)
|
|
345
|
-
if _DEBUG:
|
|
346
|
-
print(f"[comfy-env] worker alive: {worker.is_alive()}", flush=True)
|
|
347
|
-
|
|
348
|
-
# Clone tensors for IPC if needed
|
|
349
117
|
try:
|
|
350
|
-
from
|
|
351
|
-
|
|
118
|
+
from .tensor_utils import prepare_for_ipc_recursive
|
|
352
119
|
kwargs = {k: prepare_for_ipc_recursive(v) for k, v in kwargs.items()}
|
|
353
|
-
except ImportError:
|
|
354
|
-
pass # No torch available, skip cloning
|
|
120
|
+
except ImportError: pass
|
|
355
121
|
|
|
356
|
-
if _DEBUG:
|
|
357
|
-
print(f"[comfy-env] calling worker.call_method...", flush=True)
|
|
358
122
|
result = worker.call_method(
|
|
359
|
-
module_name=module_name,
|
|
360
|
-
class_name=cls.__name__,
|
|
361
|
-
method_name=func_name,
|
|
123
|
+
module_name=module_name, class_name=cls.__name__, method_name=func_name,
|
|
362
124
|
self_state=self.__dict__.copy() if hasattr(self, "__dict__") else None,
|
|
363
|
-
kwargs=kwargs,
|
|
364
|
-
timeout=600.0,
|
|
125
|
+
kwargs=kwargs, timeout=600.0,
|
|
365
126
|
)
|
|
366
|
-
if _DEBUG:
|
|
367
|
-
print(f"[comfy-env] call_method returned", flush=True)
|
|
368
127
|
|
|
369
|
-
# Clone result tensors
|
|
370
128
|
try:
|
|
371
|
-
from
|
|
372
|
-
|
|
129
|
+
from .tensor_utils import prepare_for_ipc_recursive
|
|
373
130
|
result = prepare_for_ipc_recursive(result)
|
|
374
|
-
except ImportError:
|
|
375
|
-
pass
|
|
376
|
-
|
|
131
|
+
except ImportError: pass
|
|
377
132
|
return result
|
|
378
133
|
|
|
379
|
-
# Replace the method
|
|
380
134
|
setattr(cls, func_name, proxy)
|
|
381
|
-
|
|
382
|
-
# Mark as isolated for debugging
|
|
383
135
|
cls._comfy_env_isolated = True
|
|
384
|
-
|
|
385
136
|
return cls
|
|
386
137
|
|
|
387
138
|
|
|
388
|
-
def _is_comfy_env_enabled() -> bool:
|
|
389
|
-
"""Check if comfy-env isolation is enabled (default: True)."""
|
|
390
|
-
val = os.environ.get("USE_COMFY_ENV", "1").lower()
|
|
391
|
-
return val not in ("0", "false", "no", "off")
|
|
392
|
-
|
|
393
|
-
|
|
394
139
|
def wrap_nodes() -> None:
|
|
395
|
-
"""
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
Usage:
|
|
399
|
-
from comfy_env import wrap_nodes
|
|
400
|
-
wrap_nodes()
|
|
401
|
-
"""
|
|
402
|
-
# Skip if isolation is disabled
|
|
403
|
-
if not _is_comfy_env_enabled():
|
|
404
|
-
print(f"[comfy-env] Isolation disabled, nodes running in main process")
|
|
140
|
+
"""Auto-wrap nodes for isolation. Call from __init__.py after NODE_CLASS_MAPPINGS."""
|
|
141
|
+
if not _is_enabled() or os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
|
|
405
142
|
return
|
|
406
143
|
|
|
407
|
-
# Skip if running inside worker subprocess
|
|
408
|
-
if os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
|
|
409
|
-
return
|
|
410
|
-
|
|
411
|
-
# Get caller's frame and module
|
|
412
144
|
frame = inspect.stack()[1]
|
|
413
145
|
caller_module = inspect.getmodule(frame.frame)
|
|
414
|
-
if caller_module
|
|
415
|
-
print("[comfy-env] Warning: Could not determine caller module")
|
|
416
|
-
return
|
|
146
|
+
if not caller_module: return
|
|
417
147
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if not node_class_mappings:
|
|
421
|
-
print("[comfy-env] Warning: No NODE_CLASS_MAPPINGS found in caller module")
|
|
422
|
-
return
|
|
148
|
+
mappings = getattr(caller_module, "NODE_CLASS_MAPPINGS", None)
|
|
149
|
+
if not mappings: return
|
|
423
150
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
151
|
+
pkg_dir = Path(frame.filename).resolve().parent
|
|
152
|
+
config_files = list(pkg_dir.rglob("comfy-env.toml"))
|
|
153
|
+
if not config_files: return
|
|
427
154
|
|
|
428
|
-
# Find all comfy-env.toml files
|
|
429
|
-
config_files = list(package_dir.rglob("comfy-env.toml"))
|
|
430
|
-
if not config_files:
|
|
431
|
-
return # No configs, nothing to wrap
|
|
432
|
-
|
|
433
|
-
# Get ComfyUI base path
|
|
434
155
|
try:
|
|
435
156
|
import folder_paths
|
|
436
157
|
comfyui_base = folder_paths.base_path
|
|
437
158
|
except ImportError:
|
|
438
159
|
comfyui_base = None
|
|
439
160
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
env_dir
|
|
445
|
-
site_packages, lib_dir = _find_env_paths(config_dir)
|
|
446
|
-
|
|
447
|
-
if not env_dir or not site_packages:
|
|
448
|
-
continue
|
|
161
|
+
envs = []
|
|
162
|
+
for cf in config_files:
|
|
163
|
+
env_dir = _find_env_dir(cf.parent)
|
|
164
|
+
sp, lib = _get_env_paths(env_dir) if env_dir else (None, None)
|
|
165
|
+
if not env_dir or not sp: continue
|
|
449
166
|
|
|
450
|
-
# Read env_vars from config
|
|
451
167
|
env_vars = {}
|
|
452
168
|
try:
|
|
453
169
|
import tomli
|
|
454
|
-
with open(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
except Exception:
|
|
459
|
-
pass
|
|
460
|
-
|
|
461
|
-
if comfyui_base:
|
|
462
|
-
env_vars["COMFYUI_BASE"] = str(comfyui_base)
|
|
463
|
-
|
|
464
|
-
config_envs.append({
|
|
465
|
-
"config_dir": config_dir,
|
|
466
|
-
"env_dir": env_dir,
|
|
467
|
-
"site_packages": site_packages,
|
|
468
|
-
"lib_dir": lib_dir,
|
|
469
|
-
"env_vars": env_vars,
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
if not config_envs:
|
|
473
|
-
return
|
|
170
|
+
with open(cf, "rb") as f:
|
|
171
|
+
env_vars = {str(k): str(v) for k, v in tomli.load(f).get("env_vars", {}).items()}
|
|
172
|
+
except Exception: pass
|
|
173
|
+
if comfyui_base: env_vars["COMFYUI_BASE"] = str(comfyui_base)
|
|
474
174
|
|
|
475
|
-
|
|
476
|
-
wrapped_count = 0
|
|
477
|
-
for node_name, node_cls in node_class_mappings.items():
|
|
478
|
-
if not hasattr(node_cls, "FUNCTION"):
|
|
479
|
-
continue
|
|
175
|
+
envs.append({"dir": cf.parent, "env_dir": env_dir, "sp": sp, "lib": lib, "env_vars": env_vars})
|
|
480
176
|
|
|
481
|
-
|
|
177
|
+
wrapped = 0
|
|
178
|
+
for name, cls in mappings.items():
|
|
179
|
+
if not hasattr(cls, "FUNCTION"): continue
|
|
482
180
|
try:
|
|
483
|
-
|
|
484
|
-
except (TypeError, OSError):
|
|
485
|
-
continue
|
|
181
|
+
src = Path(inspect.getfile(cls)).resolve()
|
|
182
|
+
except (TypeError, OSError): continue
|
|
486
183
|
|
|
487
|
-
|
|
488
|
-
for env_info in config_envs:
|
|
489
|
-
config_dir = env_info["config_dir"]
|
|
184
|
+
for e in envs:
|
|
490
185
|
try:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
_wrap_node_class(
|
|
497
|
-
node_cls,
|
|
498
|
-
env_info["env_dir"],
|
|
499
|
-
config_dir,
|
|
500
|
-
sys_path,
|
|
501
|
-
lib_path,
|
|
502
|
-
env_info["env_vars"],
|
|
503
|
-
)
|
|
504
|
-
wrapped_count += 1
|
|
186
|
+
src.relative_to(e["dir"])
|
|
187
|
+
_wrap_node_class(cls, e["env_dir"], e["dir"], [str(e["sp"]), str(e["dir"])],
|
|
188
|
+
str(e["lib"]) if e["lib"] else None, e["env_vars"])
|
|
189
|
+
wrapped += 1
|
|
505
190
|
break
|
|
506
|
-
except ValueError:
|
|
507
|
-
continue # Node not under this config dir
|
|
508
|
-
|
|
509
|
-
if wrapped_count > 0:
|
|
510
|
-
print(f"[comfy-env] Wrapped {wrapped_count} nodes for isolation")
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
def wrap_isolated_nodes(
|
|
514
|
-
node_class_mappings: Dict[str, type],
|
|
515
|
-
nodes_dir: Path,
|
|
516
|
-
) -> Dict[str, type]:
|
|
517
|
-
"""
|
|
518
|
-
Wrap nodes from a directory that has a comfy-env.toml.
|
|
519
|
-
|
|
520
|
-
This is the directory-based isolation API. Call it for each subdirectory
|
|
521
|
-
of nodes/ that has a comfy-env.toml.
|
|
191
|
+
except ValueError: continue
|
|
522
192
|
|
|
523
|
-
|
|
524
|
-
node_class_mappings: The NODE_CLASS_MAPPINGS dict from the nodes in this dir.
|
|
525
|
-
nodes_dir: The directory containing comfy-env.toml and the node files.
|
|
193
|
+
if wrapped: print(f"[comfy-env] Wrapped {wrapped} nodes")
|
|
526
194
|
|
|
527
|
-
Returns:
|
|
528
|
-
The same dict with node classes wrapped for isolation.
|
|
529
195
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
from pathlib import Path
|
|
534
|
-
|
|
535
|
-
NODE_CLASS_MAPPINGS = {}
|
|
536
|
-
|
|
537
|
-
# Native nodes (no isolation)
|
|
538
|
-
from .nodes.main import NODE_CLASS_MAPPINGS as main_nodes
|
|
539
|
-
NODE_CLASS_MAPPINGS.update(main_nodes)
|
|
540
|
-
|
|
541
|
-
# Isolated nodes (has comfy-env.toml)
|
|
542
|
-
from .nodes.cgal import NODE_CLASS_MAPPINGS as cgal_nodes
|
|
543
|
-
NODE_CLASS_MAPPINGS.update(
|
|
544
|
-
wrap_isolated_nodes(cgal_nodes, Path(__file__).parent / "nodes/cgal")
|
|
545
|
-
)
|
|
546
|
-
"""
|
|
547
|
-
# Skip if isolation is disabled
|
|
548
|
-
if not _is_comfy_env_enabled():
|
|
549
|
-
print(f"[comfy-env] Isolation disabled, nodes running in main process")
|
|
550
|
-
return node_class_mappings
|
|
551
|
-
|
|
552
|
-
# Skip if running inside worker subprocess
|
|
553
|
-
if os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
|
|
196
|
+
def wrap_isolated_nodes(node_class_mappings: Dict[str, type], nodes_dir: Path) -> Dict[str, type]:
|
|
197
|
+
"""Wrap nodes from a directory with comfy-env.toml for isolation."""
|
|
198
|
+
if not _is_enabled() or os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
|
|
554
199
|
return node_class_mappings
|
|
555
200
|
|
|
556
|
-
# Get ComfyUI base path from folder_paths (canonical source)
|
|
557
201
|
try:
|
|
558
202
|
import folder_paths
|
|
559
203
|
comfyui_base = folder_paths.base_path
|
|
@@ -561,56 +205,31 @@ def wrap_isolated_nodes(
|
|
|
561
205
|
comfyui_base = None
|
|
562
206
|
|
|
563
207
|
nodes_dir = Path(nodes_dir).resolve()
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if not config_file.exists():
|
|
568
|
-
print(f"[comfy-env] Warning: No comfy-env.toml in {nodes_dir}")
|
|
208
|
+
config = nodes_dir / "comfy-env.toml"
|
|
209
|
+
if not config.exists():
|
|
210
|
+
print(f"[comfy-env] No comfy-env.toml in {nodes_dir}")
|
|
569
211
|
return node_class_mappings
|
|
570
212
|
|
|
571
|
-
# Read env_vars from comfy-env.toml
|
|
572
213
|
env_vars = {}
|
|
573
214
|
try:
|
|
574
215
|
import tomli
|
|
575
|
-
with open(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
except Exception:
|
|
580
|
-
pass # Ignore errors reading config
|
|
581
|
-
|
|
582
|
-
# Set COMFYUI_BASE for worker to find ComfyUI modules
|
|
583
|
-
if comfyui_base:
|
|
584
|
-
env_vars["COMFYUI_BASE"] = str(comfyui_base)
|
|
585
|
-
|
|
586
|
-
# Find environment directory and paths
|
|
587
|
-
env_dir = _find_env_dir(nodes_dir)
|
|
588
|
-
site_packages, lib_dir = _find_env_paths(nodes_dir)
|
|
216
|
+
with open(config, "rb") as f:
|
|
217
|
+
env_vars = {str(k): str(v) for k, v in tomli.load(f).get("env_vars", {}).items()}
|
|
218
|
+
except Exception: pass
|
|
219
|
+
if comfyui_base: env_vars["COMFYUI_BASE"] = str(comfyui_base)
|
|
589
220
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
print(f"[comfy-env] Run 'comfy-env install' in {nodes_dir}")
|
|
221
|
+
env_dir = _find_env_dir(nodes_dir)
|
|
222
|
+
sp, lib = _get_env_paths(env_dir) if env_dir else (None, None)
|
|
223
|
+
if not env_dir or not sp:
|
|
224
|
+
print(f"[comfy-env] No env found. Run 'comfy-env install' in {nodes_dir}")
|
|
594
225
|
return node_class_mappings
|
|
595
226
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
# Relative imports would require importing parent package which may have host-only deps
|
|
599
|
-
sys_path = [str(site_packages), str(nodes_dir)]
|
|
600
|
-
|
|
601
|
-
# lib_dir for LD_LIBRARY_PATH (conda libraries)
|
|
602
|
-
lib_path = str(lib_dir) if lib_dir else None
|
|
227
|
+
sys_path = [str(sp), str(nodes_dir)]
|
|
228
|
+
lib_path = str(lib) if lib else None
|
|
603
229
|
|
|
604
230
|
print(f"[comfy-env] Wrapping {len(node_class_mappings)} nodes from {nodes_dir.name}")
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if env_vars:
|
|
609
|
-
print(f"[comfy-env] env_vars: {', '.join(f'{k}={v}' for k, v in env_vars.items())}")
|
|
610
|
-
|
|
611
|
-
# Wrap all node classes
|
|
612
|
-
for node_name, node_cls in node_class_mappings.items():
|
|
613
|
-
if hasattr(node_cls, "FUNCTION"):
|
|
614
|
-
_wrap_node_class(node_cls, env_dir, nodes_dir, sys_path, lib_path, env_vars)
|
|
231
|
+
for cls in node_class_mappings.values():
|
|
232
|
+
if hasattr(cls, "FUNCTION"):
|
|
233
|
+
_wrap_node_class(cls, env_dir, nodes_dir, sys_path, lib_path, env_vars)
|
|
615
234
|
|
|
616
235
|
return node_class_mappings
|