comfy-env 0.0.43__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 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
@@ -383,13 +384,19 @@ class IsolatedEnvManager:
383
384
 
384
385
  if "wheel_template" in config:
385
386
  # Direct wheel URL from template
386
- effective_version = version or config.get("default_version")
387
- if not effective_version:
388
- raise RuntimeError(f"Package {package} requires version (no default in registry)")
389
-
390
- vars_dict["version"] = effective_version
391
- wheel_url = self._substitute_template(config["wheel_template"], vars_dict)
392
- self.log(f" Installing {package}=={effective_version}...")
387
+ template = config["wheel_template"]
388
+
389
+ # Only require version if template uses {version}
390
+ effective_version = None
391
+ if "{version}" in template:
392
+ effective_version = version or config.get("default_version")
393
+ if not effective_version:
394
+ raise RuntimeError(f"Package {package} requires version (no default in registry)")
395
+ vars_dict["version"] = effective_version
396
+
397
+ wheel_url = self._substitute_template(template, vars_dict)
398
+ version_str = f"=={effective_version}" if effective_version else ""
399
+ self.log(f" Installing {package}{version_str}...")
393
400
  self.log(f" URL: {wheel_url}")
394
401
  result = subprocess.run(
395
402
  pip_args + ["--no-deps", wheel_url],
@@ -398,6 +405,19 @@ class IsolatedEnvManager:
398
405
  if result.returncode != 0:
399
406
  raise RuntimeError(f"Failed to install {package}: {result.stderr}")
400
407
 
408
+ elif "find_links" in config:
409
+ # find_links: pip resolves wheel from index URL
410
+ find_links_url = config["find_links"]
411
+ effective_version = version or config.get("default_version")
412
+ pkg_spec = f"{package}=={effective_version}" if effective_version else package
413
+ self.log(f" Installing {pkg_spec} from {find_links_url}...")
414
+ result = subprocess.run(
415
+ pip_args + ["--no-deps", "--find-links", find_links_url, pkg_spec],
416
+ capture_output=True, text=True,
417
+ )
418
+ if result.returncode != 0:
419
+ raise RuntimeError(f"Failed to install {package}: {result.stderr}")
420
+
401
421
  elif "package_name" in config:
402
422
  # PyPI variant (e.g., spconv-cu124)
403
423
  pkg_name = self._substitute_template(config["package_name"], vars_dict)
@@ -411,7 +431,7 @@ class IsolatedEnvManager:
411
431
  raise RuntimeError(f"Failed to install {package}: {result.stderr}")
412
432
 
413
433
  else:
414
- raise RuntimeError(f"Package {package} in registry but missing wheel_template or package_name")
434
+ raise RuntimeError(f"Package {package} in registry but missing wheel_template, find_links, or package_name")
415
435
 
416
436
  else:
417
437
  # Not in registry - error
@@ -434,10 +454,24 @@ class IsolatedEnvManager:
434
454
  uv = self._find_uv()
435
455
 
436
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
+
437
472
  result = subprocess.run(
438
473
  [str(uv), "pip", "install", "--python", str(python_exe),
439
- "--upgrade", "--no-cache",
440
- "comfy-env @ git+https://github.com/PozzettiAndrea/comfy-env"],
474
+ "--upgrade", "--no-cache", install_target],
441
475
  capture_output=True,
442
476
  text=True,
443
477
  )
