comfy-env 0.0.44__py3-none-any.whl → 0.0.45__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/env/manager.py +17 -2
- comfy_env/wheel_sources.yml +1 -1
- comfy_env/workers/venv.py +305 -100
- {comfy_env-0.0.44.dist-info → comfy_env-0.0.45.dist-info}/METADATA +1 -1
- {comfy_env-0.0.44.dist-info → comfy_env-0.0.45.dist-info}/RECORD +8 -8
- {comfy_env-0.0.44.dist-info → comfy_env-0.0.45.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.44.dist-info → comfy_env-0.0.45.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.44.dist-info → comfy_env-0.0.45.dist-info}/licenses/LICENSE +0 -0
comfy_env/env/manager.py
CHANGED
|
@@ -4,6 +4,7 @@ IsolatedEnvManager - Creates and manages isolated Python environments.
|
|
|
4
4
|
Uses uv for fast environment creation and package installation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import os
|
|
7
8
|
import subprocess
|
|
8
9
|
import shutil
|
|
9
10
|
import sys
|
|
@@ -453,10 +454,24 @@ class IsolatedEnvManager:
|
|
|
453
454
|
uv = self._find_uv()
|
|
454
455
|
|
|
455
456
|
self.log("Installing comfy-env (for worker support)...")
|
|
457
|
+
|
|
458
|
+
# Check for local wheel in COMFY_LOCAL_WHEELS directory (set by comfy-test --local)
|
|
459
|
+
install_target = "comfy-env @ git+https://github.com/PozzettiAndrea/comfy-env"
|
|
460
|
+
wheels_dir = os.environ.get("COMFY_LOCAL_WHEELS")
|
|
461
|
+
self.log(f" COMFY_LOCAL_WHEELS={wheels_dir}")
|
|
462
|
+
if wheels_dir:
|
|
463
|
+
wheels_path = Path(wheels_dir)
|
|
464
|
+
self.log(f" Wheels dir exists: {wheels_path.exists()}")
|
|
465
|
+
if wheels_path.exists():
|
|
466
|
+
local_wheels = list(wheels_path.glob("comfy_env-*.whl"))
|
|
467
|
+
self.log(f" Found wheels: {[w.name for w in local_wheels]}")
|
|
468
|
+
if local_wheels:
|
|
469
|
+
install_target = str(local_wheels[0])
|
|
470
|
+
self.log(f" Using local wheel: {local_wheels[0].name}")
|
|
471
|
+
|
|
456
472
|
result = subprocess.run(
|
|
457
473
|
[str(uv), "pip", "install", "--python", str(python_exe),
|
|
458
|
-
"--upgrade", "--no-cache",
|
|
459
|
-
"comfy-env @ git+https://github.com/PozzettiAndrea/comfy-env"],
|
|
474
|
+
"--upgrade", "--no-cache", install_target],
|
|
460
475
|
capture_output=True,
|
|
461
476
|
text=True,
|
|
462
477
|
)
|
comfy_env/wheel_sources.yml
CHANGED
|
@@ -136,6 +136,6 @@ packages:
|
|
|
136
136
|
# Note: This uses a special package_name field, not wheel_template
|
|
137
137
|
# ===========================================================================
|
|
138
138
|
spconv:
|
|
139
|
-
wheel_template: "https://github.com/PozzettiAndrea/cuda-wheels/releases/download/spconv_cu{cuda_short}-latest/
|
|
139
|
+
wheel_template: "https://github.com/PozzettiAndrea/cuda-wheels/releases/download/spconv_cu{cuda_short}-latest/spconv_cu{cuda_short}-{version}%2Bcu{cuda_short}torch{torch_mm}-{py_tag}-{py_tag}-{platform}.whl"
|
|
140
140
|
default_version: "2.3.8"
|
|
141
141
|
description: Sparse convolution library
|
comfy_env/workers/venv.py
CHANGED
|
@@ -29,6 +29,8 @@ Example:
|
|
|
29
29
|
import json
|
|
30
30
|
import os
|
|
31
31
|
import shutil
|
|
32
|
+
import socket
|
|
33
|
+
import struct
|
|
32
34
|
import subprocess
|
|
33
35
|
import sys
|
|
34
36
|
import tempfile
|
|
@@ -36,11 +38,154 @@ import threading
|
|
|
36
38
|
import time
|
|
37
39
|
import uuid
|
|
38
40
|
from pathlib import Path
|
|
39
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
|
41
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
40
42
|
|
|
41
43
|
from .base import Worker, WorkerError
|
|
42
44
|
|
|
43
45
|
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Socket IPC utilities - cross-platform with TCP fallback
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
def _has_af_unix() -> bool:
|
|
51
|
+
"""Check if AF_UNIX sockets are available."""
|
|
52
|
+
return hasattr(socket, 'AF_UNIX')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_socket_dir() -> Path:
|
|
56
|
+
"""Get directory for IPC sockets."""
|
|
57
|
+
if sys.platform == 'linux' and os.path.isdir('/dev/shm'):
|
|
58
|
+
return Path('/dev/shm')
|
|
59
|
+
elif sys.platform == 'win32':
|
|
60
|
+
return Path(tempfile.gettempdir())
|
|
61
|
+
else:
|
|
62
|
+
return Path(tempfile.gettempdir())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _create_server_socket() -> Tuple[socket.socket, str]:
|
|
66
|
+
"""
|
|
67
|
+
Create a server socket for IPC.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (socket, address_string).
|
|
71
|
+
Address string is "unix://path" or "tcp://host:port".
|
|
72
|
+
"""
|
|
73
|
+
if _has_af_unix():
|
|
74
|
+
# Unix domain socket (fast, no port conflicts)
|
|
75
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
76
|
+
sock_path = _get_socket_dir() / f"comfy_worker_{uuid.uuid4().hex[:12]}.sock"
|
|
77
|
+
# Remove stale socket file if exists
|
|
78
|
+
try:
|
|
79
|
+
sock_path.unlink()
|
|
80
|
+
except FileNotFoundError:
|
|
81
|
+
pass
|
|
82
|
+
sock.bind(str(sock_path))
|
|
83
|
+
sock.listen(1)
|
|
84
|
+
return sock, f"unix://{sock_path}"
|
|
85
|
+
else:
|
|
86
|
+
# TCP localhost fallback (works everywhere)
|
|
87
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
88
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
89
|
+
sock.bind(('127.0.0.1', 0)) # OS picks free port
|
|
90
|
+
sock.listen(1)
|
|
91
|
+
port = sock.getsockname()[1]
|
|
92
|
+
return sock, f"tcp://127.0.0.1:{port}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _connect_to_socket(addr: str) -> socket.socket:
|
|
96
|
+
"""
|
|
97
|
+
Connect to a server socket.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
addr: Address string ("unix://path" or "tcp://host:port").
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Connected socket.
|
|
104
|
+
"""
|
|
105
|
+
if addr.startswith("unix://"):
|
|
106
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
107
|
+
sock.connect(addr[7:]) # Strip "unix://"
|
|
108
|
+
return sock
|
|
109
|
+
elif addr.startswith("tcp://"):
|
|
110
|
+
host_port = addr[6:] # Strip "tcp://"
|
|
111
|
+
host, port = host_port.rsplit(":", 1)
|
|
112
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
113
|
+
sock.connect((host, int(port)))
|
|
114
|
+
return sock
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(f"Unknown socket address scheme: {addr}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class SocketTransport:
|
|
120
|
+
"""
|
|
121
|
+
Length-prefixed JSON transport over sockets.
|
|
122
|
+
|
|
123
|
+
Message format: [4-byte big-endian length][JSON payload]
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, sock: socket.socket):
|
|
127
|
+
self._sock = sock
|
|
128
|
+
self._send_lock = threading.Lock()
|
|
129
|
+
self._recv_lock = threading.Lock()
|
|
130
|
+
|
|
131
|
+
def send(self, obj: dict) -> None:
|
|
132
|
+
"""Send a JSON-serializable object."""
|
|
133
|
+
data = json.dumps(obj).encode('utf-8')
|
|
134
|
+
msg = struct.pack('>I', len(data)) + data
|
|
135
|
+
with self._send_lock:
|
|
136
|
+
self._sock.sendall(msg)
|
|
137
|
+
|
|
138
|
+
def recv(self, timeout: Optional[float] = None) -> dict:
|
|
139
|
+
"""Receive a JSON object. Returns None on timeout."""
|
|
140
|
+
with self._recv_lock:
|
|
141
|
+
if timeout is not None:
|
|
142
|
+
self._sock.settimeout(timeout)
|
|
143
|
+
try:
|
|
144
|
+
# Read 4-byte length header
|
|
145
|
+
raw_len = self._recvall(4)
|
|
146
|
+
if not raw_len:
|
|
147
|
+
raise ConnectionError("Socket closed")
|
|
148
|
+
msg_len = struct.unpack('>I', raw_len)[0]
|
|
149
|
+
|
|
150
|
+
# Sanity check
|
|
151
|
+
if msg_len > 100 * 1024 * 1024: # 100MB limit
|
|
152
|
+
raise ValueError(f"Message too large: {msg_len} bytes")
|
|
153
|
+
|
|
154
|
+
# Read payload
|
|
155
|
+
data = self._recvall(msg_len)
|
|
156
|
+
if len(data) < msg_len:
|
|
157
|
+
raise ConnectionError(f"Incomplete message: {len(data)}/{msg_len}")
|
|
158
|
+
|
|
159
|
+
return json.loads(data.decode('utf-8'))
|
|
160
|
+
except socket.timeout:
|
|
161
|
+
return None
|
|
162
|
+
finally:
|
|
163
|
+
if timeout is not None:
|
|
164
|
+
self._sock.settimeout(None)
|
|
165
|
+
|
|
166
|
+
def _recvall(self, n: int) -> bytes:
|
|
167
|
+
"""Receive exactly n bytes."""
|
|
168
|
+
data = bytearray()
|
|
169
|
+
while len(data) < n:
|
|
170
|
+
chunk = self._sock.recv(n - len(data))
|
|
171
|
+
if not chunk:
|
|
172
|
+
return bytes(data)
|
|
173
|
+
data.extend(chunk)
|
|
174
|
+
return bytes(data)
|
|
175
|
+
|
|
176
|
+
def close(self) -> None:
|
|
177
|
+
"""Close the socket."""
|
|
178
|
+
try:
|
|
179
|
+
self._sock.close()
|
|
180
|
+
except:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# =============================================================================
|
|
185
|
+
# Serialization helpers
|
|
186
|
+
# =============================================================================
|
|
187
|
+
|
|
188
|
+
|
|
44
189
|
def _serialize_for_ipc(obj, visited=None):
|
|
45
190
|
"""
|
|
46
191
|
Convert objects with broken __module__ paths to dicts for IPC.
|
|
@@ -413,11 +558,13 @@ class VenvWorker(Worker):
|
|
|
413
558
|
|
|
414
559
|
|
|
415
560
|
# Persistent worker script - runs as __main__ in the venv Python subprocess
|
|
416
|
-
# Uses
|
|
561
|
+
# Uses Unix socket (or TCP localhost) for IPC - completely separate from stdout/stderr
|
|
417
562
|
_PERSISTENT_WORKER_SCRIPT = '''
|
|
418
563
|
import sys
|
|
419
564
|
import os
|
|
420
565
|
import json
|
|
566
|
+
import socket
|
|
567
|
+
import struct
|
|
421
568
|
import traceback
|
|
422
569
|
from types import SimpleNamespace
|
|
423
570
|
|
|
@@ -434,6 +581,57 @@ if sys.platform == "win32":
|
|
|
434
581
|
except Exception:
|
|
435
582
|
pass
|
|
436
583
|
|
|
584
|
+
|
|
585
|
+
class SocketTransport:
|
|
586
|
+
"""Length-prefixed JSON transport."""
|
|
587
|
+
def __init__(self, sock):
|
|
588
|
+
self._sock = sock
|
|
589
|
+
|
|
590
|
+
def send(self, obj):
|
|
591
|
+
data = json.dumps(obj).encode("utf-8")
|
|
592
|
+
msg = struct.pack(">I", len(data)) + data
|
|
593
|
+
self._sock.sendall(msg)
|
|
594
|
+
|
|
595
|
+
def recv(self):
|
|
596
|
+
raw_len = self._recvall(4)
|
|
597
|
+
if not raw_len:
|
|
598
|
+
return None
|
|
599
|
+
msg_len = struct.unpack(">I", raw_len)[0]
|
|
600
|
+
data = self._recvall(msg_len)
|
|
601
|
+
return json.loads(data.decode("utf-8"))
|
|
602
|
+
|
|
603
|
+
def _recvall(self, n):
|
|
604
|
+
data = bytearray()
|
|
605
|
+
while len(data) < n:
|
|
606
|
+
chunk = self._sock.recv(n - len(data))
|
|
607
|
+
if not chunk:
|
|
608
|
+
return bytes(data)
|
|
609
|
+
data.extend(chunk)
|
|
610
|
+
return bytes(data)
|
|
611
|
+
|
|
612
|
+
def close(self):
|
|
613
|
+
try:
|
|
614
|
+
self._sock.close()
|
|
615
|
+
except:
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _connect(addr):
|
|
620
|
+
"""Connect to server socket (unix:// or tcp://)."""
|
|
621
|
+
if addr.startswith("unix://"):
|
|
622
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
623
|
+
sock.connect(addr[7:])
|
|
624
|
+
return sock
|
|
625
|
+
elif addr.startswith("tcp://"):
|
|
626
|
+
host_port = addr[6:]
|
|
627
|
+
host, port = host_port.rsplit(":", 1)
|
|
628
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
629
|
+
sock.connect((host, int(port)))
|
|
630
|
+
return sock
|
|
631
|
+
else:
|
|
632
|
+
raise ValueError(f"Unknown socket scheme: {addr}")
|
|
633
|
+
|
|
634
|
+
|
|
437
635
|
def _deserialize_isolated_objects(obj):
|
|
438
636
|
"""Reconstruct objects serialized with __isolated_object__ marker."""
|
|
439
637
|
if isinstance(obj, dict):
|
|
@@ -449,16 +647,22 @@ def _deserialize_isolated_objects(obj):
|
|
|
449
647
|
return tuple(_deserialize_isolated_objects(v) for v in obj)
|
|
450
648
|
return obj
|
|
451
649
|
|
|
452
|
-
def main():
|
|
453
|
-
# Save original stdout for JSON IPC - redirect stdout to stderr for module prints
|
|
454
|
-
_ipc_out = sys.stdout
|
|
455
|
-
sys.stdout = sys.stderr # All print() calls go to stderr now
|
|
456
650
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if
|
|
651
|
+
def main():
|
|
652
|
+
# Get socket address from command line
|
|
653
|
+
if len(sys.argv) < 2:
|
|
654
|
+
print("Usage: worker.py <socket_addr>", file=sys.stderr)
|
|
655
|
+
sys.exit(1)
|
|
656
|
+
socket_addr = sys.argv[1]
|
|
657
|
+
|
|
658
|
+
# Connect to host process
|
|
659
|
+
sock = _connect(socket_addr)
|
|
660
|
+
transport = SocketTransport(sock)
|
|
661
|
+
|
|
662
|
+
# Read config as first message
|
|
663
|
+
config = transport.recv()
|
|
664
|
+
if not config:
|
|
460
665
|
return
|
|
461
|
-
config = json.loads(config_line)
|
|
462
666
|
|
|
463
667
|
# Setup sys.path
|
|
464
668
|
for p in config.get("sys_paths", []):
|
|
@@ -468,17 +672,15 @@ def main():
|
|
|
468
672
|
# Import torch after path setup
|
|
469
673
|
import torch
|
|
470
674
|
|
|
471
|
-
# Signal ready
|
|
472
|
-
|
|
473
|
-
_ipc_out.flush()
|
|
675
|
+
# Signal ready
|
|
676
|
+
transport.send({"status": "ready"})
|
|
474
677
|
|
|
475
678
|
# Process requests
|
|
476
679
|
while True:
|
|
477
680
|
try:
|
|
478
|
-
|
|
479
|
-
if not
|
|
681
|
+
request = transport.recv()
|
|
682
|
+
if not request:
|
|
480
683
|
break
|
|
481
|
-
request = json.loads(line)
|
|
482
684
|
except Exception:
|
|
483
685
|
break
|
|
484
686
|
|
|
@@ -521,16 +723,16 @@ def main():
|
|
|
521
723
|
if outputs_path:
|
|
522
724
|
torch.save(result, outputs_path)
|
|
523
725
|
|
|
524
|
-
|
|
525
|
-
_ipc_out.flush()
|
|
726
|
+
transport.send({"status": "ok"})
|
|
526
727
|
|
|
527
728
|
except Exception as e:
|
|
528
|
-
|
|
729
|
+
transport.send({
|
|
529
730
|
"status": "error",
|
|
530
731
|
"error": str(e),
|
|
531
732
|
"traceback": traceback.format_exc(),
|
|
532
|
-
})
|
|
533
|
-
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
transport.close()
|
|
534
736
|
|
|
535
737
|
if __name__ == "__main__":
|
|
536
738
|
main()
|
|
@@ -541,15 +743,16 @@ class PersistentVenvWorker(Worker):
|
|
|
541
743
|
"""
|
|
542
744
|
Persistent version of VenvWorker that keeps subprocess alive.
|
|
543
745
|
|
|
544
|
-
Uses
|
|
545
|
-
This
|
|
546
|
-
|
|
746
|
+
Uses Unix domain sockets (or TCP localhost on older Windows) for IPC.
|
|
747
|
+
This completely separates IPC from stdout/stderr, so C libraries
|
|
748
|
+
printing to stdout (like Blender) won't corrupt the protocol.
|
|
547
749
|
|
|
548
750
|
Benefits:
|
|
549
751
|
- Works on Windows with different venv Python (full isolation)
|
|
550
752
|
- Compiled CUDA extensions load correctly in the venv
|
|
551
753
|
- ~50-100ms per call (vs ~300-500ms for VenvWorker per-call spawn)
|
|
552
754
|
- Tensor transfer via shared memory files
|
|
755
|
+
- Immune to stdout pollution from C libraries
|
|
553
756
|
|
|
554
757
|
Use this for high-frequency calls to isolated venvs.
|
|
555
758
|
"""
|
|
@@ -589,6 +792,11 @@ class PersistentVenvWorker(Worker):
|
|
|
589
792
|
self._shutdown = False
|
|
590
793
|
self._lock = threading.Lock()
|
|
591
794
|
|
|
795
|
+
# Socket IPC
|
|
796
|
+
self._server_socket: Optional[socket.socket] = None
|
|
797
|
+
self._socket_addr: Optional[str] = None
|
|
798
|
+
self._transport: Optional[SocketTransport] = None
|
|
799
|
+
|
|
592
800
|
# Write worker script to temp file
|
|
593
801
|
self._worker_script = self._temp_dir / "persistent_worker.py"
|
|
594
802
|
self._worker_script.write_text(_PERSISTENT_WORKER_SCRIPT)
|
|
@@ -619,6 +827,17 @@ class PersistentVenvWorker(Worker):
|
|
|
619
827
|
if self._process is not None and self._process.poll() is None:
|
|
620
828
|
return # Already running
|
|
621
829
|
|
|
830
|
+
# Clean up any previous socket
|
|
831
|
+
if self._transport:
|
|
832
|
+
self._transport.close()
|
|
833
|
+
self._transport = None
|
|
834
|
+
if self._server_socket:
|
|
835
|
+
self._server_socket.close()
|
|
836
|
+
self._server_socket = None
|
|
837
|
+
|
|
838
|
+
# Create server socket for IPC
|
|
839
|
+
self._server_socket, self._socket_addr = _create_server_socket()
|
|
840
|
+
|
|
622
841
|
# Set up environment
|
|
623
842
|
env = os.environ.copy()
|
|
624
843
|
env.update(self.extra_env)
|
|
@@ -638,69 +857,55 @@ class PersistentVenvWorker(Worker):
|
|
|
638
857
|
stubs_dir = Path(__file__).parent.parent / "stubs"
|
|
639
858
|
all_sys_path = [str(stubs_dir), str(self.working_dir)] + self.sys_path
|
|
640
859
|
|
|
641
|
-
# Launch subprocess with the venv Python
|
|
642
|
-
# This runs _PERSISTENT_WORKER_SCRIPT as __main__ - no reimport issues!
|
|
860
|
+
# Launch subprocess with the venv Python, passing socket address
|
|
643
861
|
self._process = subprocess.Popen(
|
|
644
|
-
[str(self.python), str(self._worker_script)],
|
|
645
|
-
stdin=subprocess.
|
|
862
|
+
[str(self.python), str(self._worker_script), self._socket_addr],
|
|
863
|
+
stdin=subprocess.DEVNULL,
|
|
646
864
|
stdout=subprocess.PIPE,
|
|
647
|
-
stderr=subprocess.
|
|
865
|
+
stderr=subprocess.STDOUT, # Merge stdout/stderr for forwarding
|
|
648
866
|
cwd=str(self.working_dir),
|
|
649
867
|
env=env,
|
|
650
|
-
bufsize=1, # Line buffered
|
|
651
|
-
text=True, # Text mode for JSON
|
|
652
868
|
)
|
|
653
869
|
|
|
654
|
-
# Start
|
|
655
|
-
def
|
|
870
|
+
# Start output forwarding thread
|
|
871
|
+
def forward_output():
|
|
656
872
|
try:
|
|
657
|
-
for line in self._process.
|
|
658
|
-
|
|
873
|
+
for line in self._process.stdout:
|
|
874
|
+
if isinstance(line, bytes):
|
|
875
|
+
line = line.decode('utf-8', errors='replace')
|
|
659
876
|
sys.stderr.write(f" {line}")
|
|
660
877
|
sys.stderr.flush()
|
|
661
878
|
except:
|
|
662
879
|
pass
|
|
663
|
-
self.
|
|
664
|
-
self.
|
|
880
|
+
self._output_thread = threading.Thread(target=forward_output, daemon=True)
|
|
881
|
+
self._output_thread.start()
|
|
665
882
|
|
|
666
|
-
#
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
# Wait for ready signal with timeout
|
|
672
|
-
import select
|
|
673
|
-
if sys.platform == "win32":
|
|
674
|
-
# Windows: can't use select on pipes, use thread with timeout
|
|
675
|
-
ready_line = [None]
|
|
676
|
-
def read_ready():
|
|
677
|
-
try:
|
|
678
|
-
ready_line[0] = self._process.stdout.readline()
|
|
679
|
-
except:
|
|
680
|
-
pass
|
|
681
|
-
t = threading.Thread(target=read_ready, daemon=True)
|
|
682
|
-
t.start()
|
|
683
|
-
t.join(timeout=60)
|
|
684
|
-
line = ready_line[0]
|
|
685
|
-
else:
|
|
686
|
-
# Unix: use select for timeout
|
|
687
|
-
import select
|
|
688
|
-
ready, _, _ = select.select([self._process.stdout], [], [], 60)
|
|
689
|
-
line = self._process.stdout.readline() if ready else None
|
|
690
|
-
|
|
691
|
-
if not line:
|
|
883
|
+
# Accept connection from worker with timeout
|
|
884
|
+
self._server_socket.settimeout(60)
|
|
885
|
+
try:
|
|
886
|
+
client_sock, _ = self._server_socket.accept()
|
|
887
|
+
except socket.timeout:
|
|
692
888
|
stderr = ""
|
|
693
889
|
try:
|
|
694
890
|
self._process.kill()
|
|
695
|
-
|
|
891
|
+
stdout, _ = self._process.communicate(timeout=5)
|
|
892
|
+
stderr = stdout.decode('utf-8', errors='replace') if stdout else ""
|
|
696
893
|
except:
|
|
697
894
|
pass
|
|
698
|
-
raise RuntimeError(f"{self.name}: Worker failed to
|
|
895
|
+
raise RuntimeError(f"{self.name}: Worker failed to connect (timeout). output: {stderr}")
|
|
896
|
+
finally:
|
|
897
|
+
self._server_socket.settimeout(None)
|
|
699
898
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
899
|
+
self._transport = SocketTransport(client_sock)
|
|
900
|
+
|
|
901
|
+
# Send config
|
|
902
|
+
config = {"sys_paths": all_sys_path}
|
|
903
|
+
self._transport.send(config)
|
|
904
|
+
|
|
905
|
+
# Wait for ready signal
|
|
906
|
+
msg = self._transport.recv(timeout=60)
|
|
907
|
+
if not msg:
|
|
908
|
+
raise RuntimeError(f"{self.name}: Worker failed to send ready signal")
|
|
704
909
|
|
|
705
910
|
if msg.get("status") != "ready":
|
|
706
911
|
raise RuntimeError(f"{self.name}: Unexpected ready message: {msg}")
|
|
@@ -718,31 +923,17 @@ class PersistentVenvWorker(Worker):
|
|
|
718
923
|
)
|
|
719
924
|
|
|
720
925
|
def _send_request(self, request: dict, timeout: float) -> dict:
|
|
721
|
-
"""Send request via
|
|
926
|
+
"""Send request via socket and read response with timeout."""
|
|
927
|
+
if not self._transport:
|
|
928
|
+
raise RuntimeError(f"{self.name}: Transport not initialized")
|
|
929
|
+
|
|
722
930
|
# Send request
|
|
723
|
-
self.
|
|
724
|
-
self._process.stdin.flush()
|
|
931
|
+
self._transport.send(request)
|
|
725
932
|
|
|
726
933
|
# Read response with timeout
|
|
727
|
-
|
|
728
|
-
# Windows: use thread for timeout
|
|
729
|
-
response_line = [None]
|
|
730
|
-
def read_response():
|
|
731
|
-
try:
|
|
732
|
-
response_line[0] = self._process.stdout.readline()
|
|
733
|
-
except:
|
|
734
|
-
pass
|
|
735
|
-
t = threading.Thread(target=read_response, daemon=True)
|
|
736
|
-
t.start()
|
|
737
|
-
t.join(timeout=timeout)
|
|
738
|
-
line = response_line[0]
|
|
739
|
-
else:
|
|
740
|
-
# Unix: use select
|
|
741
|
-
import select
|
|
742
|
-
ready, _, _ = select.select([self._process.stdout], [], [], timeout)
|
|
743
|
-
line = self._process.stdout.readline() if ready else None
|
|
934
|
+
response = self._transport.recv(timeout=timeout)
|
|
744
935
|
|
|
745
|
-
if
|
|
936
|
+
if response is None:
|
|
746
937
|
# Timeout - kill process
|
|
747
938
|
try:
|
|
748
939
|
self._process.kill()
|
|
@@ -751,10 +942,7 @@ class PersistentVenvWorker(Worker):
|
|
|
751
942
|
self._shutdown = True
|
|
752
943
|
raise TimeoutError(f"{self.name}: Call timed out after {timeout}s")
|
|
753
944
|
|
|
754
|
-
|
|
755
|
-
return json.loads(line)
|
|
756
|
-
except json.JSONDecodeError as e:
|
|
757
|
-
raise WorkerError(f"Invalid response from worker: {line!r}") from e
|
|
945
|
+
return response
|
|
758
946
|
|
|
759
947
|
def call_method(
|
|
760
948
|
self,
|
|
@@ -882,16 +1070,33 @@ class PersistentVenvWorker(Worker):
|
|
|
882
1070
|
return
|
|
883
1071
|
self._shutdown = True
|
|
884
1072
|
|
|
885
|
-
# Send shutdown signal via
|
|
886
|
-
if self._process and self._process.poll() is None:
|
|
1073
|
+
# Send shutdown signal via socket
|
|
1074
|
+
if self._transport and self._process and self._process.poll() is None:
|
|
887
1075
|
try:
|
|
888
|
-
self.
|
|
889
|
-
self._process.stdin.flush()
|
|
890
|
-
self._process.stdin.close()
|
|
1076
|
+
self._transport.send({"method": "shutdown"})
|
|
891
1077
|
except:
|
|
892
1078
|
pass
|
|
893
1079
|
|
|
894
|
-
|
|
1080
|
+
# Close transport and socket
|
|
1081
|
+
if self._transport:
|
|
1082
|
+
self._transport.close()
|
|
1083
|
+
self._transport = None
|
|
1084
|
+
|
|
1085
|
+
if self._server_socket:
|
|
1086
|
+
try:
|
|
1087
|
+
self._server_socket.close()
|
|
1088
|
+
except:
|
|
1089
|
+
pass
|
|
1090
|
+
# Clean up unix socket file
|
|
1091
|
+
if self._socket_addr and self._socket_addr.startswith("unix://"):
|
|
1092
|
+
try:
|
|
1093
|
+
Path(self._socket_addr[7:]).unlink()
|
|
1094
|
+
except:
|
|
1095
|
+
pass
|
|
1096
|
+
self._server_socket = None
|
|
1097
|
+
|
|
1098
|
+
# Wait for process to exit
|
|
1099
|
+
if self._process and self._process.poll() is None:
|
|
895
1100
|
try:
|
|
896
1101
|
self._process.wait(timeout=5)
|
|
897
1102
|
except subprocess.TimeoutExpired:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-env
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.45
|
|
4
4
|
Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
|
|
5
5
|
Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
|
|
6
6
|
Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
|
|
@@ -11,7 +11,7 @@ comfy_env/env/__init__.py,sha256=imQdoQEQvrRT-QDtyNpFlkVbm2fBzgACdpQwRPd09fI,115
|
|
|
11
11
|
comfy_env/env/config.py,sha256=Ila-5Yal3bj6jENbBeYJlZtkbgdwnzJzImVZK3ZF1lg,7645
|
|
12
12
|
comfy_env/env/config_file.py,sha256=HzFKeQh9zQ--K1V-XuvgE6DiE_bYrXrChL1ZT8Tzlq4,24684
|
|
13
13
|
comfy_env/env/cuda_gpu_detection.py,sha256=YLuXUdWg6FeKdNyLlQAHPlveg4rTenXJ2VbeAaEi9QE,9755
|
|
14
|
-
comfy_env/env/manager.py,sha256
|
|
14
|
+
comfy_env/env/manager.py,sha256=-qdbZDsbNfs70onVbC7mhKCzNsxYx3WmG7ttlBinhGI,23659
|
|
15
15
|
comfy_env/env/security.py,sha256=dNSitAnfBNVdvxgBBntYw33AJaCs_S1MHb7KJhAVYzM,8171
|
|
16
16
|
comfy_env/env/platform/__init__.py,sha256=Nb5MPZIEeanSMEWwqU4p4bnEKTJn1tWcwobnhq9x9IY,614
|
|
17
17
|
comfy_env/env/platform/base.py,sha256=iS0ptTTVjXRwPU4qWUdvHI7jteuzxGSjWr5BUQ7hGiU,2453
|
|
@@ -35,10 +35,10 @@ comfy_env/workers/base.py,sha256=ZILYXlvGCWuCZXmjKqfG8VeD19ihdYaASdlbasl2BMo,231
|
|
|
35
35
|
comfy_env/workers/pool.py,sha256=MtjeOWfvHSCockq8j1gfnxIl-t01GSB79T5N4YB82Lg,6956
|
|
36
36
|
comfy_env/workers/tensor_utils.py,sha256=TCuOAjJymrSbkgfyvcKtQ_KbVWTqSwP9VH_bCaFLLq8,6409
|
|
37
37
|
comfy_env/workers/torch_mp.py,sha256=4YSNPn7hALrvMVbkO4RkTeFTcc0lhfLMk5QTWjY4PHw,22134
|
|
38
|
-
comfy_env/workers/venv.py,sha256=
|
|
39
|
-
comfy_env/wheel_sources.yml,sha256=
|
|
40
|
-
comfy_env-0.0.
|
|
41
|
-
comfy_env-0.0.
|
|
42
|
-
comfy_env-0.0.
|
|
43
|
-
comfy_env-0.0.
|
|
44
|
-
comfy_env-0.0.
|
|
38
|
+
comfy_env/workers/venv.py,sha256=YXIjSo4JSq2m9W2sDXA6N0pBc8kv_MpxfOJ2YZjBkw4,38219
|
|
39
|
+
comfy_env/wheel_sources.yml,sha256=K5dksy21YcT7QdFlVDkKF4Rv9ZCjHaWhQgoEhdSyAOI,8156
|
|
40
|
+
comfy_env-0.0.45.dist-info/METADATA,sha256=sVpd0ratFmnZ9CG2fN3-NP0CDrEscDwmlGpOPLCq2wk,7138
|
|
41
|
+
comfy_env-0.0.45.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
42
|
+
comfy_env-0.0.45.dist-info/entry_points.txt,sha256=J4fXeqgxU_YenuW_Zxn_pEL7J-3R0--b6MS5t0QmAr0,49
|
|
43
|
+
comfy_env-0.0.45.dist-info/licenses/LICENSE,sha256=E68QZMMpW4P2YKstTZ3QU54HRQO8ecew09XZ4_Vn870,1093
|
|
44
|
+
comfy_env-0.0.45.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|