comfy-env 0.0.50__tar.gz → 0.0.52__tar.gz
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-0.0.50 → comfy_env-0.0.52}/PKG-INFO +1 -1
- {comfy_env-0.0.50 → comfy_env-0.0.52}/pyproject.toml +1 -1
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/pixi.py +11 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/stub_imports.py +18 -11
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/workers/venv.py +150 -11
- {comfy_env-0.0.50 → comfy_env-0.0.52}/.github/workflows/publish.yml +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/.gitignore +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/LICENSE +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/README.md +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/__init__.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/cli.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/decorator.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/__init__.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/config.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/config_file.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/cuda_gpu_detection.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/manager.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/platform/__init__.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/platform/base.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/platform/darwin.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/platform/linux.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/platform/windows.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/env/security.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/errors.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/install.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/ipc/__init__.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/ipc/bridge.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/ipc/protocol.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/ipc/tensor.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/ipc/torch_bridge.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/ipc/transport.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/ipc/worker.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/isolation.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/nodes.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/registry.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/resolver.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/stubs/__init__.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/stubs/comfy/__init__.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/stubs/comfy/model_management.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/stubs/comfy/utils.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/stubs/folder_paths.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/templates/comfy-env.toml +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/wheel_sources.yml +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/workers/__init__.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/workers/base.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/workers/pool.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/workers/tensor_utils.py +0 -0
- {comfy_env-0.0.50 → comfy_env-0.0.52}/src/comfy_env/workers/torch_mp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-env
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.52
|
|
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
|
|
@@ -233,12 +233,23 @@ def create_pixi_toml(
|
|
|
233
233
|
elif sys.platform == "win32":
|
|
234
234
|
lines.append('platforms = ["win-64"]')
|
|
235
235
|
|
|
236
|
+
# System requirements - specify glibc version for proper wheel resolution
|
|
237
|
+
# Ubuntu 22.04+ has glibc 2.35, enabling manylinux_2_35 wheels
|
|
238
|
+
if sys.platform == "linux":
|
|
239
|
+
lines.append("")
|
|
240
|
+
lines.append("[system-requirements]")
|
|
241
|
+
lines.append('libc = { family = "glibc", version = "2.35" }')
|
|
242
|
+
|
|
236
243
|
lines.append("")
|
|
237
244
|
|
|
238
245
|
# Dependencies section (conda packages)
|
|
239
246
|
lines.append("[dependencies]")
|
|
240
247
|
lines.append(f'python = "{env_config.python}.*"')
|
|
241
248
|
|
|
249
|
+
# On Windows, use MKL BLAS to avoid OpenBLAS crashes (numpy blas_fpe_check issue)
|
|
250
|
+
if sys.platform == "win32":
|
|
251
|
+
lines.append('libblas = { version = "*", build = "*mkl" }')
|
|
252
|
+
|
|
242
253
|
for pkg in conda.packages:
|
|
243
254
|
# Parse package spec (name=version or name>=version or just name)
|
|
244
255
|
if "=" in pkg and not pkg.startswith("="):
|
|
@@ -156,19 +156,26 @@ def _get_import_names_from_pixi(node_dir: Path) -> Set[str]:
|
|
|
156
156
|
"""
|
|
157
157
|
import_names = set()
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
pixi_lib = node_dir / ".pixi" / "envs" / "default" / "lib"
|
|
159
|
+
pixi_base = node_dir / ".pixi" / "envs" / "default"
|
|
161
160
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
python_dirs = list(pixi_lib.glob("python3.*"))
|
|
167
|
-
if not python_dirs:
|
|
168
|
-
return import_names
|
|
161
|
+
# Find site-packages (different paths on Windows vs Linux)
|
|
162
|
+
# Linux: .pixi/envs/default/lib/python3.x/site-packages
|
|
163
|
+
# Windows: .pixi/envs/default/Lib/site-packages
|
|
164
|
+
site_packages = None
|
|
169
165
|
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
# Try Windows path first (Lib/site-packages)
|
|
167
|
+
win_site = pixi_base / "Lib" / "site-packages"
|
|
168
|
+
if win_site.exists():
|
|
169
|
+
site_packages = win_site
|
|
170
|
+
else:
|
|
171
|
+
# Try Linux path (lib/python3.x/site-packages)
|
|
172
|
+
pixi_lib = pixi_base / "lib"
|
|
173
|
+
if pixi_lib.exists():
|
|
174
|
+
python_dirs = list(pixi_lib.glob("python3.*"))
|
|
175
|
+
if python_dirs:
|
|
176
|
+
site_packages = python_dirs[0] / "site-packages"
|
|
177
|
+
|
|
178
|
+
if site_packages is None or not site_packages.exists():
|
|
172
179
|
return import_names
|
|
173
180
|
|
|
174
181
|
# Scan for importable modules
|
|
@@ -252,10 +252,29 @@ def _serialize_for_ipc(obj, visited=None):
|
|
|
252
252
|
# Worker script template - minimal, runs in target venv
|
|
253
253
|
_WORKER_SCRIPT = '''
|
|
254
254
|
import sys
|
|
255
|
+
import os
|
|
255
256
|
import json
|
|
256
257
|
import traceback
|
|
257
258
|
from types import SimpleNamespace
|
|
258
259
|
|
|
260
|
+
# On Windows, add DLL directories for proper library loading
|
|
261
|
+
if sys.platform == "win32" and hasattr(os, "add_dll_directory"):
|
|
262
|
+
_host_python_dir = os.environ.get("COMFYUI_HOST_PYTHON_DIR")
|
|
263
|
+
if _host_python_dir:
|
|
264
|
+
try:
|
|
265
|
+
os.add_dll_directory(_host_python_dir)
|
|
266
|
+
_dlls_dir = os.path.join(_host_python_dir, "DLLs")
|
|
267
|
+
if os.path.isdir(_dlls_dir):
|
|
268
|
+
os.add_dll_directory(_dlls_dir)
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
271
|
+
_pixi_library_bin = os.environ.get("COMFYUI_PIXI_LIBRARY_BIN")
|
|
272
|
+
if _pixi_library_bin:
|
|
273
|
+
try:
|
|
274
|
+
os.add_dll_directory(_pixi_library_bin)
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
259
278
|
def _deserialize_isolated_objects(obj):
|
|
260
279
|
"""Reconstruct objects serialized with __isolated_object__ marker."""
|
|
261
280
|
if isinstance(obj, dict):
|
|
@@ -475,12 +494,29 @@ class VenvWorker(Worker):
|
|
|
475
494
|
env.update(self.extra_env)
|
|
476
495
|
env["COMFYUI_ISOLATION_WORKER"] = "1"
|
|
477
496
|
|
|
478
|
-
# For conda/pixi environments, add lib dir to LD_LIBRARY_PATH
|
|
497
|
+
# For conda/pixi environments, add lib dir to LD_LIBRARY_PATH (Linux)
|
|
479
498
|
lib_dir = self.python.parent.parent / "lib"
|
|
480
499
|
if lib_dir.is_dir():
|
|
481
500
|
existing = env.get("LD_LIBRARY_PATH", "")
|
|
482
501
|
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{existing}" if existing else str(lib_dir)
|
|
483
502
|
|
|
503
|
+
# On Windows, pass host Python directory and pixi Library/bin for DLL loading
|
|
504
|
+
if sys.platform == "win32":
|
|
505
|
+
env["COMFYUI_HOST_PYTHON_DIR"] = str(Path(sys.executable).parent)
|
|
506
|
+
|
|
507
|
+
# For pixi environments with MKL, add Library/bin to PATH for DLL loading
|
|
508
|
+
# Pixi has python.exe directly in env dir, not in Scripts/
|
|
509
|
+
env_dir = self.python.parent
|
|
510
|
+
library_bin = env_dir / "Library" / "bin"
|
|
511
|
+
if library_bin.is_dir():
|
|
512
|
+
existing_path = env.get("PATH", "")
|
|
513
|
+
env["PATH"] = f"{env_dir};{library_bin};{existing_path}"
|
|
514
|
+
env["COMFYUI_PIXI_LIBRARY_BIN"] = str(library_bin)
|
|
515
|
+
# Allow duplicate OpenMP libraries (MKL's libiomp5md.dll + PyTorch's libomp.dll)
|
|
516
|
+
env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
517
|
+
# Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
|
|
518
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
519
|
+
|
|
484
520
|
# Run subprocess
|
|
485
521
|
cmd = [
|
|
486
522
|
str(self.python),
|
|
@@ -572,8 +608,13 @@ import json
|
|
|
572
608
|
import socket
|
|
573
609
|
import struct
|
|
574
610
|
import traceback
|
|
611
|
+
import faulthandler
|
|
575
612
|
from types import SimpleNamespace
|
|
576
613
|
|
|
614
|
+
# Enable faulthandler to dump traceback on SIGSEGV/SIGABRT/etc
|
|
615
|
+
faulthandler.enable(file=sys.stderr, all_threads=True)
|
|
616
|
+
print("[worker] Faulthandler enabled", flush=True)
|
|
617
|
+
|
|
577
618
|
# On Windows, add host Python's DLL directories so packages like opencv can find VC++ runtime
|
|
578
619
|
if sys.platform == "win32":
|
|
579
620
|
_host_python_dir = os.environ.get("COMFYUI_HOST_PYTHON_DIR")
|
|
@@ -587,6 +628,15 @@ if sys.platform == "win32":
|
|
|
587
628
|
except Exception:
|
|
588
629
|
pass
|
|
589
630
|
|
|
631
|
+
# For pixi environments with MKL, add Library/bin for MKL DLLs
|
|
632
|
+
_pixi_library_bin = os.environ.get("COMFYUI_PIXI_LIBRARY_BIN")
|
|
633
|
+
if _pixi_library_bin and hasattr(os, "add_dll_directory"):
|
|
634
|
+
try:
|
|
635
|
+
os.add_dll_directory(_pixi_library_bin)
|
|
636
|
+
print(f"[worker] Added pixi Library/bin to DLL search: {_pixi_library_bin}", flush=True)
|
|
637
|
+
except Exception as e:
|
|
638
|
+
print(f"[worker] Failed to add pixi Library/bin: {e}", flush=True)
|
|
639
|
+
|
|
590
640
|
# =============================================================================
|
|
591
641
|
# Object Reference System - keep complex objects in worker, pass refs to host
|
|
592
642
|
# =============================================================================
|
|
@@ -741,20 +791,24 @@ def _deserialize_isolated_objects(obj):
|
|
|
741
791
|
|
|
742
792
|
|
|
743
793
|
def main():
|
|
794
|
+
print("[worker] Starting...", flush=True)
|
|
744
795
|
# Get socket address from command line
|
|
745
796
|
if len(sys.argv) < 2:
|
|
746
797
|
print("Usage: worker.py <socket_addr>", file=sys.stderr)
|
|
747
798
|
sys.exit(1)
|
|
748
799
|
socket_addr = sys.argv[1]
|
|
800
|
+
print(f"[worker] Connecting to {socket_addr}...", flush=True)
|
|
749
801
|
|
|
750
802
|
# Connect to host process
|
|
751
803
|
sock = _connect(socket_addr)
|
|
752
804
|
transport = SocketTransport(sock)
|
|
805
|
+
print("[worker] Connected, waiting for config...", flush=True)
|
|
753
806
|
|
|
754
807
|
# Read config as first message
|
|
755
808
|
config = transport.recv()
|
|
756
809
|
if not config:
|
|
757
810
|
return
|
|
811
|
+
print("[worker] Got config, setting up paths...", flush=True)
|
|
758
812
|
|
|
759
813
|
# Setup sys.path
|
|
760
814
|
for p in config.get("sys_paths", []):
|
|
@@ -762,10 +816,13 @@ def main():
|
|
|
762
816
|
sys.path.insert(0, p)
|
|
763
817
|
|
|
764
818
|
# Import torch after path setup
|
|
819
|
+
print("[worker] Importing torch...", flush=True)
|
|
765
820
|
import torch
|
|
821
|
+
print(f"[worker] Torch imported: {torch.__version__}", flush=True)
|
|
766
822
|
|
|
767
823
|
# Signal ready
|
|
768
824
|
transport.send({"status": "ready"})
|
|
825
|
+
print("[worker] Ready, entering request loop...", flush=True)
|
|
769
826
|
|
|
770
827
|
# Process requests
|
|
771
828
|
while True:
|
|
@@ -784,30 +841,41 @@ def main():
|
|
|
784
841
|
module_name = request["module"]
|
|
785
842
|
inputs_path = request.get("inputs_path")
|
|
786
843
|
outputs_path = request.get("outputs_path")
|
|
844
|
+
print(f"[worker] Request: {request_type} {module_name}", flush=True)
|
|
787
845
|
|
|
788
846
|
# Load inputs
|
|
789
847
|
if inputs_path:
|
|
848
|
+
print(f"[worker] Loading inputs from {inputs_path}...", flush=True)
|
|
790
849
|
inputs = torch.load(inputs_path, weights_only=False)
|
|
850
|
+
print(f"[worker] Deserializing isolated objects...", flush=True)
|
|
791
851
|
inputs = _deserialize_isolated_objects(inputs)
|
|
792
852
|
# Resolve any object references from previous node calls
|
|
853
|
+
print(f"[worker] Resolving object references...", flush=True)
|
|
793
854
|
inputs = _deserialize_input(inputs)
|
|
855
|
+
print(f"[worker] Inputs ready: {list(inputs.keys())}", flush=True)
|
|
794
856
|
else:
|
|
795
857
|
inputs = {}
|
|
796
858
|
|
|
797
859
|
# Import module
|
|
860
|
+
print(f"[worker] Importing module {module_name}...", flush=True)
|
|
798
861
|
module = __import__(module_name, fromlist=[""])
|
|
862
|
+
print(f"[worker] Module imported", flush=True)
|
|
799
863
|
|
|
800
864
|
if request_type == "call_method":
|
|
801
865
|
class_name = request["class_name"]
|
|
802
866
|
method_name = request["method_name"]
|
|
803
867
|
self_state = request.get("self_state")
|
|
868
|
+
print(f"[worker] Getting class {class_name}...", flush=True)
|
|
804
869
|
|
|
805
870
|
cls = getattr(module, class_name)
|
|
871
|
+
print(f"[worker] Creating instance...", flush=True)
|
|
806
872
|
instance = object.__new__(cls)
|
|
807
873
|
if self_state:
|
|
808
874
|
instance.__dict__.update(self_state)
|
|
875
|
+
print(f"[worker] Calling {method_name}...", flush=True)
|
|
809
876
|
method = getattr(instance, method_name)
|
|
810
877
|
result = method(**inputs)
|
|
878
|
+
print(f"[worker] Method returned", flush=True)
|
|
811
879
|
else:
|
|
812
880
|
func_name = request["func"]
|
|
813
881
|
func = getattr(module, func_name)
|
|
@@ -892,6 +960,10 @@ class PersistentVenvWorker(Worker):
|
|
|
892
960
|
self._socket_addr: Optional[str] = None
|
|
893
961
|
self._transport: Optional[SocketTransport] = None
|
|
894
962
|
|
|
963
|
+
# Stderr buffer for crash diagnostics
|
|
964
|
+
self._stderr_buffer: List[str] = []
|
|
965
|
+
self._stderr_lock = threading.Lock()
|
|
966
|
+
|
|
895
967
|
# Write worker script to temp file
|
|
896
968
|
self._worker_script = self._temp_dir / "persistent_worker.py"
|
|
897
969
|
self._worker_script.write_text(_PERSISTENT_WORKER_SCRIPT)
|
|
@@ -950,6 +1022,22 @@ class PersistentVenvWorker(Worker):
|
|
|
950
1022
|
if sys.platform == "win32":
|
|
951
1023
|
env["COMFYUI_HOST_PYTHON_DIR"] = str(Path(sys.executable).parent)
|
|
952
1024
|
|
|
1025
|
+
# For pixi environments with MKL, add Library/bin to PATH for DLL loading
|
|
1026
|
+
# MKL DLLs are in .pixi/envs/default/Library/bin/
|
|
1027
|
+
# Pixi has python.exe directly in env dir, not in Scripts/
|
|
1028
|
+
env_dir = self.python.parent
|
|
1029
|
+
library_bin = env_dir / "Library" / "bin"
|
|
1030
|
+
if library_bin.is_dir():
|
|
1031
|
+
existing_path = env.get("PATH", "")
|
|
1032
|
+
# Add env dir and Library/bin to PATH
|
|
1033
|
+
env["PATH"] = f"{env_dir};{library_bin};{existing_path}"
|
|
1034
|
+
# Also pass as env var so worker can use os.add_dll_directory()
|
|
1035
|
+
env["COMFYUI_PIXI_LIBRARY_BIN"] = str(library_bin)
|
|
1036
|
+
# Allow duplicate OpenMP libraries (MKL's libiomp5md.dll + PyTorch's libomp.dll)
|
|
1037
|
+
env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
1038
|
+
# Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
|
|
1039
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
1040
|
+
|
|
953
1041
|
# Find ComfyUI base and set env var for folder_paths stub
|
|
954
1042
|
comfyui_base = self._find_comfyui_base()
|
|
955
1043
|
if comfyui_base:
|
|
@@ -964,13 +1052,17 @@ class PersistentVenvWorker(Worker):
|
|
|
964
1052
|
[str(self.python), str(self._worker_script), self._socket_addr],
|
|
965
1053
|
stdin=subprocess.DEVNULL,
|
|
966
1054
|
stdout=subprocess.PIPE,
|
|
967
|
-
stderr=subprocess.
|
|
1055
|
+
stderr=subprocess.PIPE, # Capture stderr separately for crash diagnostics
|
|
968
1056
|
cwd=str(self.working_dir),
|
|
969
1057
|
env=env,
|
|
970
1058
|
)
|
|
971
1059
|
|
|
972
|
-
#
|
|
973
|
-
|
|
1060
|
+
# Clear stderr buffer for new process
|
|
1061
|
+
with self._stderr_lock:
|
|
1062
|
+
self._stderr_buffer.clear()
|
|
1063
|
+
|
|
1064
|
+
# Start stdout forwarding thread
|
|
1065
|
+
def forward_stdout():
|
|
974
1066
|
try:
|
|
975
1067
|
for line in self._process.stdout:
|
|
976
1068
|
if isinstance(line, bytes):
|
|
@@ -979,22 +1071,43 @@ class PersistentVenvWorker(Worker):
|
|
|
979
1071
|
sys.stderr.flush()
|
|
980
1072
|
except:
|
|
981
1073
|
pass
|
|
982
|
-
self.
|
|
983
|
-
self.
|
|
1074
|
+
self._stdout_thread = threading.Thread(target=forward_stdout, daemon=True)
|
|
1075
|
+
self._stdout_thread.start()
|
|
1076
|
+
|
|
1077
|
+
# Start stderr capture thread (buffer for crash diagnostics)
|
|
1078
|
+
def capture_stderr():
|
|
1079
|
+
try:
|
|
1080
|
+
for line in self._process.stderr:
|
|
1081
|
+
if isinstance(line, bytes):
|
|
1082
|
+
line = line.decode('utf-8', errors='replace')
|
|
1083
|
+
# Print to terminal AND buffer for crash reporting
|
|
1084
|
+
sys.stderr.write(f" [stderr] {line}")
|
|
1085
|
+
sys.stderr.flush()
|
|
1086
|
+
with self._stderr_lock:
|
|
1087
|
+
self._stderr_buffer.append(line.rstrip())
|
|
1088
|
+
# Keep last 50 lines
|
|
1089
|
+
if len(self._stderr_buffer) > 50:
|
|
1090
|
+
self._stderr_buffer.pop(0)
|
|
1091
|
+
except:
|
|
1092
|
+
pass
|
|
1093
|
+
self._stderr_thread = threading.Thread(target=capture_stderr, daemon=True)
|
|
1094
|
+
self._stderr_thread.start()
|
|
984
1095
|
|
|
985
1096
|
# Accept connection from worker with timeout
|
|
986
1097
|
self._server_socket.settimeout(60)
|
|
987
1098
|
try:
|
|
988
1099
|
client_sock, _ = self._server_socket.accept()
|
|
989
1100
|
except socket.timeout:
|
|
990
|
-
stderr
|
|
1101
|
+
# Collect stderr from buffer
|
|
1102
|
+
time.sleep(0.2) # Give stderr thread time to capture
|
|
1103
|
+
with self._stderr_lock:
|
|
1104
|
+
stderr = "\n".join(self._stderr_buffer) if self._stderr_buffer else "(no stderr captured)"
|
|
991
1105
|
try:
|
|
992
1106
|
self._process.kill()
|
|
993
|
-
|
|
994
|
-
stderr = stdout.decode('utf-8', errors='replace') if stdout else ""
|
|
1107
|
+
self._process.wait(timeout=5)
|
|
995
1108
|
except:
|
|
996
1109
|
pass
|
|
997
|
-
raise RuntimeError(f"{self.name}: Worker failed to connect (timeout)
|
|
1110
|
+
raise RuntimeError(f"{self.name}: Worker failed to connect (timeout).\nStderr:\n{stderr}")
|
|
998
1111
|
finally:
|
|
999
1112
|
self._server_socket.settimeout(None)
|
|
1000
1113
|
|
|
@@ -1035,7 +1148,33 @@ class PersistentVenvWorker(Worker):
|
|
|
1035
1148
|
self._transport.send(request)
|
|
1036
1149
|
|
|
1037
1150
|
# Read response with timeout
|
|
1038
|
-
|
|
1151
|
+
try:
|
|
1152
|
+
response = self._transport.recv(timeout=timeout)
|
|
1153
|
+
except ConnectionError as e:
|
|
1154
|
+
# Socket closed - check if worker process died
|
|
1155
|
+
self._shutdown = True
|
|
1156
|
+
time.sleep(0.2) # Give process time to fully exit and stderr to flush
|
|
1157
|
+
exit_code = None
|
|
1158
|
+
if self._process:
|
|
1159
|
+
exit_code = self._process.poll()
|
|
1160
|
+
|
|
1161
|
+
# Get captured stderr
|
|
1162
|
+
with self._stderr_lock:
|
|
1163
|
+
stderr_output = "\n".join(self._stderr_buffer) if self._stderr_buffer else "(no stderr captured)"
|
|
1164
|
+
|
|
1165
|
+
if exit_code is not None:
|
|
1166
|
+
raise RuntimeError(
|
|
1167
|
+
f"{self.name}: Worker process died with exit code {exit_code}. "
|
|
1168
|
+
f"This usually indicates a crash in native code (CGAL, pymeshlab, etc.).\n"
|
|
1169
|
+
f"Stderr:\n{stderr_output}"
|
|
1170
|
+
) from e
|
|
1171
|
+
else:
|
|
1172
|
+
# Process still alive but socket closed - something weird
|
|
1173
|
+
raise RuntimeError(
|
|
1174
|
+
f"{self.name}: Socket closed but worker process still running. "
|
|
1175
|
+
f"This may indicate a protocol error or worker bug.\n"
|
|
1176
|
+
f"Stderr:\n{stderr_output}"
|
|
1177
|
+
) from e
|
|
1039
1178
|
|
|
1040
1179
|
if response is None:
|
|
1041
1180
|
# Timeout - kill process
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|