comfy_env/install.py CHANGED
@@ -367,11 +367,25 @@ def _resolve_wheel_url(
367
367
 
368
368
  # wheel_template: direct URL
369
369
  if "wheel_template" in config:
370
+ template = config["wheel_template"]
371
+
372
+ # Only require version if template uses {version}
373
+ if "{version}" in template:
374
+ effective_version = version or config.get("default_version")
375
+ if not effective_version:
376
+ raise InstallError(f"Package {package} requires version (no default in registry)")
377
+ vars_dict["version"] = effective_version
378
+
379
+ return _substitute_template(template, vars_dict)
380
+
381
+ # find_links: pip resolves wheel from index (e.g., miropsota's detectron2)
382
+ if "find_links" in config:
383
+ find_links_url = config["find_links"]
370
384
  effective_version = version or config.get("default_version")
371
- if not effective_version:
372
- raise InstallError(f"Package {package} requires version (no default in registry)")
373
- vars_dict["version"] = effective_version
374
- return _substitute_template(config["wheel_template"], vars_dict)
385
+ if effective_version:
386
+ return f"find_links:{find_links_url}:{package}=={effective_version}"
387
+ else:
388
+ return f"find_links:{find_links_url}:{package}"
375
389
 
376
390
  # package_name: PyPI variant (e.g., spconv-cu124)
377
391
  if "package_name" in config:
@@ -406,6 +420,11 @@ def _install_cuda_package(
406
420
  pkg_spec = f"{pkg_name}=={version}" if version else pkg_name
407
421
  log(f" Installing {package} as {pkg_spec} from PyPI...")
408
422
  _pip_install([pkg_spec], no_deps=False, log=log)
423
+ elif url_or_marker.startswith("find_links:"):
424
+ # find_links: pip resolves from index URL
425
+ _, find_links_url, pkg_spec = url_or_marker.split(":", 2)
426
+ log(f" Installing {pkg_spec} from {find_links_url}...")
427
+ _pip_install([pkg_spec, "--find-links", find_links_url], no_deps=True, log=log)
409
428
  else:
410
429
  # Direct wheel URL
411
430
  log(f" Installing {package}...")
@@ -25,18 +25,22 @@ packages:
25
25
  # ===========================================================================
26
26
  torch-scatter:
27
27
  wheel_template: "https://data.pyg.org/whl/torch-{torch_version}%2Bcu{cuda_short}/torch_scatter-{version}%2Bpt{torch_mm}cu{cuda_short}-{py_tag}-{py_tag}-{platform}.whl"
28
+ default_version: "2.1.2"
28
29
  description: Scatter operations for PyTorch
29
30
 
30
31
  torch-cluster:
31
32
  wheel_template: "https://data.pyg.org/whl/torch-{torch_version}%2Bcu{cuda_short}/torch_cluster-{version}%2Bpt{torch_mm}cu{cuda_short}-{py_tag}-{py_tag}-{platform}.whl"
33
+ default_version: "1.6.3"
32
34
  description: Clustering algorithms for PyTorch
33
35
 
34
36
  torch-sparse:
35
37
  wheel_template: "https://data.pyg.org/whl/torch-{torch_version}%2Bcu{cuda_short}/torch_sparse-{version}%2Bpt{torch_mm}cu{cuda_short}-{py_tag}-{py_tag}-{platform}.whl"
38
+ default_version: "0.6.18"
36
39
  description: Sparse tensor operations for PyTorch
37
40
 
38
41
  torch-spline-conv:
39
42
  wheel_template: "https://data.pyg.org/whl/torch-{torch_version}%2Bcu{cuda_short}/torch_spline_conv-{version}%2Bpt{torch_mm}cu{cuda_short}-{py_tag}-{py_tag}-{platform}.whl"
43
+ default_version: "1.2.2"
40
44
  description: Spline convolutions for PyTorch
41
45
 
42
46
  # ===========================================================================
@@ -114,7 +118,8 @@ packages:
114
118
  # Prebuilt wheels from miropsota's torch_packages_builder
115
119
  # ===========================================================================
116
120
  detectron2:
117
- wheel_template: "https://miropsota.github.io/torch_packages_builder/detectron2/detectron2-0.6%2B2a420edpt{torch_version}cu{cuda_short}-{py_tag}-{py_tag}-{platform}.whl"
121
+ find_links: "https://miropsota.github.io/torch_packages_builder/detectron2/"
122
+ default_version: "0.6"
118
123
  description: Detectron2 - Facebook's detection and segmentation library
119
124
 
120
125
  # ===========================================================================
@@ -131,5 +136,6 @@ packages:
131
136
  # Note: This uses a special package_name field, not wheel_template
132
137
  # ===========================================================================
133
138
  spconv:
134
- package_name: "spconv-cu{cuda_short2}"
135
- description: Sparse convolution library (installs spconv-cu124, spconv-cu126, etc.)
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
+ default_version: "2.3.8"
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 stdin/stdout JSON for IPC - avoids Windows multiprocessing spawn issues entirely
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
- # Read config from first line
458
- config_line = sys.stdin.readline()
459
- if not config_line:
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 (use _ipc_out, not stdout)
472
- _ipc_out.write(json.dumps({"status": "ready"}) + "\\n")
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
- line = sys.stdin.readline()
479
- if not line:
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
- _ipc_out.write(json.dumps({"status": "ok"}) + "\\n")
525
- _ipc_out.flush()
726
+ transport.send({"status": "ok"})
526
727
 
527
728
  except Exception as e:
528
- _ipc_out.write(json.dumps({
729
+ transport.send({
529
730
  "status": "error",
530
731
  "error": str(e),
531
732
  "traceback": traceback.format_exc(),
532
- }) + "\\n")
533
- _ipc_out.flush()
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 subprocess.Popen with stdin/stdout JSON IPC instead of multiprocessing.
545
- This avoids Windows multiprocessing spawn issues where the child process
546
- tries to reimport __main__ (which fails when using a different Python).
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.PIPE,
862
+ [str(self.python), str(self._worker_script), self._socket_addr],
863
+ stdin=subprocess.DEVNULL,
646
864
  stdout=subprocess.PIPE,
647
- stderr=subprocess.PIPE,
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 stderr forwarding thread to show worker output in real-time
655
- def forward_stderr():
870
+ # Start output forwarding thread
871
+ def forward_output():
656
872
  try:
657
- for line in self._process.stderr:
658
- # Forward to main process stderr (visible in console)
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._stderr_thread = threading.Thread(target=forward_stderr, daemon=True)
664
- self._stderr_thread.start()
880
+ self._output_thread = threading.Thread(target=forward_output, daemon=True)
881
+ self._output_thread.start()
665
882
 
666
- # Send config
667
- config = {"sys_paths": all_sys_path}
668
- self._process.stdin.write(json.dumps(config) + "\n")
669
- self._process.stdin.flush()
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
- _, stderr = self._process.communicate(timeout=5)
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 start (timeout). stderr: {stderr}")
895
+ raise RuntimeError(f"{self.name}: Worker failed to connect (timeout). output: {stderr}")
896
+ finally:
897
+ self._server_socket.settimeout(None)
699
898
 
700
- try:
701
- msg = json.loads(line)
702
- except json.JSONDecodeError as e:
703
- raise RuntimeError(f"{self.name}: Invalid ready message: {line!r}") from e
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 stdin and read response from stdout with timeout."""
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._process.stdin.write(json.dumps(request) + "\n")
724
- self._process.stdin.flush()
931
+ self._transport.send(request)
725
932
 
726
933
  # Read response with timeout
727
- if sys.platform == "win32":
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 not line:
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
- try:
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 stdin
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._process.stdin.write(json.dumps({"method": "shutdown"}) + "\n")
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
- # Wait for process to exit
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.43
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
@@ -2,7 +2,7 @@ comfy_env/__init__.py,sha256=OQJFNjmArjLcgrfHAFxgDJQFH_IhxibqMXbU5bu_j9Q,3822
2
2
  comfy_env/cli.py,sha256=hZv_oJsmaMoG62Fr2Fjp778P_32BHr4fzS7G4lULSwU,13153
3
3
  comfy_env/decorator.py,sha256=6JCKwLHaZtOLVDexs_gh_-NtS2ZK0V7nGCPqkyeYEAA,16688
4
4
  comfy_env/errors.py,sha256=8hN8NDlo8oBUdapc-eT3ZluigI5VBzfqsSBvQdfWlz4,9943
5
- comfy_env/install.py,sha256=lKkW55mDLut3zpJOUgjAi-BdLluBRPN-bblONHjo-Ws,16595
5
+ comfy_env/install.py,sha256=m4NKlfCcQGI5xzVRjHw3ep-lWbqx5kE1e21sUUZ2Leo,17528
6
6
  comfy_env/nodes.py,sha256=CWUe35jU5SKk4ur-SddZePdqWgxJDlxGhpcJiu5pAK4,4354
7
7
  comfy_env/pixi.py,sha256=y25mUDhB3bCqhPMGF0h23Tf8ZHykK4gLJrkvOhsPWmE,14398
8
8
  comfy_env/registry.py,sha256=w-QwvAPFlCrBYRAv4cXkp2zujQPZn8Fk5DUxKCtox8o,3430
@@ -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=eDcrtJeNrW3jYb0q0R_DmUfAYjGo5Cs4BMuZUxWEzeg,21789
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=PmsVOu5i89tBYkGRupo2bjOLPBmk06q4GNUwDWsd9F8,32088
39
- comfy_env/wheel_sources.yml,sha256=07-ruJ7HHGtXJUs7rDmud8X6OsBcfogZKG6cyU7Gacg,7950
40
- comfy_env-0.0.43.dist-info/METADATA,sha256=HzyWA3jqZjM9YsoFQEhZkHMTB8b8agepqMvo2YkOQY8,7138
41
- comfy_env-0.0.43.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
42
- comfy_env-0.0.43.dist-info/entry_points.txt,sha256=J4fXeqgxU_YenuW_Zxn_pEL7J-3R0--b6MS5t0QmAr0,49
43
- comfy_env-0.0.43.dist-info/licenses/LICENSE,sha256=E68QZMMpW4P2YKstTZ3QU54HRQO8ecew09XZ4_Vn870,1093
44
- comfy_env-0.0.43.dist-info/RECORD,,
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,,